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

Configure Feed

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

at 557ff54b2b435e5f1e789c6a8a4e1bebf2d7deb6 191 lines 8.2 kB view raw
1import _ from 'lodash'; 2import { type Opaque, type ReadonlyDeep } from 'type-fest'; 3 4import { outcomeToNullableBool } from '../condition_evaluator/condition.js'; 5import { getConditionSetResults } from '../condition_evaluator/conditionSet.js'; 6import makeGetDerivedFieldValueWithCache from '../condition_evaluator/getDerivedFieldValue.js'; 7import { type Dependencies } from '../iocContainer/index.js'; 8import { inject } from '../iocContainer/utils.js'; 9import { type ConditionSetWithResult } from '../models/rules/RuleModel.js'; 10import { 11 type DerivedFieldSpec, 12 type DerivedFieldValue, 13} from '../services/derivedFieldsService/index.js'; 14import { type ItemSubmission } from '../services/itemProcessingService/index.js'; 15import { 16 type ConditionSet, 17 type ItemType, 18} from '../services/moderationConfigService/index.js'; 19import { type TransientRunSignalWithCache } from '../services/orgAwareSignalExecutionService/index.js'; 20import { type SignalId } from '../services/signalsService/index.js'; 21import type { 22 ActionExecutionSourceType, 23 RuleExecutionSourceType, 24} from '../services/analyticsLoggers/index.js'; 25import { instantiateOpaqueType } from '../utils/typescript-types.js'; 26 27// A rule can be run on either a full submission, or on just the identifier of 28// an item (namely, in the case of user rules). 29export type RuleInput = 30 // Policies only present in reporting + routing rules; 31 // refers to the policy of the report. 32 | (ItemSubmission & { 33 policyIds?: string[]; 34 sourceType?: RuleExecutionSourceType | ActionExecutionSourceType; 35 }) 36 | ReadonlyDeep<{ itemId: string; itemType: Pick<ItemType, 'id' | 'kind' | 'name'> }>; 37 38export function isFullSubmission(input: RuleInput): input is ItemSubmission { 39 return 'data' in input && 'submissionId' in input && Boolean(input.data); 40} 41 42export function getUserFromRuleInput(it: RuleInput) { 43 return isFullSubmission(it) 44 ? it.creator 45 : it.itemType.kind === 'USER' 46 ? { id: it.itemId, typeId: it.itemType.id } 47 : undefined; 48} 49 50type RuleEvaluationContextImpl = Readonly<{ 51 org: { id: string /* TODO: might add api keys or api key ids here too */ }; 52 input: RuleInput; 53 runSignal: TransientRunSignalWithCache; 54 getSignalCost: (id: SignalId) => Promise<number>; 55 getDerivedFieldValue: (spec: DerivedFieldSpec) => Promise<DerivedFieldValue>; 56}>; 57 58export type RuleEvaluationContext = Opaque< 59 RuleEvaluationContextImpl, 60 RuleEvaluationContextImpl 61>; 62 63export type RuleExecutionResult = { 64 passed: boolean; 65 conditionResults: ConditionSetWithResult; 66}; 67 68/** 69 * This is the main Rule Engine class. It's responsible for running 70 * all of the user's Rules on a single piece of content sent to 71 * our API. 72 */ 73class RuleEvaluator { 74 constructor( 75 private readonly makeRunSignal: Dependencies['TransientRunSignalWithCacheFactory'], 76 private readonly signalsService: Dependencies['SignalsService'], 77 private readonly tracer: Dependencies['Tracer'], 78 ) {} 79 80 /** 81 * Evaluating a rule's conditionSet requires some arguments, like the content 82 * against which you want to evaluate the conditions (see RuleInput). 83 * 84 * However, in addition to these data arguments, running a rule also requires 85 * various "capabilities", e.g., the ability to evaluate a signal for some 86 * value (as required by one of the rule's conditions) and the ability to 87 * compute derived values from the content. 88 * 89 * For these "capabilities", we may want to retain some state between from one 90 * rule evaluation to the next, for the purpose of caching. E.g., if multiple 91 * conditions (in the same rule, or across rules) run the same signal against 92 * the same input, we'd like to be able to cache and reuse that result. 93 * Similarly, if multiple conditions reference the same derived field, we'd 94 * like to only have to compute its value once. 95 * 96 * The scope of this cache -- e.g., is it distributed, in a way all api 97 * servers can access? if not, and it just lives in one server's memory, does 98 * it persist across requests to that server (and expire on a TTL), or does it 99 * get created at the start of some request/unit of work and discarded at the 100 * end? -- is likely to change over time, as we evolve the system. 101 * 102 * For now, though, we have an explicit object that holds this cached state, 103 * and which is expected to be created at the start of some unit of work (like 104 * a rule set execution) and then discarded shortly thereafter. That object is 105 * a "rule execution context", and it's what's returned here. Because, again, 106 * this concept may change over time, I'm treating it as a detail of the 107 * RuleEngine, by having the RuleEngine be the only code (in the method below) 108 * that can create the context (using an opaque type), and the only code that 109 * will consume it. 110 * 111 * As implied by this method's arguments, a context cannot be reused across 112 * different RuleInputs/different item submissions, in part to help enforce 113 * that these objects be short-lived, but also because the RuleInput data 114 * contributes to the cache keys used within the evaluation context (e.g., for 115 * `getDerivedFieldValue`). 116 */ 117 makeRuleExecutionContext(args: { 118 orgId: string; 119 input: RuleInput; 120 }): RuleEvaluationContext { 121 const { orgId, input } = args; 122 const runSignal = this.makeRunSignal(); 123 const getDerivedFieldValue = makeGetDerivedFieldValueWithCache( 124 runSignal, 125 orgId, 126 ); 127 128 return instantiateOpaqueType<RuleEvaluationContext>({ 129 org: { id: orgId }, 130 input, 131 runSignal, 132 getSignalCost: async (signalId: SignalId) => 133 this.signalsService 134 .getSignalOrThrow({ orgId, signalId }) 135 .then((s) => s.getCost()) 136 .catch(() => Infinity), 137 // If the item data is missing, as it will be in user rule 138 // RuleEvaluationContexts, then we can't extract a derived field value 139 // (at least for now, since we don't have a notion of derived fields 140 // from from item ids), so we return undefined. 141 getDerivedFieldValue: isFullSubmission(input) 142 ? async (spec: DerivedFieldSpec) => getDerivedFieldValue(input, spec) 143 : async (_spec: DerivedFieldSpec) => undefined, 144 }); 145 } 146 147 /** 148 * This function runs a piece of content through a single Rule and returns the 149 * results. 150 * 151 * Note that this function _does not_ perform any of the side effects that 152 * usually attend a rule execution, including (even) logging that execution to 153 * the data warehouse. For that reason, this function is private, and should only be 154 * called as an implementation detail of the RuleEngine. 155 * 156 * @param ruleConditions - The conditions that logically define the rule 157 * (i.e., determine whether the it passes, independent of metadata about it 158 * like its name etc). We don't need full Rule instance from our db, which 159 * is a fact we might leverage later (e.g., if we wanna cache the results of 160 * this function, we'd do it by ruleConditions, rather than rule id + version, 161 * as the conditions might not change between versions). 162 * @param context - the context needed to run the rule, including, most 163 * notably, the user-generated content to run the rule against and/or a user 164 * id that can be selected as an input to (future) user-scoring signals. 165 * @return Whether the content passed the rule's conditions, and details 166 * about which subconditions did/didn't match. 167 */ 168 public async runRule( 169 ruleConditions: ReadonlyDeep<ConditionSet>, 170 context: RuleEvaluationContext, 171 ): Promise<RuleExecutionResult> { 172 const results = await getConditionSetResults( 173 ruleConditions, 174 context, 175 this.tracer, 176 ); 177 178 return { 179 // If the rule outcome was null (e.g., if some critical condition errored), 180 // coalesce to false to treat the rule as though it didn't pass 181 passed: outcomeToNullableBool(results.result.outcome) ?? false, 182 conditionResults: results, 183 }; 184 } 185} 186 187export default inject( 188 ['TransientRunSignalWithCacheFactory', 'SignalsService', 'Tracer'], 189 RuleEvaluator, 190); 191export { type RuleEvaluator };