ProjectService.vala 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. using Invercargill;
  2. using Invercargill.DataStructures;
  3. using Invercargill.Expressions;
  4. using InvercargillSql.Orm;
  5. using Inversion;
  6. using ClankerSaddle.Data;
  7. namespace ClankerSaddle.Services {
  8. /** Valid values of {@link ProjectLink.relationship}. */
  9. public const string LINK_CO_DEVELOPED = "co-developed";
  10. public const string LINK_REFERENCE = "reference";
  11. /**
  12. * Project storage: CRUD over `projects`, the directed
  13. * co-developed/reference link graph in `project_links`, and the
  14. * project's LLM-authored notes.
  15. */
  16. public class ProjectService : Object {
  17. private OrmSession db = inject<OrmSession>();
  18. /** Inserts a project; `summary` may be empty. */
  19. public async Project create_project(string name, string git_url,
  20. string default_branch, string? summary) throws Error {
  21. var project = new Project() {
  22. name = name,
  23. git_url = git_url,
  24. default_branch = default_branch.length > 0 ? default_branch : "main",
  25. summary = summary != null && summary.length > 0 ? summary : null,
  26. created_at = new DateTime.now_local()
  27. };
  28. yield db.insert_async<Project>(project);
  29. return project;
  30. }
  31. /** Loads the project identified by `id`, or null. */
  32. public async Project? get_project(int64 id) throws Error {
  33. return yield db.query<Project>()
  34. .where(expr("id == $0", elem<int64?>(id)))
  35. .first_async();
  36. }
  37. /** Lists every project ordered by name. */
  38. public async ImmutableLot<Project> list_projects() throws Error {
  39. return yield db.query<Project>()
  40. .order_by(expr("name"))
  41. .materialise_async();
  42. }
  43. /**
  44. * Saves the editable fields of the project identified by `id`;
  45. * empty arguments keep their current values. Returns the saved
  46. * project, or null when no such project exists.
  47. */
  48. public async Project? update_project(int64 id, string? name, string? git_url,
  49. string? default_branch, string? summary) throws Error {
  50. var project = yield get_project(id);
  51. if (project == null) {
  52. return null;
  53. }
  54. if (name != null && name.length > 0) {
  55. project.name = name;
  56. }
  57. if (git_url != null && git_url.length > 0) {
  58. project.git_url = git_url;
  59. }
  60. if (default_branch != null && default_branch.length > 0) {
  61. project.default_branch = default_branch;
  62. }
  63. if (summary != null) {
  64. project.summary = summary.length > 0 ? summary : null;
  65. }
  66. yield db.update_async<Project>(project);
  67. return project;
  68. }
  69. /**
  70. * Links `linked_id` to `parent_id` with `relationship`
  71. * ('co-developed' or 'reference'); self-links and duplicates are
  72. * rejected.
  73. */
  74. public async ProjectLink add_link(int64 parent_id, int64 linked_id,
  75. string relationship) throws Error {
  76. if (relationship != LINK_CO_DEVELOPED && relationship != LINK_REFERENCE) {
  77. throw new Error(Quark.from_string("clanker-saddle"), 1,
  78. @"Relationship must be '$(LINK_CO_DEVELOPED)' or '$(LINK_REFERENCE)'");
  79. }
  80. if (parent_id == linked_id) {
  81. throw new Error(Quark.from_string("clanker-saddle"), 2,
  82. "A project cannot be linked to itself");
  83. }
  84. var parent = yield get_project(parent_id);
  85. var linked = yield get_project(linked_id);
  86. if (parent == null || linked == null) {
  87. throw new Error(Quark.from_string("clanker-saddle"), 3,
  88. "Both projects must exist");
  89. }
  90. var existing = yield db.query<ProjectLink>()
  91. .where(expr("parent_project_id == $0 && linked_project_id == $1",
  92. elem<int64?>(parent_id), elem<int64?>(linked_id)))
  93. .materialise_async();
  94. if (existing.length > 0) {
  95. throw new Error(Quark.from_string("clanker-saddle"), 4,
  96. "Those projects are already linked");
  97. }
  98. var link = new ProjectLink() {
  99. parent_project_id = parent_id,
  100. linked_project_id = linked_id,
  101. relationship = relationship
  102. };
  103. yield db.insert_async<ProjectLink>(link);
  104. return link;
  105. }
  106. /** Deletes the link identified by `id`. */
  107. public async void remove_link(int64 id) throws Error {
  108. var link = yield db.query<ProjectLink>()
  109. .where(expr("id == $0", elem<int64?>(id)))
  110. .first_async();
  111. if (link != null) {
  112. yield db.delete_async<ProjectLink>((!)link);
  113. }
  114. }
  115. /** Lists every link whose parent is `project_id`. */
  116. public async ImmutableLot<ProjectLink> list_links(int64 project_id) throws Error {
  117. return yield db.query<ProjectLink>()
  118. .where(expr("parent_project_id == $0", elem<int64?>(project_id)))
  119. .materialise_async();
  120. }
  121. /** Loads the project identified by `name`, or null. */
  122. public async Project? get_project_by_name(string name) throws Error {
  123. return yield db.query<Project>()
  124. .where(expr("name == $0", elem(name)))
  125. .first_async();
  126. }
  127. /**
  128. * Inserts a project note (LLM-authored via the callback endpoint).
  129. * `source_task_id` may be 0 (unknown source task).
  130. */
  131. public async ProjectNote add_note(int64 project_id, int64 source_task_id,
  132. string body) throws Error {
  133. var note = new ProjectNote() {
  134. project_id = project_id,
  135. source_task_id = source_task_id,
  136. body = body,
  137. created_at = new DateTime.now_local()
  138. };
  139. yield db.insert_async<ProjectNote>(note);
  140. return note;
  141. }
  142. /** Loads the note identified by `id`, or null. */
  143. public async ProjectNote? get_note(int64 id) throws Error {
  144. return yield db.query<ProjectNote>()
  145. .where(expr("id == $0", elem<int64?>(id)))
  146. .first_async();
  147. }
  148. /** A project's notes, oldest first. */
  149. public async ImmutableLot<ProjectNote> list_notes(int64 project_id) throws Error {
  150. return yield db.query<ProjectNote>()
  151. .where(expr("project_id == $0", elem<int64?>(project_id)))
  152. .order_by(expr("id"))
  153. .materialise_async();
  154. }
  155. /** Deletes the note identified by `id`. */
  156. public async void delete_note(int64 id) throws Error {
  157. var note = yield get_note(id);
  158. if (note != null) {
  159. yield db.delete_async<ProjectNote>((!)note);
  160. }
  161. }
  162. }
  163. }