entry_query_builder.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. // Copyright 2017 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
  5. import (
  6. "fmt"
  7. "strings"
  8. "time"
  9. "github.com/miniflux/miniflux2/helper"
  10. "github.com/miniflux/miniflux2/model"
  11. )
  12. // EntryQueryBuilder builds a SQL query to fetch entries.
  13. type EntryQueryBuilder struct {
  14. store *Storage
  15. feedID int64
  16. userID int64
  17. timezone string
  18. categoryID int64
  19. status string
  20. notStatus string
  21. order string
  22. direction string
  23. limit int
  24. offset int
  25. entryID int64
  26. }
  27. // WithEntryID set the entryID.
  28. func (e *EntryQueryBuilder) WithEntryID(entryID int64) *EntryQueryBuilder {
  29. e.entryID = entryID
  30. return e
  31. }
  32. // WithFeedID set the feedID.
  33. func (e *EntryQueryBuilder) WithFeedID(feedID int64) *EntryQueryBuilder {
  34. e.feedID = feedID
  35. return e
  36. }
  37. // WithCategoryID set the categoryID.
  38. func (e *EntryQueryBuilder) WithCategoryID(categoryID int64) *EntryQueryBuilder {
  39. e.categoryID = categoryID
  40. return e
  41. }
  42. // WithStatus set the entry status.
  43. func (e *EntryQueryBuilder) WithStatus(status string) *EntryQueryBuilder {
  44. e.status = status
  45. return e
  46. }
  47. // WithoutStatus set the entry status that should not be returned.
  48. func (e *EntryQueryBuilder) WithoutStatus(status string) *EntryQueryBuilder {
  49. e.notStatus = status
  50. return e
  51. }
  52. // WithOrder set the sorting order.
  53. func (e *EntryQueryBuilder) WithOrder(order string) *EntryQueryBuilder {
  54. e.order = order
  55. return e
  56. }
  57. // WithDirection set the sorting direction.
  58. func (e *EntryQueryBuilder) WithDirection(direction string) *EntryQueryBuilder {
  59. e.direction = direction
  60. return e
  61. }
  62. // WithLimit set the limit.
  63. func (e *EntryQueryBuilder) WithLimit(limit int) *EntryQueryBuilder {
  64. e.limit = limit
  65. return e
  66. }
  67. // WithOffset set the offset.
  68. func (e *EntryQueryBuilder) WithOffset(offset int) *EntryQueryBuilder {
  69. e.offset = offset
  70. return e
  71. }
  72. // CountEntries count the number of entries that match the condition.
  73. func (e *EntryQueryBuilder) CountEntries() (count int, err error) {
  74. defer helper.ExecutionTime(
  75. time.Now(),
  76. fmt.Sprintf("[EntryQueryBuilder:CountEntries] userID=%d, feedID=%d, status=%s", e.userID, e.feedID, e.status),
  77. )
  78. query := `SELECT count(*) FROM entries e LEFT JOIN feeds f ON f.id=e.feed_id WHERE %s`
  79. args, condition := e.buildCondition()
  80. err = e.store.db.QueryRow(fmt.Sprintf(query, condition), args...).Scan(&count)
  81. if err != nil {
  82. return 0, fmt.Errorf("unable to count entries: %v", err)
  83. }
  84. return count, nil
  85. }
  86. // GetEntry returns a single entry that match the condition.
  87. func (e *EntryQueryBuilder) GetEntry() (*model.Entry, error) {
  88. e.limit = 1
  89. entries, err := e.GetEntries()
  90. if err != nil {
  91. return nil, err
  92. }
  93. if len(entries) != 1 {
  94. return nil, nil
  95. }
  96. entries[0].Enclosures, err = e.store.GetEnclosures(entries[0].ID)
  97. if err != nil {
  98. return nil, err
  99. }
  100. return entries[0], nil
  101. }
  102. // GetEntries returns a list of entries that match the condition.
  103. func (e *EntryQueryBuilder) GetEntries() (model.Entries, error) {
  104. debugStr := "[EntryQueryBuilder:GetEntries] userID=%d, feedID=%d, categoryID=%d, status=%s, order=%s, direction=%s, offset=%d, limit=%d"
  105. defer helper.ExecutionTime(time.Now(), fmt.Sprintf(debugStr, e.userID, e.feedID, e.categoryID, e.status, e.order, e.direction, e.offset, e.limit))
  106. query := `
  107. SELECT
  108. e.id, e.user_id, e.feed_id, e.hash, e.published_at at time zone '%s', e.title, e.url, e.author, e.content, e.status,
  109. f.title as feed_title, f.feed_url, f.site_url, f.checked_at,
  110. f.category_id, c.title as category_title,
  111. fi.icon_id
  112. FROM entries e
  113. LEFT JOIN feeds f ON f.id=e.feed_id
  114. LEFT JOIN categories c ON c.id=f.category_id
  115. LEFT JOIN feed_icons fi ON fi.feed_id=f.id
  116. WHERE %s %s
  117. `
  118. args, conditions := e.buildCondition()
  119. query = fmt.Sprintf(query, e.timezone, conditions, e.buildSorting())
  120. // log.Println(query)
  121. rows, err := e.store.db.Query(query, args...)
  122. if err != nil {
  123. return nil, fmt.Errorf("unable to get entries: %v", err)
  124. }
  125. defer rows.Close()
  126. entries := make(model.Entries, 0)
  127. for rows.Next() {
  128. var entry model.Entry
  129. var iconID interface{}
  130. entry.Feed = &model.Feed{UserID: e.userID}
  131. entry.Feed.Category = &model.Category{UserID: e.userID}
  132. entry.Feed.Icon = &model.FeedIcon{}
  133. err := rows.Scan(
  134. &entry.ID,
  135. &entry.UserID,
  136. &entry.FeedID,
  137. &entry.Hash,
  138. &entry.Date,
  139. &entry.Title,
  140. &entry.URL,
  141. &entry.Author,
  142. &entry.Content,
  143. &entry.Status,
  144. &entry.Feed.Title,
  145. &entry.Feed.FeedURL,
  146. &entry.Feed.SiteURL,
  147. &entry.Feed.CheckedAt,
  148. &entry.Feed.Category.ID,
  149. &entry.Feed.Category.Title,
  150. &iconID,
  151. )
  152. if err != nil {
  153. return nil, fmt.Errorf("unable to fetch entry row: %v", err)
  154. }
  155. if iconID == nil {
  156. entry.Feed.Icon.IconID = 0
  157. } else {
  158. entry.Feed.Icon.IconID = iconID.(int64)
  159. }
  160. entry.Feed.ID = entry.FeedID
  161. entry.Feed.Icon.FeedID = entry.FeedID
  162. entries = append(entries, &entry)
  163. }
  164. return entries, nil
  165. }
  166. func (e *EntryQueryBuilder) buildCondition() ([]interface{}, string) {
  167. args := []interface{}{e.userID}
  168. conditions := []string{"e.user_id = $1"}
  169. if e.categoryID != 0 {
  170. conditions = append(conditions, fmt.Sprintf("f.category_id=$%d", len(args)+1))
  171. args = append(args, e.categoryID)
  172. }
  173. if e.feedID != 0 {
  174. conditions = append(conditions, fmt.Sprintf("e.feed_id=$%d", len(args)+1))
  175. args = append(args, e.feedID)
  176. }
  177. if e.entryID != 0 {
  178. conditions = append(conditions, fmt.Sprintf("e.id=$%d", len(args)+1))
  179. args = append(args, e.entryID)
  180. }
  181. if e.status != "" {
  182. conditions = append(conditions, fmt.Sprintf("e.status=$%d", len(args)+1))
  183. args = append(args, e.status)
  184. }
  185. if e.notStatus != "" {
  186. conditions = append(conditions, fmt.Sprintf("e.status != $%d", len(args)+1))
  187. args = append(args, e.notStatus)
  188. }
  189. return args, strings.Join(conditions, " AND ")
  190. }
  191. func (e *EntryQueryBuilder) buildSorting() string {
  192. var queries []string
  193. if e.order != "" {
  194. queries = append(queries, fmt.Sprintf(`ORDER BY "%s"`, e.order))
  195. }
  196. if e.direction != "" {
  197. queries = append(queries, fmt.Sprintf(`%s`, e.direction))
  198. }
  199. if e.limit != 0 {
  200. queries = append(queries, fmt.Sprintf(`LIMIT %d`, e.limit))
  201. }
  202. if e.offset != 0 {
  203. queries = append(queries, fmt.Sprintf(`OFFSET %d`, e.offset))
  204. }
  205. return strings.Join(queries, " ")
  206. }
  207. // NewEntryQueryBuilder returns a new EntryQueryBuilder.
  208. func NewEntryQueryBuilder(store *Storage, userID int64, timezone string) *EntryQueryBuilder {
  209. return &EntryQueryBuilder{
  210. store: store,
  211. userID: userID,
  212. timezone: timezone,
  213. }
  214. }