using Invercargill; using Invercargill.DataStructures; using Invercargill.Expressions; using InvercargillSql.Orm; using Inversion; using ClankerSaddle.Data; namespace ClankerSaddle.Services { /** Valid values of {@link ProjectLink.relationship}. */ public const string LINK_CO_DEVELOPED = "co-developed"; public const string LINK_REFERENCE = "reference"; /** * Project storage: CRUD over `projects`, the directed * co-developed/reference link graph in `project_links`, and the * project's LLM-authored notes. */ public class ProjectService : Object { private OrmSession db = inject(); /** Inserts a project; `summary` may be empty. */ public async Project create_project(string name, string git_url, string default_branch, string? summary) throws Error { var project = new Project() { name = name, git_url = git_url, default_branch = default_branch.length > 0 ? default_branch : "main", summary = summary != null && summary.length > 0 ? summary : null, created_at = new DateTime.now_local() }; yield db.insert_async(project); return project; } /** Loads the project identified by `id`, or null. */ public async Project? get_project(int64 id) throws Error { return yield db.query() .where(expr("id == $0", elem(id))) .first_async(); } /** Lists every project ordered by name. */ public async ImmutableLot list_projects() throws Error { return yield db.query() .order_by(expr("name")) .materialise_async(); } /** * Saves the editable fields of the project identified by `id`; * empty arguments keep their current values. Returns the saved * project, or null when no such project exists. */ public async Project? update_project(int64 id, string? name, string? git_url, string? default_branch, string? summary) throws Error { var project = yield get_project(id); if (project == null) { return null; } if (name != null && name.length > 0) { project.name = name; } if (git_url != null && git_url.length > 0) { project.git_url = git_url; } if (default_branch != null && default_branch.length > 0) { project.default_branch = default_branch; } if (summary != null) { project.summary = summary.length > 0 ? summary : null; } yield db.update_async(project); return project; } /** * Links `linked_id` to `parent_id` with `relationship` * ('co-developed' or 'reference'); self-links and duplicates are * rejected. */ public async ProjectLink add_link(int64 parent_id, int64 linked_id, string relationship) throws Error { if (relationship != LINK_CO_DEVELOPED && relationship != LINK_REFERENCE) { throw new Error(Quark.from_string("clanker-saddle"), 1, @"Relationship must be '$(LINK_CO_DEVELOPED)' or '$(LINK_REFERENCE)'"); } if (parent_id == linked_id) { throw new Error(Quark.from_string("clanker-saddle"), 2, "A project cannot be linked to itself"); } var parent = yield get_project(parent_id); var linked = yield get_project(linked_id); if (parent == null || linked == null) { throw new Error(Quark.from_string("clanker-saddle"), 3, "Both projects must exist"); } var existing = yield db.query() .where(expr("parent_project_id == $0 && linked_project_id == $1", elem(parent_id), elem(linked_id))) .materialise_async(); if (existing.length > 0) { throw new Error(Quark.from_string("clanker-saddle"), 4, "Those projects are already linked"); } var link = new ProjectLink() { parent_project_id = parent_id, linked_project_id = linked_id, relationship = relationship }; yield db.insert_async(link); return link; } /** Deletes the link identified by `id`. */ public async void remove_link(int64 id) throws Error { var link = yield db.query() .where(expr("id == $0", elem(id))) .first_async(); if (link != null) { yield db.delete_async((!)link); } } /** Lists every link whose parent is `project_id`. */ public async ImmutableLot list_links(int64 project_id) throws Error { return yield db.query() .where(expr("parent_project_id == $0", elem(project_id))) .materialise_async(); } /** Loads the project identified by `name`, or null. */ public async Project? get_project_by_name(string name) throws Error { return yield db.query() .where(expr("name == $0", elem(name))) .first_async(); } /** * Inserts a project note (LLM-authored via the callback endpoint). * `source_task_id` may be 0 (unknown source task). */ public async ProjectNote add_note(int64 project_id, int64 source_task_id, string body) throws Error { var note = new ProjectNote() { project_id = project_id, source_task_id = source_task_id, body = body, created_at = new DateTime.now_local() }; yield db.insert_async(note); return note; } /** Loads the note identified by `id`, or null. */ public async ProjectNote? get_note(int64 id) throws Error { return yield db.query() .where(expr("id == $0", elem(id))) .first_async(); } /** A project's notes, oldest first. */ public async ImmutableLot list_notes(int64 project_id) throws Error { return yield db.query() .where(expr("project_id == $0", elem(project_id))) .order_by(expr("id")) .materialise_async(); } /** Deletes the note identified by `id`. */ public async void delete_note(int64 id) throws Error { var note = yield get_note(id); if (note != null) { yield db.delete_async((!)note); } } } }