Something went wrong. Try again.
Mirrored from GitHub github.com/roostorg/coop
Something went wrong. Try again.
7.2 kB · 212 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213import _ from 'lodash';import { type ReadonlyDeep } from 'type-fest';
import { type Dependencies } from '../../iocContainer/index.js';import { RuleEnvironment } from '../../rule_engine/RuleEngine.js';import { type RuleEvaluationContext } from '../../rule_engine/RuleEvaluator.js';import { equalLengthZip } from '../../utils/fp-helpers.js';import { logErrorJson } from '../../utils/logging.js';import { safePick } from '../../utils/misc.js';import { type ItemSubmission } from '../itemProcessingService/makeItemSubmission.js';import { type Action } from '../moderationConfigService/index.js';import type ReportingRules from './ReportingRules.js';import { ReportingRuleStatus, type ReportingRule } from './ReportingRules.js';import { type ReportingRuleExecutionCorrelationId } from './reportingService.js';
const { uniqBy } = _;
export default class ReportingRuleEngine { constructor( private readonly ruleEvaluator: Dependencies['RuleEvaluator'], private readonly reportingRuleExecutionLogger: Dependencies['ReportingRuleExecutionLogger'], private readonly actionPublisher: Dependencies['ActionPublisher'], private readonly getActionsByIdEventuallyConsistent: Dependencies['getActionsByIdEventuallyConsistent'], private readonly getPoliciesByIdEventuallyConsistent: Dependencies['getPoliciesByIdEventuallyConsistent'], private readonly tracer: Dependencies['Tracer'], private readonly reportingRules: ReportingRules, ) {}
/** * Runs the rules that are "enabled" ({@see ItemType.getEnabledRules}) for * this item type, against the given itemSubmission. * * @param itemSubmission * @param executionsCorrelationId - An id that should be associated with all * rule executions generated as part of running these rules, for correlating * the execution with the event in the system that caused it. * {@see getCorrelationId} */ async runEnabledRules( itemSubmission: ItemSubmission, executionsCorrelationId: ReportingRuleExecutionCorrelationId, ) { const enabledRules = await this.reportingRules.getReportingRules({ orgId: itemSubmission.itemType.orgId, });
const liveRules = enabledRules.filter( (it) => it.status === ReportingRuleStatus.LIVE, ); const backgroundRules = enabledRules.filter( (it) => it.status === ReportingRuleStatus.BACKGROUND, );
const evaluationContext = this.ruleEvaluator.makeRuleExecutionContext({ orgId: itemSubmission.itemType.orgId, input: itemSubmission, });
await Promise.all([ this.runRuleSet( liveRules, evaluationContext, RuleEnvironment.LIVE, executionsCorrelationId, ), this.runRuleSet( backgroundRules, evaluationContext, RuleEnvironment.BACKGROUND, executionsCorrelationId, ), ]); }
async runRuleSet( rules: ReadonlyDeep<ReportingRule[]>, evaluationContext: RuleEvaluationContext, environment: RuleEnvironment, executionsCorrelationId: ReportingRuleExecutionCorrelationId, ) { if (!rules.length) { return; }
const shouldRunActions = environment === 'LIVE';
const ruleResults = await Promise.all( rules.map(async (it) => this.ruleEvaluator.runRule(it.conditionSet, evaluationContext), ), );
const rulesToResults = new Map(equalLengthZip(rules, ruleResults));
const passingRules = [...rulesToResults.entries()] .filter(([_rule, result]) => result.passed) .map(([rule, _result]) => rule);
const passingRuleActions = await Promise.all( passingRules.map(async (it) => this.getActionsByIdEventuallyConsistent({ ids: it.actionIds, orgId: it.orgId, }), ), ); const passingRulePolicies = await Promise.all( passingRules.map(async (it) => this.getPoliciesByIdEventuallyConsistent({ ids: it.actionIds, orgId: it.orgId, }), ), ); const passingRulesToPolicies = new Map( equalLengthZip( passingRules.map((it) => it.id), passingRulePolicies, ), ); const passingRulesToActions = new Map( equalLengthZip(passingRules, passingRuleActions), );
// NB: while we only run _deduped_ actions, we record the actions and // update the rule action run counts as though no deduping took place, // since, logically, each rule triggered the action. const { org, input: ruleInput } = evaluationContext;
// Catch at construction; awaited below via Promise.all. const logRuleExecutionsPromise = this.reportingRuleExecutionLogger .logReportingRuleExecutions( [...rulesToResults.entries()].map(([rule, result]) => ({ orgId: org.id, reportingRule: { id: rule.id, name: rule.name, version: rule.version, environment, }, ruleInput: ruleInput as ItemSubmission, result: result.conditionResults, correlationId: executionsCorrelationId, passed: result.passed, policyNames: passingRulesToPolicies.get(rule.id)?.map((policy) => policy.name) ?? [], policyIds: rule.policyIds, })), ) .catch((err) => { this.tracer.logActiveSpanFailedIfAny(err); // eslint-disable-next-line no-restricted-syntax logErrorJson({ message: `logReportingRuleExecutions failed orgId=${org.id} environment=${environment} correlationId=${executionsCorrelationId}`, error: err, }); });
if (!shouldRunActions) { await logRuleExecutionsPromise; return { rulesToResults, actions: [] }; }
const dedupedActions = uniqBy( [...passingRulesToActions.values()].flat(), (it) => it.id, ) as Action[];
// Publish all (deduped) actions + update the rule action counts if appropriate. const publishActionsPromise = this.actionPublisher .publishActions( dedupedActions.map((action) => { return { action, ruleEnvironment: environment, matchingRules: [...passingRulesToActions.entries()].flatMap( ([rule, actions]) => actions.includes(action) ? [ { ...safePick(rule, ['id', 'name']), version: rule.version, policies: passingRulesToPolicies.get(rule.id) ?? [], tags: [], }, ] : [], ), policies: _.uniqBy( [...passingRulesToActions.keys()].flatMap( (rule) => passingRulesToPolicies.get(rule.id) ?? [], ), 'id', ), }; }), { orgId: org.id, targetItem: ruleInput, correlationId: executionsCorrelationId, }, ) .catch((e) => { this.tracer.logActiveSpanFailedIfAny(e); throw e; });
await Promise.all([publishActionsPromise, logRuleExecutionsPromise]); return; }}