Loggest thine Stuff
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

450 lines
11 KiB

2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
  1. package mysql
  2. import (
  3. "context"
  4. "database/sql"
  5. "git.aiterp.net/stufflog3/stufflog3/entities"
  6. "git.aiterp.net/stufflog3/stufflog3/internal/genutils"
  7. "git.aiterp.net/stufflog3/stufflog3/models"
  8. "git.aiterp.net/stufflog3/stufflog3/ports/mysql/mysqlcore"
  9. "git.aiterp.net/stufflog3/stufflog3/ports/mysql/sqltypes"
  10. "github.com/Masterminds/squirrel"
  11. "sort"
  12. "strings"
  13. )
  14. type itemRepository struct {
  15. db *sql.DB
  16. q *mysqlcore.Queries
  17. }
  18. func (r *itemRepository) Find(ctx context.Context, scopeID, itemID int) (*entities.Item, error) {
  19. row, err := r.q.GetItem(ctx, mysqlcore.GetItemParams{ScopeID: scopeID, ID: itemID})
  20. if err != nil {
  21. return nil, err
  22. }
  23. tags, err := r.q.ListTagsByObject(ctx, mysqlcore.ListTagsByObjectParams{
  24. ObjectKind: tagObjectKindItem,
  25. ObjectID: row.ID,
  26. })
  27. return &entities.Item{
  28. ID: row.ID,
  29. ScopeID: row.ScopeID,
  30. OwnerID: row.OwnerID,
  31. ProjectID: intPtr(row.ProjectID),
  32. RequirementID: intPtr(row.ProjectRequirementID),
  33. Name: row.Name,
  34. Description: row.Description,
  35. CreatedTime: row.CreatedTime,
  36. AcquiredTime: timePtr(row.AcquiredTime),
  37. ScheduledDate: row.ScheduledDate.AsPtr(),
  38. Tags: tags,
  39. }, nil
  40. }
  41. func (r *itemRepository) Fetch(ctx context.Context, filter models.ItemFilter) ([]entities.Item, error) {
  42. // Blank arrays are not the same as nulls
  43. if filter.IDs != nil && len(filter.IDs) == 0 {
  44. return []entities.Item{}, nil
  45. }
  46. if filter.ScopeIDs != nil && len(filter.ScopeIDs) == 0 {
  47. return []entities.Item{}, nil
  48. }
  49. if filter.ProjectIDs != nil && len(filter.ProjectIDs) == 0 {
  50. return []entities.Item{}, nil
  51. }
  52. if filter.RequirementIDs != nil && len(filter.RequirementIDs) == 0 {
  53. return []entities.Item{}, nil
  54. }
  55. if filter.StatIDs != nil && len(filter.StatIDs) == 0 {
  56. return []entities.Item{}, nil
  57. }
  58. if filter.Tags != nil && len(filter.Tags) == 0 {
  59. return []entities.Item{}, nil
  60. }
  61. sq := squirrel.Select(
  62. "i.id, i.scope_id, i.project_requirement_id, pr.project_id, i.owner_id, i.name," +
  63. " i.description, i.created_time, i.acquired_time, i.scheduled_date",
  64. ).From("item i").LeftJoin("project_requirement pr ON pr.id = i.project_requirement_id")
  65. dateOr := squirrel.Or{}
  66. if filter.CreatedTime != nil {
  67. dateOr = append(dateOr, squirrel.And{
  68. squirrel.GtOrEq{"i.created_time": filter.CreatedTime.Min},
  69. squirrel.Lt{"i.created_time": filter.CreatedTime.Max},
  70. })
  71. }
  72. if filter.UnAcquired {
  73. sq = sq.Where("i.acquired_time IS NULL")
  74. } else if filter.AcquiredTime != nil {
  75. dateOr = append(dateOr, squirrel.And{
  76. squirrel.GtOrEq{"i.acquired_time": filter.AcquiredTime.Min},
  77. squirrel.Lt{"i.acquired_time": filter.AcquiredTime.Max},
  78. })
  79. }
  80. if filter.UnScheduled {
  81. sq = sq.Where("i.scheduled_date IS NULL")
  82. } else if filter.ScheduledDate != nil {
  83. dateOr = append(dateOr, squirrel.And{
  84. squirrel.GtOrEq{"i.scheduled_date": filter.ScheduledDate.Min.AsTime()},
  85. squirrel.Lt{"i.scheduled_date": filter.ScheduledDate.Max.AsTime()},
  86. })
  87. }
  88. if len(dateOr) > 0 {
  89. sq = sq.Where(dateOr)
  90. }
  91. if len(filter.IDs) > 0 {
  92. sq = sq.Where(squirrel.Eq{"i.id": filter.IDs})
  93. }
  94. if len(filter.ScopeIDs) > 0 {
  95. sq = sq.Where(squirrel.Eq{"i.scope_id": filter.ScopeIDs})
  96. }
  97. if len(filter.RequirementIDs) > 0 {
  98. sq = sq.Where(squirrel.Eq{"i.project_requirement_id": filter.RequirementIDs})
  99. }
  100. if len(filter.ProjectIDs) > 0 {
  101. sq = sq.Where(squirrel.Eq{"pr.project_id": filter.ProjectIDs})
  102. }
  103. if len(filter.StatIDs) > 0 {
  104. sq = sq.LeftJoin("item_stat_progress isp ON isp.item_id = i.id")
  105. sq = sq.Where(squirrel.Eq{"isp.stat_id": filter.StatIDs})
  106. }
  107. if filter.OwnerID != nil {
  108. sq = sq.Where(squirrel.Eq{"i.owner_id": filter.OwnerID})
  109. }
  110. if filter.Loose {
  111. sq = sq.Where("i.project_requirement_id IS NULL")
  112. }
  113. query, params, err := sq.ToSql()
  114. if err != nil {
  115. return nil, err
  116. }
  117. rows, err := r.db.QueryContext(ctx, query, params...)
  118. if err != nil {
  119. if err == sql.ErrNoRows {
  120. return []entities.Item{}, nil
  121. }
  122. return nil, err
  123. }
  124. seen := make(map[int]bool, 32)
  125. res := make([]entities.Item, 0, 32)
  126. ids := genutils.Set[int]{}
  127. projectIDs := genutils.Set[int]{}
  128. requirementIDs := genutils.Set[int]{}
  129. for rows.Next() {
  130. item := entities.Item{}
  131. var projectRequirementId, projectId sql.NullInt32
  132. var acquiredTime sql.NullTime
  133. var scheduledDate sqltypes.NullDate
  134. err = rows.Scan(
  135. &item.ID,
  136. &item.ScopeID,
  137. &projectRequirementId,
  138. &projectId,
  139. &item.OwnerID,
  140. &item.Name,
  141. &item.Description,
  142. &item.CreatedTime,
  143. &acquiredTime,
  144. &scheduledDate,
  145. )
  146. if err != nil {
  147. if err == sql.ErrNoRows {
  148. break
  149. }
  150. return nil, err
  151. }
  152. if seen[item.ID] {
  153. continue
  154. }
  155. seen[item.ID] = true
  156. item.RequirementID = intPtr(projectRequirementId)
  157. item.ProjectID = intPtr(projectId)
  158. item.AcquiredTime = timePtr(acquiredTime)
  159. item.ScheduledDate = scheduledDate.AsPtr()
  160. item.Tags = []string{}
  161. ids.Add(item.ID)
  162. if item.ProjectID != nil {
  163. projectIDs.Add(*item.ProjectID)
  164. requirementIDs.Add(*item.RequirementID)
  165. }
  166. res = append(res, item)
  167. }
  168. err = fetchTags(ctx, r.db, tagObjectKindItem, ids.Values(), func(id int, tag string) {
  169. for i := range res {
  170. if id == res[i].ID {
  171. res[i].Tags = append(res[i].Tags, tag)
  172. }
  173. }
  174. })
  175. if err != nil {
  176. return nil, err
  177. }
  178. if len(filter.Tags) > 0 {
  179. projectTagMap := make(map[int][]string, 64)
  180. err = fetchTags(ctx, r.db, tagObjectKindProject, projectIDs.Values(), func(id int, tag string) {
  181. projectTagMap[id] = append(projectTagMap[id], tag)
  182. })
  183. if err != nil {
  184. return nil, err
  185. }
  186. requirementTagMap := make(map[int][]string, 64)
  187. err = fetchTags(ctx, r.db, tagObjectKindRequirement, requirementIDs.Values(), func(id int, tag string) {
  188. requirementTagMap[id] = append(requirementTagMap[id], tag)
  189. })
  190. if err != nil {
  191. return nil, err
  192. }
  193. res = genutils.RetainInPlace(res, func(item entities.Item) bool {
  194. good := false
  195. for _, tag := range filter.Tags {
  196. if strings.HasPrefix(tag, "!") {
  197. tag = tag[1:]
  198. if !item.HasTag(tag) {
  199. if item.RequirementID != nil {
  200. if !genutils.Contains(requirementTagMap[*item.RequirementID], tag) &&
  201. !genutils.Contains(projectTagMap[*item.ProjectID], tag) {
  202. return false
  203. }
  204. } else {
  205. return false
  206. }
  207. }
  208. good = true
  209. } else {
  210. if item.HasTag(tag) {
  211. good = true
  212. } else if item.RequirementID != nil &&
  213. (genutils.Contains(requirementTagMap[*item.RequirementID], tag) ||
  214. genutils.Contains(projectTagMap[*item.ProjectID], tag)) {
  215. good = true
  216. }
  217. }
  218. }
  219. return good
  220. })
  221. }
  222. sort.Slice(res, func(i, j int) bool {
  223. // Acquired time, descending
  224. ati, atj := res[i].AcquiredTime, res[j].AcquiredTime
  225. if ati == nil && atj != nil {
  226. return true
  227. }
  228. if ati != nil && atj == nil {
  229. return false
  230. }
  231. if ati != nil && atj != nil {
  232. return ati.After(*atj)
  233. }
  234. // Scheduled date, ascending
  235. cti, ctj := res[i].CreatedTime, res[j].CreatedTime
  236. sdi, sdj := res[i].ScheduledDate, res[j].ScheduledDate
  237. if sdi != nil && sdj == nil {
  238. return true
  239. }
  240. if sdi == nil && sdj != nil {
  241. return false
  242. }
  243. if sdi != nil && sdj != nil {
  244. if *sdi == *sdj {
  245. // This should change the behavior on the front page only. #hax
  246. if filter.UnAcquired {
  247. return cti.Before(ctj)
  248. } else {
  249. return cti.After(ctj)
  250. }
  251. }
  252. return sdi.Before(*sdj)
  253. }
  254. // Created time, descending
  255. return cti.After(ctj)
  256. })
  257. return res, nil
  258. }
  259. func (r *itemRepository) Insert(ctx context.Context, item entities.Item) (*entities.Item, error) {
  260. tx, err := r.db.BeginTx(ctx, nil)
  261. if err != nil {
  262. return nil, err
  263. }
  264. defer tx.Rollback()
  265. q := mysqlcore.New(tx)
  266. res, err := q.InsertItem(ctx, mysqlcore.InsertItemParams{
  267. ScopeID: item.ScopeID,
  268. ProjectRequirementID: sqlIntPtr(item.RequirementID),
  269. Name: item.Name,
  270. Description: item.Description,
  271. CreatedTime: item.CreatedTime,
  272. OwnerID: item.OwnerID,
  273. AcquiredTime: sqlTimePtr(item.AcquiredTime),
  274. ScheduledDate: sqlDatePtr(item.ScheduledDate),
  275. })
  276. if err != nil {
  277. return nil, err
  278. }
  279. id, err := res.LastInsertId()
  280. if err != nil {
  281. return nil, err
  282. }
  283. item.ID = int(id)
  284. for _, tag := range item.Tags {
  285. err := q.InsertTag(ctx, mysqlcore.InsertTagParams{
  286. ObjectKind: tagObjectKindItem,
  287. ObjectID: item.ID,
  288. TagName: tag,
  289. })
  290. if err != nil {
  291. return nil, err
  292. }
  293. }
  294. err = tx.Commit()
  295. if err != nil {
  296. return nil, err
  297. }
  298. return &item, nil
  299. }
  300. func (r *itemRepository) Update(ctx context.Context, item entities.Item, update models.ItemUpdate) error {
  301. item.ApplyUpdate(update)
  302. tx, err := r.db.BeginTx(ctx, nil)
  303. if err != nil {
  304. return err
  305. }
  306. defer tx.Rollback()
  307. q := mysqlcore.New(tx)
  308. err = q.UpdateItem(ctx, mysqlcore.UpdateItemParams{
  309. ProjectRequirementID: sqlIntPtr(item.RequirementID),
  310. Name: item.Name,
  311. Description: item.Description,
  312. AcquiredTime: sqlTimePtr(item.AcquiredTime),
  313. ScheduledDate: sqlDatePtr(item.ScheduledDate),
  314. OwnerID: item.OwnerID,
  315. ID: item.ID,
  316. })
  317. if err != nil {
  318. return err
  319. }
  320. for _, tag := range update.RemoveTags {
  321. err := q.DeleteTag(ctx, mysqlcore.DeleteTagParams{
  322. ObjectKind: tagObjectKindItem,
  323. ObjectID: item.ID,
  324. TagName: tag,
  325. })
  326. if err != nil {
  327. return err
  328. }
  329. }
  330. for _, tag := range update.AddTags {
  331. err = q.InsertTag(ctx, mysqlcore.InsertTagParams{
  332. ObjectKind: tagObjectKindItem,
  333. ObjectID: item.ID,
  334. TagName: tag,
  335. })
  336. if err != nil {
  337. return err
  338. }
  339. }
  340. return tx.Commit()
  341. }
  342. func (r *itemRepository) Delete(ctx context.Context, item entities.Item) error {
  343. err := r.q.DeleteTagByObject(ctx, mysqlcore.DeleteTagByObjectParams{
  344. ObjectKind: tagObjectKindItem,
  345. ObjectID: item.ID,
  346. })
  347. if err != nil {
  348. return err
  349. }
  350. return r.q.DeleteItem(ctx, item.ID)
  351. }
  352. func (r *itemRepository) ListStat(ctx context.Context, items ...entities.Item) ([]entities.ItemStat, error) {
  353. if len(items) == 0 {
  354. return []entities.ItemStat{}, nil
  355. }
  356. ids := make([]interface{}, 0, 64)
  357. for _, item := range items {
  358. ids = append(ids, item.ID)
  359. }
  360. query := `
  361. SELECT item_id, stat_id, acquired, required FROM item_stat_progress
  362. WHERE item_id IN (?` + strings.Repeat(",?", len(ids)-1) + `);
  363. `
  364. rows, err := r.db.QueryContext(ctx, query, ids...)
  365. if err != nil {
  366. if err == sql.ErrNoRows {
  367. return []entities.ItemStat{}, nil
  368. }
  369. return nil, err
  370. }
  371. res := make([]entities.ItemStat, 0, 8)
  372. for rows.Next() {
  373. progress := entities.ItemStat{}
  374. err = rows.Scan(&progress.ItemID, &progress.StatID, &progress.Acquired, &progress.Required)
  375. if err != nil {
  376. return nil, err
  377. }
  378. res = append(res, progress)
  379. }
  380. return res, nil
  381. }
  382. func (r *itemRepository) UpdateStat(ctx context.Context, item entities.ItemStat) error {
  383. if item.Required <= 0 {
  384. return r.q.DeleteItemStatProgress(ctx, mysqlcore.DeleteItemStatProgressParams{ItemID: item.ItemID, StatID: item.StatID})
  385. }
  386. return r.q.ReplaceItemStatProgress(ctx, mysqlcore.ReplaceItemStatProgressParams{
  387. ItemID: item.ItemID,
  388. StatID: item.StatID,
  389. Acquired: item.Acquired,
  390. Required: item.Required,
  391. })
  392. }