| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266 |
- using Invercargill;
- using Invercargill.DataStructures;
- using Invercargill.Expressions;
- using InvercargillSql.Orm;
- using Inversion;
- using ClankerSaddle.Data;
- namespace ClankerSaddle.Services {
- /**
- * Stack storage (goal 8 task 2 B): CRUD over `stacks`, the stack's
- * project members in `stack_projects` (relationship 'co-developed'
- * or 'reference', validated like the project link graph), and the
- * two apply paths — a stack's members land as `project_links` rows
- * on a project or as `goal_links` rows on a goal, skipping exact
- * duplicates (same target, same relationship) so re-applying is
- * idempotent.
- */
- public class StackService : Object {
- private OrmSession db = inject<OrmSession>();
- private ProjectService projects = inject<ProjectService>();
- private GoalService goals = inject<GoalService>();
- private StatePulse pulse = inject<StatePulse>();
- /** Whether `relationship` is a valid stack-member relationship. */
- private void require_valid_relationship(string relationship) throws Error {
- if (relationship != LINK_CO_DEVELOPED && relationship != LINK_REFERENCE) {
- throw new Error(Quark.from_string("clanker-saddle"), 90,
- @"Relationship must be '$(LINK_CO_DEVELOPED)' or '$(LINK_REFERENCE)'");
- }
- }
- /** Inserts a stack; `summary` may be empty. */
- public async Stack create_stack(string name, string? summary) throws Error {
- string trimmed = name.strip();
- if (trimmed.length == 0) {
- throw new Error(Quark.from_string("clanker-saddle"), 91,
- "A stack name is required");
- }
- var existing = yield get_stack_by_name(trimmed);
- if (existing != null) {
- throw new Error(Quark.from_string("clanker-saddle"), 92,
- @"A stack named '$(trimmed)' already exists");
- }
- var stack = new Stack() {
- name = trimmed,
- summary = summary != null && summary.strip().length > 0
- ? summary.strip() : null,
- created_at = new DateTime.now_local()
- };
- yield db.insert_async<Stack>(stack);
- yield pulse.bump(0, "stack created");
- return stack;
- }
- /** Loads the stack identified by `id`, or null. */
- public async Stack? get_stack(int64 id) throws Error {
- return yield db.query<Stack>()
- .where(expr("id == $0", elem<int64?>(id)))
- .first_async();
- }
- /** Loads the stack identified by `name`, or null. */
- public async Stack? get_stack_by_name(string name) throws Error {
- return yield db.query<Stack>()
- .where(expr("name == $0", elem(name)))
- .first_async();
- }
- /** Every stack, newest first. */
- public async ImmutableLot<Stack> list_stacks() throws Error {
- return yield db.query<Stack>()
- .order_by_desc(expr("id"))
- .materialise_async();
- }
- /**
- * Saves the editable fields of the stack identified by `id`;
- * null/empty arguments keep their current values. Returns the
- * saved stack, or null when no such stack exists.
- */
- public async Stack? update_stack(int64 id, string? name, string? summary) throws Error {
- var stack = yield get_stack(id);
- if (stack == null) {
- return null;
- }
- if (name != null && name.strip().length > 0) {
- string trimmed = name.strip();
- var clash = yield get_stack_by_name(trimmed);
- if (clash != null && ((!)clash).id != id) {
- throw new Error(Quark.from_string("clanker-saddle"), 93,
- @"A stack named '$(trimmed)' already exists");
- }
- ((!)stack).name = trimmed;
- }
- if (summary != null) {
- ((!)stack).summary = summary.strip().length > 0 ? summary.strip() : null;
- }
- yield db.update_async<Stack>((!)stack);
- yield pulse.bump(0, "stack saved");
- return stack;
- }
- /** Deletes the stack and its members. */
- public async void delete_stack(int64 id) throws Error {
- var stack = yield get_stack(id);
- if (stack == null) {
- return;
- }
- foreach (var member in yield list_members(id)) {
- yield db.delete_async<StackProject>(member);
- }
- yield db.delete_async<Stack>((!)stack);
- yield pulse.bump(0, "stack deleted");
- }
- /** The stack's member rows, oldest first. */
- public async ImmutableLot<StackProject> list_members(int64 stack_id) throws Error {
- return yield db.query<StackProject>()
- .where(expr("stack_id == $0", elem<int64?>(stack_id)))
- .order_by(expr("id"))
- .materialise_async();
- }
- /**
- * Adds a project to the stack with `relationship`;
- * duplicates (same stack, same project) are rejected.
- */
- public async StackProject add_member(int64 stack_id, int64 project_id,
- string relationship) throws Error {
- require_valid_relationship(relationship);
- var stack = yield get_stack(stack_id);
- if (stack == null) {
- throw new Error(Quark.from_string("clanker-saddle"), 94,
- @"No stack exists with id $(stack_id)");
- }
- var project = yield projects.get_project(project_id);
- if (project == null) {
- throw new Error(Quark.from_string("clanker-saddle"), 95,
- @"No project exists with id $(project_id)");
- }
- var existing = yield db.query<StackProject>()
- .where(expr("stack_id == $0 && project_id == $1",
- elem<int64?>(stack_id), elem<int64?>(project_id)))
- .materialise_async();
- if (existing.length > 0) {
- throw new Error(Quark.from_string("clanker-saddle"), 96,
- "That project is already in the stack");
- }
- var member = new StackProject() {
- stack_id = stack_id,
- project_id = project_id,
- relationship = relationship,
- created_at = new DateTime.now_local()
- };
- yield db.insert_async<StackProject>(member);
- yield pulse.bump(0, "stack member added");
- return member;
- }
- /** Removes the stack member identified by `id`. */
- public async void remove_member(int64 member_id) throws Error {
- var member = yield db.query<StackProject>()
- .where(expr("id == $0", elem<int64?>(member_id)))
- .first_async();
- if (member != null) {
- yield db.delete_async<StackProject>((!)member);
- yield pulse.bump(0, "stack member removed");
- }
- }
- /**
- * Applies the stack to a project: every member becomes a
- * `project_links` row on `parent_id`, same relationship, as
- * long as the parent does not already carry that exact pair
- * (same target AND same relationship — a different
- * relationship on the same target stays). Returns how many
- * links landed. Never throws on duplicates: re-applying is
- * idempotent by design.
- */
- public async int64 apply_to_project(int64 stack_id, int64 parent_id) throws Error {
- var stack = yield require_stack(stack_id);
- var parent = yield projects.get_project(parent_id);
- if (parent == null) {
- throw new Error(Quark.from_string("clanker-saddle"), 97,
- @"No project exists with id $(parent_id)");
- }
- int64 added = 0;
- foreach (var member in yield list_members(stack_id)) {
- if (member.project_id == parent_id) {
- continue; // a project never links to itself
- }
- bool duplicate = false;
- foreach (var link in yield projects.list_links(parent_id)) {
- if (link.linked_project_id == member.project_id
- && link.relationship == member.relationship) {
- duplicate = true;
- break;
- }
- }
- if (duplicate) {
- continue;
- }
- yield db.insert_async<ProjectLink>(new ProjectLink() {
- parent_project_id = parent_id,
- linked_project_id = member.project_id,
- relationship = member.relationship
- });
- added++;
- }
- yield pulse.bump(0, @"stack $(((!)stack).name) applied to project");
- return added;
- }
- /**
- * Applies the stack to a goal: every member becomes a
- * `goal_links` row on `goal_id`, same relationship, skipping
- * exact duplicates. The primary project is implicitly linked
- * and never duplicated. Returns how many links landed.
- */
- public async int64 apply_to_goal(int64 stack_id, int64 goal_id) throws Error {
- var stack = yield require_stack(stack_id);
- var goal = yield goals.get_goal(goal_id);
- if (goal == null) {
- throw new Error(Quark.from_string("clanker-saddle"), 98,
- @"No goal exists with id $(goal_id)");
- }
- int64 added = 0;
- foreach (var member in yield list_members(stack_id)) {
- if (member.project_id == ((!)goal).project_id) {
- continue; // the primary project is implicitly linked
- }
- bool duplicate = false;
- foreach (var link in yield goals.list_links(goal_id)) {
- if (link.project_id == member.project_id
- && link.relationship == member.relationship) {
- duplicate = true;
- break;
- }
- }
- if (duplicate) {
- continue;
- }
- yield db.insert_async<GoalLink>(new GoalLink() {
- goal_id = goal_id,
- project_id = member.project_id,
- relationship = member.relationship
- });
- added++;
- }
- yield pulse.bump(0, @"stack $(((!)stack).name) applied to goal");
- return added;
- }
- /** Loads the stack or throws (the apply paths' guard). */
- private async Stack require_stack(int64 id) throws Error {
- var stack = yield get_stack(id);
- if (stack == null) {
- throw new Error(Quark.from_string("clanker-saddle"), 94,
- @"No stack exists with id $(id)");
- }
- return (!)stack;
- }
- }
- }
|