Mirror of https://github.com/roostorg/coop github.com/roostorg/coop
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 557ff54b2b435e5f1e789c6a8a4e1bebf2d7deb6 202 lines 6.8 kB view raw
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}