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.

638 lines
15 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
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. "github.com/Masterminds/squirrel"
  10. )
  11. type projectRepository struct {
  12. db *sql.DB
  13. q *mysqlcore.Queries
  14. }
  15. func (r *projectRepository) Find(ctx context.Context, scopeID, projectID int) (*entities.Project, error) {
  16. row, err := r.q.GetProject(ctx, mysqlcore.GetProjectParams{ID: projectID, ScopeID: scopeID})
  17. if err != nil {
  18. if err == sql.ErrNoRows {
  19. return nil, models.NotFoundError("Project")
  20. }
  21. return nil, err
  22. }
  23. tags, err := r.q.ListTagsByObject(ctx, mysqlcore.ListTagsByObjectParams{
  24. ObjectKind: tagObjectKindProject,
  25. ObjectID: row.ID,
  26. })
  27. return &entities.Project{
  28. ID: row.ID,
  29. ScopeID: row.ScopeID,
  30. OwnerID: row.OwnerID,
  31. CreatedTime: row.CreatedTime,
  32. Name: row.Name,
  33. Description: row.Description,
  34. Status: models.Status(row.Status),
  35. Tags: tags,
  36. }, nil
  37. }
  38. func (r *projectRepository) FetchProjects(ctx context.Context, scopeID int, ids ...int) ([]entities.Project, error) {
  39. if len(ids) == 0 {
  40. return []entities.Project{}, nil
  41. } else if len(ids) == 1 && scopeID != -1 {
  42. project, err := r.Find(ctx, scopeID, ids[0])
  43. if err != nil {
  44. return nil, err
  45. }
  46. return []entities.Project{*project}, nil
  47. }
  48. sq := squirrel.Select("id,scope_id,owner_id,name,status,description,created_time").
  49. From("project").
  50. Where(squirrel.Eq{"id": ids})
  51. if scopeID != -1 {
  52. sq = sq.Where(squirrel.Eq{"scope_id": scopeID})
  53. }
  54. query, args, err := sq.ToSql()
  55. if err != nil {
  56. return nil, err
  57. }
  58. rows, err := r.db.QueryContext(ctx, query, args...)
  59. if err != nil {
  60. return nil, err
  61. }
  62. projects := make([]entities.Project, 0, len(ids))
  63. for rows.Next() {
  64. project := entities.Project{}
  65. if err := rows.Scan(
  66. &project.ID,
  67. &project.ScopeID,
  68. &project.OwnerID,
  69. &project.Name,
  70. &project.Status,
  71. &project.Description,
  72. &project.CreatedTime,
  73. ); err != nil {
  74. return nil, err
  75. }
  76. project.Tags = []string{}
  77. projects = append(projects, project)
  78. }
  79. if err := rows.Close(); err != nil {
  80. return nil, err
  81. }
  82. if err := rows.Err(); err != nil {
  83. return nil, err
  84. }
  85. // Fill tags
  86. err = fetchTags(ctx, r.db, tagObjectKindProject, ids, func(id int, tag string) {
  87. for i := range projects {
  88. if projects[i].ID == id {
  89. projects[i].Tags = append(projects[i].Tags, tag)
  90. }
  91. }
  92. })
  93. if err != nil {
  94. return nil, err
  95. }
  96. return projects, nil
  97. }
  98. func (r *projectRepository) List(ctx context.Context, scopeID int) ([]entities.Project, error) {
  99. rows, err := r.q.ListProjects(ctx, scopeID)
  100. if err != nil {
  101. if err == sql.ErrNoRows {
  102. return []entities.Project{}, nil
  103. }
  104. return nil, err
  105. }
  106. res := make([]entities.Project, 0, len(rows))
  107. ids := make([]int, 0, len(rows))
  108. for _, row := range rows {
  109. res = append(res, entities.Project{
  110. ID: row.ID,
  111. ScopeID: row.ScopeID,
  112. OwnerID: row.OwnerID,
  113. CreatedTime: row.CreatedTime,
  114. Name: row.Name,
  115. Description: row.Description,
  116. Status: models.Status(row.Status),
  117. Tags: []string{},
  118. })
  119. ids = append(ids, row.ID)
  120. }
  121. // Fill tags
  122. err = fetchTags(ctx, r.db, tagObjectKindProject, ids, func(id int, tag string) {
  123. for i := range res {
  124. if id == res[i].ID {
  125. res[i].Tags = append(res[i].Tags, tag)
  126. }
  127. }
  128. })
  129. if err != nil {
  130. return nil, err
  131. }
  132. return res, nil
  133. }
  134. func (r *projectRepository) ListByTags(ctx context.Context, scopeID int, tags []string) ([]entities.Project, error) {
  135. query, args, err := squirrel.Select("object_id, tag_name").
  136. From("tag").
  137. Where(squirrel.Eq{"tag_name": tags, "object_kind": tagObjectKindProject}).
  138. ToSql()
  139. if err != nil {
  140. return nil, err
  141. }
  142. rows, err := r.db.QueryContext(ctx, query, args...)
  143. if err != nil {
  144. return nil, err
  145. }
  146. ids := make([]int, 0, 16)
  147. matches := make(map[int]int, 64)
  148. for rows.Next() {
  149. var objectID int
  150. var tagName string
  151. err := rows.Scan(&objectID, &tagName)
  152. if err != nil {
  153. return nil, err
  154. }
  155. if matches[objectID] == 0 {
  156. ids = append(ids, objectID)
  157. }
  158. matches[objectID] += 1
  159. }
  160. err = rows.Close()
  161. if err != nil {
  162. return nil, err
  163. }
  164. ids = genutils.RetainInPlace(ids, func(id int) bool {
  165. return matches[id] == len(tags)
  166. })
  167. return r.FetchProjects(ctx, scopeID, ids...)
  168. }
  169. func (r *projectRepository) Insert(ctx context.Context, project entities.Project) (*entities.Project, error) {
  170. tx, err := r.db.BeginTx(ctx, nil)
  171. if err != nil {
  172. return nil, err
  173. }
  174. defer tx.Rollback()
  175. q := r.q.WithTx(tx)
  176. res, err := q.InsertProject(ctx, mysqlcore.InsertProjectParams{
  177. ScopeID: project.ScopeID,
  178. OwnerID: project.OwnerID,
  179. Name: project.Name,
  180. Status: int(project.Status),
  181. Description: project.Description,
  182. })
  183. if err != nil {
  184. return nil, err
  185. }
  186. id, err := res.LastInsertId()
  187. if err != nil {
  188. return nil, err
  189. }
  190. project.ID = int(id)
  191. for _, tag := range project.Tags {
  192. err := q.InsertTag(ctx, mysqlcore.InsertTagParams{
  193. ObjectKind: tagObjectKindProject,
  194. ObjectID: project.ID,
  195. TagName: tag,
  196. })
  197. if err != nil {
  198. return nil, err
  199. }
  200. }
  201. err = tx.Commit()
  202. if err != nil {
  203. return nil, err
  204. }
  205. return &project, nil
  206. }
  207. func (r *projectRepository) Update(ctx context.Context, project entities.Project, update models.ProjectUpdate) error {
  208. tx, err := r.db.BeginTx(ctx, nil)
  209. if err != nil {
  210. return err
  211. }
  212. defer tx.Rollback()
  213. q := r.q.WithTx(tx)
  214. project.Update(update)
  215. err = q.UpdateProject(ctx, mysqlcore.UpdateProjectParams{
  216. OwnerID: project.OwnerID,
  217. Name: project.Name,
  218. Status: int(project.Status),
  219. Description: project.Description,
  220. ID: project.ID,
  221. ScopeID: project.ScopeID,
  222. })
  223. if err != nil {
  224. return err
  225. }
  226. for _, tag := range update.RemoveTags {
  227. err := q.DeleteTag(ctx, mysqlcore.DeleteTagParams{
  228. ObjectKind: tagObjectKindProject,
  229. ObjectID: project.ID,
  230. TagName: tag,
  231. })
  232. if err != nil {
  233. return err
  234. }
  235. }
  236. for _, tag := range update.AddTags {
  237. err = q.InsertTag(ctx, mysqlcore.InsertTagParams{
  238. ObjectKind: tagObjectKindProject,
  239. ObjectID: project.ID,
  240. TagName: tag,
  241. })
  242. if err != nil {
  243. return err
  244. }
  245. }
  246. return tx.Commit()
  247. }
  248. func (r *projectRepository) Delete(ctx context.Context, project entities.Project) error {
  249. tx, err := r.db.BeginTx(ctx, nil)
  250. if err != nil {
  251. return err
  252. }
  253. defer tx.Rollback()
  254. q := r.q.WithTx(tx)
  255. reqs, err := q.ListProjectRequirements(ctx, project.ID)
  256. if err != nil {
  257. return err
  258. }
  259. for _, req := range reqs {
  260. err = q.ClearItemProjectRequirement(ctx, sql.NullInt32{Valid: true, Int32: int32(req.ID)})
  261. if err != nil {
  262. return err
  263. }
  264. err = q.DeleteAllProjectRequirementStats(ctx, req.ID)
  265. if err != nil {
  266. return err
  267. }
  268. err = q.DeleteTagByObject(ctx, mysqlcore.DeleteTagByObjectParams{
  269. ObjectKind: tagObjectKindRequirement,
  270. ObjectID: req.ID,
  271. })
  272. if err != nil {
  273. return err
  274. }
  275. }
  276. err = q.DeleteTagByObject(ctx, mysqlcore.DeleteTagByObjectParams{
  277. ObjectKind: tagObjectKindProject,
  278. ObjectID: project.ID,
  279. })
  280. if err != nil {
  281. return err
  282. }
  283. err = q.DeleteAllProjectRequirements(ctx, project.ID)
  284. if err != nil {
  285. return err
  286. }
  287. err = q.DeleteProject(ctx, mysqlcore.DeleteProjectParams{ID: project.ID, ScopeID: project.ScopeID})
  288. if err != nil {
  289. return err
  290. }
  291. return tx.Commit()
  292. }
  293. func (r *projectRepository) FetchRequirements(ctx context.Context, scopeID int, requirementIDs ...int) ([]entities.Requirement, []entities.RequirementStat, error) {
  294. if len(requirementIDs) == 0 {
  295. return []entities.Requirement{}, []entities.RequirementStat{}, nil
  296. }
  297. sq := squirrel.Select("id, scope_id, project_id, name, status, description, is_coarse, aggregate_required").
  298. From("project_requirement").
  299. Where(squirrel.Eq{"id": requirementIDs})
  300. if scopeID != -1 {
  301. sq = sq.Where(squirrel.Eq{"scope_id": scopeID})
  302. }
  303. query, args, err := sq.ToSql()
  304. if err != nil {
  305. return nil, nil, err
  306. }
  307. rows, err := r.db.QueryContext(ctx, query, args...)
  308. if err != nil {
  309. return nil, nil, err
  310. }
  311. ids := make([]int, 0, 16)
  312. requirements := make([]entities.Requirement, 0, len(requirementIDs))
  313. for rows.Next() {
  314. requirement := entities.Requirement{}
  315. if err := rows.Scan(
  316. &requirement.ID,
  317. &requirement.ScopeID,
  318. &requirement.ProjectID,
  319. &requirement.Name,
  320. &requirement.Status,
  321. &requirement.Description,
  322. &requirement.IsCoarse,
  323. &requirement.AggregateRequired,
  324. ); err != nil {
  325. return nil, nil, err
  326. }
  327. requirement.Tags = []string{}
  328. requirements = append(requirements, requirement)
  329. ids = append(ids, requirement.ID)
  330. }
  331. // Fill tags
  332. err = fetchTags(ctx, r.db, tagObjectKindRequirement, ids, func(id int, tag string) {
  333. for i := range requirements {
  334. if id == requirements[i].ID {
  335. requirements[i].Tags = append(requirements[i].Tags, tag)
  336. break
  337. }
  338. }
  339. })
  340. if err != nil {
  341. return nil, nil, err
  342. }
  343. query, args, err = squirrel.Select("project_requirement_id, stat_id, required").
  344. From("project_requirement_stat").
  345. Where(squirrel.Eq{"project_requirement_id": requirementIDs}).
  346. ToSql()
  347. if err != nil {
  348. return nil, nil, err
  349. }
  350. rows, err = r.db.QueryContext(ctx, query, args...)
  351. if err != nil {
  352. return nil, nil, err
  353. }
  354. stats := make([]entities.RequirementStat, 0, len(requirementIDs))
  355. for rows.Next() {
  356. stat := entities.RequirementStat{}
  357. if err := rows.Scan(
  358. &stat.RequirementID,
  359. &stat.StatID,
  360. &stat.Required,
  361. ); err != nil {
  362. return nil, nil, err
  363. }
  364. stats = append(stats, stat)
  365. }
  366. return requirements, stats, nil
  367. }
  368. func (r *projectRepository) ListRequirementsByScope(ctx context.Context, scopeID int) ([]entities.Requirement, []entities.RequirementStat, error) {
  369. reqRows, err := r.q.ListProjectRequirementsByScopeID(ctx, scopeID)
  370. if err != nil && err != sql.ErrNoRows {
  371. return nil, nil, err
  372. }
  373. statsRows, err := r.q.ListProjectRequirementsStatsByScopeID(ctx, scopeID)
  374. if err != nil && err != sql.ErrNoRows {
  375. return nil, nil, err
  376. }
  377. return r.fillRequirements(ctx, reqRows, statsRows)
  378. }
  379. func (r *projectRepository) ListRequirements(ctx context.Context, projectID int) ([]entities.Requirement, []entities.RequirementStat, error) {
  380. reqRows, err := r.q.ListProjectRequirements(ctx, projectID)
  381. if err != nil && err != sql.ErrNoRows {
  382. return nil, nil, err
  383. }
  384. statsRows, err := r.q.ListProjectRequirementsStats(ctx, projectID)
  385. if err != nil && err != sql.ErrNoRows {
  386. return nil, nil, err
  387. }
  388. return r.fillRequirements(ctx, reqRows, statsRows)
  389. }
  390. func (r *projectRepository) fillRequirements(ctx context.Context, reqRows []mysqlcore.ProjectRequirement, statsRows []mysqlcore.ProjectRequirementStat) ([]entities.Requirement, []entities.RequirementStat, error) {
  391. requirements := make([]entities.Requirement, 0, len(reqRows))
  392. ids := make([]int, 0, len(reqRows))
  393. for _, row := range reqRows {
  394. requirements = append(requirements, entities.Requirement{
  395. ID: row.ID,
  396. ScopeID: row.ScopeID,
  397. ProjectID: row.ProjectID,
  398. Name: row.Name,
  399. Description: row.Description,
  400. IsCoarse: row.IsCoarse,
  401. AggregateRequired: row.AggregateRequired,
  402. Status: models.Status(row.Status),
  403. Tags: []string{},
  404. })
  405. ids = append(ids, row.ID)
  406. }
  407. // Fill tags
  408. err := fetchTags(ctx, r.db, tagObjectKindRequirement, ids, func(id int, tag string) {
  409. for i := range requirements {
  410. if id == requirements[i].ID {
  411. requirements[i].Tags = append(requirements[i].Tags, tag)
  412. break
  413. }
  414. }
  415. })
  416. if err != nil {
  417. return nil, nil, err
  418. }
  419. stats := make([]entities.RequirementStat, 0, len(statsRows))
  420. for _, row := range statsRows {
  421. stats = append(stats, entities.RequirementStat{
  422. RequirementID: row.ProjectRequirementID,
  423. StatID: row.StatID,
  424. Required: row.Required,
  425. })
  426. }
  427. return requirements, stats, nil
  428. }
  429. func (r *projectRepository) CreateRequirement(ctx context.Context, requirement entities.Requirement) (*entities.Requirement, error) {
  430. tx, err := r.db.BeginTx(ctx, nil)
  431. if err != nil {
  432. return nil, err
  433. }
  434. defer tx.Rollback()
  435. q := r.q.WithTx(tx)
  436. res, err := q.InsertProjectRequirement(ctx, mysqlcore.InsertProjectRequirementParams{
  437. ScopeID: requirement.ScopeID,
  438. ProjectID: requirement.ProjectID,
  439. Name: requirement.Name,
  440. Status: int(requirement.Status),
  441. Description: requirement.Description,
  442. IsCoarse: requirement.IsCoarse,
  443. AggregateRequired: requirement.AggregateRequired,
  444. })
  445. if err != nil {
  446. return nil, err
  447. }
  448. id, err := res.LastInsertId()
  449. if err != nil {
  450. return nil, err
  451. }
  452. for _, tag := range requirement.Tags {
  453. err := q.InsertTag(ctx, mysqlcore.InsertTagParams{
  454. ObjectKind: tagObjectKindRequirement,
  455. ObjectID: int(id),
  456. TagName: tag,
  457. })
  458. if err != nil {
  459. return nil, err
  460. }
  461. }
  462. err = tx.Commit()
  463. if err != nil {
  464. return nil, err
  465. }
  466. requirement.ID = int(id)
  467. return &requirement, nil
  468. }
  469. func (r *projectRepository) UpdateRequirement(ctx context.Context, requirement entities.Requirement, update models.RequirementUpdate) error {
  470. tx, err := r.db.BeginTx(ctx, nil)
  471. if err != nil {
  472. return err
  473. }
  474. defer tx.Rollback()
  475. q := r.q.WithTx(tx)
  476. requirement.Update(update)
  477. _ = q.UpdateProjectRequirement(ctx, mysqlcore.UpdateProjectRequirementParams{
  478. Name: requirement.Name,
  479. Status: int(requirement.Status),
  480. Description: requirement.Description,
  481. IsCoarse: requirement.IsCoarse,
  482. ID: requirement.ID,
  483. ScopeID: requirement.ScopeID,
  484. AggregateRequired: requirement.AggregateRequired,
  485. ProjectID: requirement.ProjectID,
  486. })
  487. for _, tag := range update.RemoveTags {
  488. err := q.DeleteTag(ctx, mysqlcore.DeleteTagParams{
  489. ObjectKind: tagObjectKindRequirement,
  490. ObjectID: requirement.ID,
  491. TagName: tag,
  492. })
  493. if err != nil {
  494. return err
  495. }
  496. }
  497. for _, tag := range update.AddTags {
  498. err = q.InsertTag(ctx, mysqlcore.InsertTagParams{
  499. ObjectKind: tagObjectKindRequirement,
  500. ObjectID: requirement.ID,
  501. TagName: tag,
  502. })
  503. if err != nil {
  504. return err
  505. }
  506. }
  507. return tx.Commit()
  508. }
  509. func (r *projectRepository) DeleteRequirement(ctx context.Context, requirement entities.Requirement) error {
  510. tx, err := r.db.BeginTx(ctx, nil)
  511. if err != nil {
  512. return err
  513. }
  514. defer tx.Rollback()
  515. q := r.q.WithTx(tx)
  516. err = q.ClearItemProjectRequirement(ctx, sql.NullInt32{Valid: true, Int32: int32(requirement.ID)})
  517. if err != nil {
  518. return err
  519. }
  520. err = q.DeleteAllProjectRequirementStats(ctx, requirement.ID)
  521. if err != nil {
  522. return err
  523. }
  524. err = q.DeleteProjectRequirement(ctx, mysqlcore.DeleteProjectRequirementParams{
  525. ScopeID: requirement.ScopeID,
  526. ID: requirement.ID,
  527. })
  528. if err != nil {
  529. return err
  530. }
  531. err = q.DeleteTagByObject(ctx, mysqlcore.DeleteTagByObjectParams{
  532. ObjectKind: tagObjectKindRequirement,
  533. ObjectID: requirement.ID,
  534. })
  535. if err != nil {
  536. return err
  537. }
  538. return tx.Commit()
  539. }
  540. func (r *projectRepository) UpsertRequirementStat(ctx context.Context, stat entities.RequirementStat) error {
  541. return r.q.ReplaceProjectRequirementStat(ctx, mysqlcore.ReplaceProjectRequirementStatParams{
  542. ProjectRequirementID: stat.RequirementID,
  543. StatID: stat.StatID,
  544. Required: stat.Required,
  545. })
  546. }
  547. func (r *projectRepository) DeleteRequirementStat(ctx context.Context, stat entities.RequirementStat) error {
  548. return r.q.DeleteProjectRequirementStat(ctx, mysqlcore.DeleteProjectRequirementStatParams{
  549. ProjectRequirementID: stat.RequirementID,
  550. StatID: stat.StatID,
  551. })
  552. }