Mirror of https://github.com/roostorg/coop
github.com/roostorg/coop
1import _ from 'lodash';
2import { type ReadonlyDeep } from 'type-fest';
3
4import { type Dependencies } from '../../iocContainer/index.js';
5import { RuleEnvironment } from '../../rule_engine/RuleEngine.js';
6import { type RuleEvaluationContext } from '../../rule_engine/RuleEvaluator.js';
7import { equalLengthZip } from '../../utils/fp-helpers.js';
8import { safePick } from '../../utils/misc.js';
9import { type ItemSubmission } from '../itemProcessingService/makeItemSubmission.js';
10import { type Action } from '../moderationConfigService/index.js';
11import type ReportingRules from './ReportingRules.js';
12import { ReportingRuleStatus, type ReportingRule } from './ReportingRules.js';
13import { type ReportingRuleExecutionCorrelationId } from './reportingService.js';
14
15const { uniqBy } = _;
16
17export default class ReportingRuleEngine {
18 constructor(
19 private readonly ruleEvaluator: Dependencies['RuleEvaluator'],
20 private readonly reportingRuleExecutionLogger: Dependencies['ReportingRuleExecutionLogger'],
21 private readonly actionPublisher: Dependencies['ActionPublisher'],
22 private readonly getActionsByIdEventuallyConsistent: Dependencies['getActionsByIdEventuallyConsistent'],
23 private readonly getPoliciesByIdEventuallyConsistent: Dependencies['getPoliciesByIdEventuallyConsistent'],
24 private readonly tracer: Dependencies['Tracer'],
25 private readonly reportingRules: ReportingRules,
26 ) {}
27
28 /**
29 * Runs the rules that are "enabled" ({@see ItemType.getEnabledRules}) for
30 * this item type, against the given itemSubmission.
31 *
32 * @param itemSubmission
33 * @param executionsCorrelationId - An id that should be associated with all
34 * rule executions generated as part of running these rules, for correlating
35 * the execution with the event in the system that caused it.
36 * {@see getCorrelationId}
37 */
38 async runEnabledRules(
39 itemSubmission: ItemSubmission,
40 executionsCorrelationId: ReportingRuleExecutionCorrelationId,
41 ) {
42 const enabledRules = await this.reportingRules.getReportingRules({
43 orgId: itemSubmission.itemType.orgId,
44 });
45
46 const liveRules = enabledRules.filter(
47 (it) => it.status === ReportingRuleStatus.LIVE,
48 );
49 const backgroundRules = enabledRules.filter(
50 (it) => it.status === ReportingRuleStatus.BACKGROUND,
51 );
52
53 const evaluationContext = this.ruleEvaluator.makeRuleExecutionContext({
54 orgId: itemSubmission.itemType.orgId,
55 input: itemSubmission,
56 });
57
58 await Promise.all([
59 this.runRuleSet(
60 liveRules,
61 evaluationContext,
62 RuleEnvironment.LIVE,
63 executionsCorrelationId,
64 ),
65 this.runRuleSet(
66 backgroundRules,
67 evaluationContext,
68 RuleEnvironment.BACKGROUND,
69 executionsCorrelationId,
70 ),
71 ]);
72 }
73
74 async runRuleSet(
75 rules: ReadonlyDeep<ReportingRule[]>,
76 evaluationContext: RuleEvaluationContext,
77 environment: RuleEnvironment,
78 executionsCorrelationId: ReportingRuleExecutionCorrelationId,
79 ) {
80 if (!rules.length) {
81 return;
82 }
83
84 const shouldRunActions = environment === 'LIVE';
85
86 const ruleResults = await Promise.all(
87 rules.map(async (it) =>
88 this.ruleEvaluator.runRule(it.conditionSet, evaluationContext),
89 ),
90 );
91
92 const rulesToResults = new Map(equalLengthZip(rules, ruleResults));
93
94 const passingRules = [...rulesToResults.entries()]
95 .filter(([_rule, result]) => result.passed)
96 .map(([rule, _result]) => rule);
97
98 const passingRuleActions = await Promise.all(
99 passingRules.map(async (it) =>
100 this.getActionsByIdEventuallyConsistent({
101 ids: it.actionIds,
102 orgId: it.orgId,
103 }),
104 ),
105 );
106 const passingRulePolicies = await Promise.all(
107 passingRules.map(async (it) =>
108 this.getPoliciesByIdEventuallyConsistent({
109 ids: it.actionIds,
110 orgId: it.orgId,
111 }),
112 ),
113 );
114 const passingRulesToPolicies = new Map(
115 equalLengthZip(
116 passingRules.map((it) => it.id),
117 passingRulePolicies,
118 ),
119 );
120 const passingRulesToActions = new Map(
121 equalLengthZip(passingRules, passingRuleActions),
122 );
123
124 // NB: while we only run _deduped_ actions, we record the actions and
125 // update the rule action run counts as though no deduping took place,
126 // since, logically, each rule triggered the action.
127 const { org, input: ruleInput } = evaluationContext;
128
129 const logRuleExecutionsPromise =
130 this.reportingRuleExecutionLogger.logReportingRuleExecutions(
131 [...rulesToResults.entries()].map(([rule, result]) => ({
132 orgId: org.id,
133 reportingRule: {
134 id: rule.id,
135 name: rule.name,
136 version: rule.version,
137 environment,
138 },
139 ruleInput: ruleInput as ItemSubmission,
140 result: result.conditionResults,
141 correlationId: executionsCorrelationId,
142 passed: result.passed,
143 policyNames:
144 passingRulesToPolicies.get(rule.id)?.map((policy) => policy.name) ??
145 [],
146 policyIds: rule.policyIds,
147 })),
148 );
149
150 if (!shouldRunActions) {
151 await logRuleExecutionsPromise;
152 return { rulesToResults, actions: [] };
153 }
154
155 const dedupedActions = uniqBy(
156 [...passingRulesToActions.values()].flat(),
157 (it) => it.id,
158 ) as Action[];
159
160 // Publish all (deduped) actions + update the rule action counts if appropriate.
161 const publishActionsPromise = this.actionPublisher
162 .publishActions(
163 dedupedActions.map((action) => {
164 return {
165 action,
166 ruleEnvironment: environment,
167 matchingRules: [...passingRulesToActions.entries()].flatMap(
168 ([rule, actions]) =>
169 actions.includes(action)
170 ? [
171 {
172 ...safePick(rule, ['id', 'name']),
173 version: rule.version,
174 policies: passingRulesToPolicies.get(rule.id) ?? [],
175 tags: [],
176 },
177 ]
178 : [],
179 ),
180 policies: _.uniqBy(
181 [...passingRulesToActions.keys()].flatMap(
182 (rule) => passingRulesToPolicies.get(rule.id) ?? [],
183 ),
184 'id',
185 ),
186 };
187 }),
188 {
189 orgId: org.id,
190 targetItem: ruleInput,
191 correlationId: executionsCorrelationId,
192 },
193 )
194 .catch((e) => {
195 this.tracer.logActiveSpanFailedIfAny(e);
196 throw e;
197 });
198
199 await Promise.all([publishActionsPromise, logRuleExecutionsPromise]);
200 return;
201 }
202}