|
|
@@ -0,0 +1,252 @@
|
|
|
+using Invercargill;
|
|
|
+using Invercargill.DataStructures;
|
|
|
+using Invercargill.Expressions;
|
|
|
+using InvercargillSql.Orm;
|
|
|
+using Inversion;
|
|
|
+using ClankerSaddle.Data;
|
|
|
+
|
|
|
+namespace ClankerSaddle.Services {
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Rule storage: CRUD over `rules` plus their project and task
|
|
|
+ * attachments, and the effective-rule computation the dispatcher
|
|
|
+ * ships in the `/start` payload.
|
|
|
+ */
|
|
|
+ public class RuleService : Object {
|
|
|
+
|
|
|
+ private OrmSession db = inject<OrmSession>();
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Inserts a rule; names are unique. Returns the stored row (with
|
|
|
+ * its id).
|
|
|
+ */
|
|
|
+ public async Rule create_rule(string name, string body) throws Error {
|
|
|
+ if (name.strip().length == 0) {
|
|
|
+ throw new Error(Quark.from_string("clanker-saddle"), 30,
|
|
|
+ "A rule name is required");
|
|
|
+ }
|
|
|
+ if (body.strip().length == 0) {
|
|
|
+ throw new Error(Quark.from_string("clanker-saddle"), 31,
|
|
|
+ "A rule body is required");
|
|
|
+ }
|
|
|
+ var existing = yield db.query<Rule>()
|
|
|
+ .where(expr("name == $0", elem(name.strip())))
|
|
|
+ .first_async();
|
|
|
+ if (existing != null) {
|
|
|
+ throw new Error(Quark.from_string("clanker-saddle"), 32,
|
|
|
+ @"A rule named '$(name)' already exists");
|
|
|
+ }
|
|
|
+ var rule = new Rule() {
|
|
|
+ name = name.strip(),
|
|
|
+ body = body,
|
|
|
+ created_at = new DateTime.now_local()
|
|
|
+ };
|
|
|
+ yield db.insert_async<Rule>(rule);
|
|
|
+ return rule;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** Loads the rule identified by `id`, or null. */
|
|
|
+ public async Rule? get_rule(int64 id) throws Error {
|
|
|
+ return yield db.query<Rule>()
|
|
|
+ .where(expr("id == $0", elem<int64?>(id)))
|
|
|
+ .first_async();
|
|
|
+ }
|
|
|
+
|
|
|
+ /** Every rule ordered by name. */
|
|
|
+ public async ImmutableLot<Rule> list_rules() throws Error {
|
|
|
+ return yield db.query<Rule>()
|
|
|
+ .order_by(expr("name"))
|
|
|
+ .materialise_async();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Saves the editable fields of the rule identified by `id`; null
|
|
|
+ * or empty arguments keep their current values. Returns null when
|
|
|
+ * the rule does not exist.
|
|
|
+ */
|
|
|
+ public async Rule? update_rule(int64 id, string? name, string? body) throws Error {
|
|
|
+ var rule = yield get_rule(id);
|
|
|
+ if (rule == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (name != null && name.strip().length > 0) {
|
|
|
+ var clash = yield db.query<Rule>()
|
|
|
+ .where(expr("name == $0 && id != $1", elem(name.strip()), elem<int64?>(id)))
|
|
|
+ .first_async();
|
|
|
+ if (clash != null) {
|
|
|
+ throw new Error(Quark.from_string("clanker-saddle"), 32,
|
|
|
+ @"A rule named '$(name)' already exists");
|
|
|
+ }
|
|
|
+ ((!)rule).name = name.strip();
|
|
|
+ }
|
|
|
+ if (body != null && body.length > 0) {
|
|
|
+ ((!)rule).body = body;
|
|
|
+ }
|
|
|
+ yield db.update_async<Rule>((!)rule);
|
|
|
+ return rule;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Deletes a rule and every attachment referencing it (task and
|
|
|
+ * project links go with it).
|
|
|
+ */
|
|
|
+ public async void delete_rule(int64 id) throws Error {
|
|
|
+ foreach (var link in yield db.query<TaskRule>()
|
|
|
+ .where(expr("rule_id == $0", elem<int64?>(id)))
|
|
|
+ .materialise_async()) {
|
|
|
+ yield db.delete_async<TaskRule>(link);
|
|
|
+ }
|
|
|
+ foreach (var link in yield db.query<RuleProject>()
|
|
|
+ .where(expr("rule_id == $0", elem<int64?>(id)))
|
|
|
+ .materialise_async()) {
|
|
|
+ yield db.delete_async<RuleProject>(link);
|
|
|
+ }
|
|
|
+ var rule = yield get_rule(id);
|
|
|
+ if (rule != null) {
|
|
|
+ yield db.delete_async<Rule>((!)rule);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Attaches `rule_id` to `project_id`; duplicates are rejected
|
|
|
+ * (the pair is unique).
|
|
|
+ */
|
|
|
+ public async void attach_rule_to_project(int64 rule_id, int64 project_id) throws Error {
|
|
|
+ var rule = yield get_rule(rule_id);
|
|
|
+ if (rule == null) {
|
|
|
+ throw new Error(Quark.from_string("clanker-saddle"), 33,
|
|
|
+ @"No rule exists with id $(rule_id)");
|
|
|
+ }
|
|
|
+ var existing = yield db.query<RuleProject>()
|
|
|
+ .where(expr("rule_id == $0 && project_id == $1",
|
|
|
+ elem<int64?>(rule_id), elem<int64?>(project_id)))
|
|
|
+ .materialise_async();
|
|
|
+ if (existing.length > 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ yield db.insert_async<RuleProject>(new RuleProject() {
|
|
|
+ rule_id = rule_id,
|
|
|
+ project_id = project_id
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /** Detaches the `rule_projects` row identified by `id`. */
|
|
|
+ public async void detach_rule_from_project(int64 link_id) throws Error {
|
|
|
+ var link = yield db.query<RuleProject>()
|
|
|
+ .where(expr("id == $0", elem<int64?>(link_id)))
|
|
|
+ .first_async();
|
|
|
+ if (link != null) {
|
|
|
+ yield db.delete_async<RuleProject>((!)link);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** The `rule_projects` attachments of `project_id`, oldest first. */
|
|
|
+ public async ImmutableLot<RuleProject> list_project_attachments(int64 project_id) throws Error {
|
|
|
+ return yield db.query<RuleProject>()
|
|
|
+ .where(expr("project_id == $0", elem<int64?>(project_id)))
|
|
|
+ .order_by(expr("id"))
|
|
|
+ .materialise_async();
|
|
|
+ }
|
|
|
+
|
|
|
+ /** The rules attached to `project_id`, attachment order. */
|
|
|
+ public async ImmutableLot<Rule> list_rules_for_project(int64 project_id) throws Error {
|
|
|
+ var rows = new Vector<Rule>();
|
|
|
+ foreach (var link in yield list_project_attachments(project_id)) {
|
|
|
+ var rule = yield get_rule(link.rule_id);
|
|
|
+ if (rule != null) {
|
|
|
+ rows.add((!)rule);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return rows.to_immutable_buffer();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Attaches `rule_id` to `task_id`; duplicates are rejected (the
|
|
|
+ * pair is unique).
|
|
|
+ */
|
|
|
+ public async void attach_rule_to_task(int64 rule_id, int64 task_id) throws Error {
|
|
|
+ var rule = yield get_rule(rule_id);
|
|
|
+ if (rule == null) {
|
|
|
+ throw new Error(Quark.from_string("clanker-saddle"), 33,
|
|
|
+ @"No rule exists with id $(rule_id)");
|
|
|
+ }
|
|
|
+ var existing = yield db.query<TaskRule>()
|
|
|
+ .where(expr("rule_id == $0 && task_id == $1",
|
|
|
+ elem<int64?>(rule_id), elem<int64?>(task_id)))
|
|
|
+ .materialise_async();
|
|
|
+ if (existing.length > 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ yield db.insert_async<TaskRule>(new TaskRule() {
|
|
|
+ rule_id = rule_id,
|
|
|
+ task_id = task_id
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /** Detaches the `task_rules` row identified by `id`. */
|
|
|
+ public async void detach_rule_from_task(int64 link_id) throws Error {
|
|
|
+ var link = yield db.query<TaskRule>()
|
|
|
+ .where(expr("id == $0", elem<int64?>(link_id)))
|
|
|
+ .first_async();
|
|
|
+ if (link != null) {
|
|
|
+ yield db.delete_async<TaskRule>((!)link);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** The `task_rules` attachments of `task_id`, oldest first. */
|
|
|
+ public async ImmutableLot<TaskRule> list_task_attachments(int64 task_id) throws Error {
|
|
|
+ return yield db.query<TaskRule>()
|
|
|
+ .where(expr("task_id == $0", elem<int64?>(task_id)))
|
|
|
+ .order_by(expr("id"))
|
|
|
+ .materialise_async();
|
|
|
+ }
|
|
|
+
|
|
|
+ /** The rules attached to `task_id`, attachment order. */
|
|
|
+ public async ImmutableLot<Rule> list_rules_for_task(int64 task_id) throws Error {
|
|
|
+ var rows = new Vector<Rule>();
|
|
|
+ foreach (var link in yield list_task_attachments(task_id)) {
|
|
|
+ var rule = yield get_rule(link.rule_id);
|
|
|
+ if (rule != null) {
|
|
|
+ rows.add((!)rule);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return rows.to_immutable_buffer();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The effective rule set for a dispatched task, in the wire
|
|
|
+ * contract's stable order: the task's own rules, then the primary
|
|
|
+ * project's, then the co-developed projects' in link order —
|
|
|
+ * deduped by id (reference projects contribute nothing).
|
|
|
+ */
|
|
|
+ public async ImmutableLot<Rule> effective_rules(TaskEntity task,
|
|
|
+ ProjectService projects) throws Error {
|
|
|
+ var ordered = new Vector<Rule>();
|
|
|
+ // Ids are stringified: the Invercargill collections cannot
|
|
|
+ // key (or hash) on value types.
|
|
|
+ var seen = new HashSet<string>();
|
|
|
+
|
|
|
+ foreach (var rule in yield list_rules_for_task(task.id)) {
|
|
|
+ if (seen.add(rule.id.to_string())) {
|
|
|
+ ordered.add(rule);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ foreach (var rule in yield list_rules_for_project(task.project_id)) {
|
|
|
+ if (seen.add(rule.id.to_string())) {
|
|
|
+ ordered.add(rule);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ foreach (var link in yield projects.list_links(task.project_id)) {
|
|
|
+ if (link.relationship != LINK_CO_DEVELOPED) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ foreach (var rule in yield list_rules_for_project(link.linked_project_id)) {
|
|
|
+ if (seen.add(rule.id.to_string())) {
|
|
|
+ ordered.add(rule);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return ordered.to_immutable_buffer();
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|