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 250 lines 8.9 kB view raw
1/** 2 * @fileoverview This defines a few separate "services" that can be injected 3 * into the RuleEngine and other services in the POST /content hot path (like 4 * the signal execution service) to enable those services to make the queries 5 * they need to run a rule. Having these queries defined separately + injected 6 * into the consumers gives us a cleaner place to add optimizations to the query 7 * logic (i.e., run the queries against replicas, add caching, etc) and makes 8 * the consumers much more unit testable. 9 */ 10import { Op } from 'sequelize'; 11 12import { inject } from '../iocContainer/index.js'; 13import { type LocationArea } from '../models/types/locationArea.js'; 14import { type Action } from '../services/moderationConfigService/index.js'; 15import { cached } from '../utils/caching.js'; 16import { jsonParse, jsonStringify } from '../utils/encoding.js'; 17import { getUtcDateOnlyString } from '../utils/time.js'; 18 19export const makeGetEnabledRulesForItemTypeEventuallyConsistent = inject( 20 ['getSequelizeItemTypeEventuallyConsistent'], 21 function (getSequelizeItemTypeEventuallyConsistent) { 22 return cached({ 23 async producer(itemTypeId: string) { 24 // Getting the enabledRules is currently coupled to sequelize, so, 25 // annoyingly, we first have to convert the contentTypeId into a full 26 // contentType model object. However, we don't want to incur too much 27 // overhead for that, so we use a cached lookup. (Note: we can't just 28 // take a model object as the argument because our caching library 29 // requires the cache key to be a string; further, even if we could just 30 // accept the model object, we wouldn't want to, because we want to move 31 // away from this coupling to sequelize.) 32 const itemType = await getSequelizeItemTypeEventuallyConsistent({ 33 id: itemTypeId, 34 }); 35 36 return itemType ? itemType.getEnabledRules() : null; 37 }, 38 directives: { freshUntilAge: 20 }, 39 }); 40 }, 41); 42 43export type GetEnabledRulesForItemTypeEventuallyConsistent = ReturnType< 44 typeof makeGetEnabledRulesForItemTypeEventuallyConsistent 45>; 46 47export const makeGetSequelizeItemTypeEventuallyConsistent = inject( 48 ['ItemTypeModel'], 49 (ItemType) => { 50 return cached({ 51 async producer(key: { id: string } | { name: string; orgId: string }) { 52 return 'id' in key 53 ? ItemType.findByPk(key.id) 54 : ItemType.findOne({ where: { name: key.name, orgId: key.orgId } }); 55 }, 56 directives: { freshUntilAge: 10, maxStale: [0, 2, 2] }, 57 }); 58 }, 59); 60 61export type GetSequelizeItemTypeEventuallyConsistent = ReturnType< 62 typeof makeGetSequelizeItemTypeEventuallyConsistent 63>; 64 65export const makeGetItemTypesForOrgEventuallyConsistent = inject( 66 ['ModerationConfigService'], 67 (moderationConfigService) => async (orgId: string) => 68 moderationConfigService.getItemTypes({ 69 orgId, 70 }), 71); 72 73export type GetItemTypesForOrgEventuallyConsistent = ReturnType< 74 typeof makeGetItemTypesForOrgEventuallyConsistent 75>; 76 77// TODO: this could probably be improved to increase cache hit rates, since 78// rn the cache will only be used if all the ids have previously been fetched. 79export const makeGetPoliciesForRulesEventuallyConsistent = inject( 80 ['PolicyModel'], 81 function (Policy) { 82 return cached({ 83 keyGeneration: { 84 toString: (ids: readonly string[]) => jsonStringify([...ids].sort()), 85 fromString: (it) => jsonParse(it), 86 }, 87 async producer(key) { 88 return Policy.getPoliciesForRuleIds(key); 89 }, 90 directives: { freshUntilAge: 120 }, 91 }); 92 }, 93); 94 95export type GetPoliciesForRulesEventuallyConsistent = ReturnType< 96 typeof makeGetPoliciesForRulesEventuallyConsistent 97>; 98 99export const makeGetActionsForRuleEventuallyConsistent = inject( 100 ['ActionModel'], 101 (Action) => { 102 return cached({ 103 async producer(ruleId: string) { 104 // This generates a pretty slow/overly-complex query, but I think it's 105 // the best we can do with Sequelize. Eventually, we want to move off of 106 // Sequelize, so we don't fetch full model instances + we cast the 107 // result to be a plain data object, so that the rest of the code can't 108 // depend on getting an Action model instance back, as that won't always 109 // be the case. 110 return Action.findAll({ 111 where: { '$rules.id$': ruleId }, 112 include: [{ association: 'rules', attributes: ['id'] }], 113 raw: true, 114 }) as Promise<Action[]>; 115 }, 116 directives: { freshUntilAge: 30 }, 117 }); 118 }, 119); 120 121export type GetActionsForRuleEventuallyConsistent = ReturnType< 122 typeof makeGetActionsForRuleEventuallyConsistent 123>; 124 125export const makeGetLocationBankLocationsEventuallyConsistent = inject( 126 ['LocationBankLocationModel'], 127 (LocationBankLocation) => { 128 return cached({ 129 async producer(bankId: string) { 130 // NB: we use `raw: true` to get back plain JS objects, rather than 131 // sequelize model instances. We do that because, with model instances, 132 // every proprety access runs some extra getter code; see 133 // https://github.com/sequelize/sequelize/blob/e77dcf78b341b62c97dbb29f16ce7a23f46ddc53/src/model.js#L42 134 // This ends up killing the performance of our hot-path code that checks 135 // whether a location is in each of these location areas. 136 // 137 // Meanwhile, we have to cast to LocationArea[] because findAll is still 138 // typed (incorrectly) to return the model instance, even when `raw: 139 // true` is provided. 140 return LocationBankLocation.findAll({ 141 where: { bankId }, 142 raw: true, 143 }) as Promise<LocationArea[]>; 144 }, 145 directives(locations) { 146 const numLocations = locations.length; 147 const cacheTime = 15 + numLocations ** (1 / 3); 148 const swrTime = numLocations ** (2 / 3); 149 return { freshUntilAge: cacheTime, maxStale: [0, swrTime, swrTime] }; 150 }, 151 collapseOverlappingRequestsTime: 60, 152 }); 153 }, 154); 155 156export type GetLocationBankLocationsBankEventuallyConsistent = ReturnType< 157 typeof makeGetLocationBankLocationsEventuallyConsistent 158>; 159 160export const makeGetTextBankStringsEventuallyConsistent = inject( 161 ['ModerationConfigService'], 162 (moderationConfigService) => { 163 return cached({ 164 async producer(input: { orgId: string; bankId: string }) { 165 const { orgId, bankId } = input; 166 const bank = await moderationConfigService.getTextBank({ 167 id: bankId, 168 orgId, 169 }); 170 171 return bank.strings; 172 }, 173 directives: { freshUntilAge: 60, maxStale: [0, 5, 5] }, 174 }); 175 }, 176); 177 178export type GetTextBankStringsEventuallyConsistent = ReturnType< 179 typeof makeGetTextBankStringsEventuallyConsistent 180>; 181 182export const makeGetImageBankEventuallyConsistent = inject( 183 ['HMAHashBankService'], 184 (hmaService) => { 185 return cached({ 186 async producer(input: { orgId: string; bankId: string }) { 187 const { orgId, bankId } = input; 188 return hmaService.getBankById(orgId, parseInt(bankId, 10)); 189 }, 190 directives: { freshUntilAge: 60, maxStale: [0, 5, 5] }, 191 }); 192 }, 193); 194 195export type GetImageBankEventuallyConsistent = ReturnType< 196 typeof makeGetImageBankEventuallyConsistent 197>; 198 199export const makeRecordRuleActionLimitUsage = inject( 200 ['Sequelize', 'Tracer'], 201 (db, tracer) => { 202 /** 203 * Record that each of the rules given by ruleIds has used up one of its 204 * daily action runs, against its maxDailyActions. 205 */ 206 async function recordRuleActionLimitUsage(ruleIds: readonly string[]) { 207 if (ruleIds.length === 0) { 208 return; 209 } 210 211 const today = getUtcDateOnlyString(); 212 await db.transactionWithRetry(async () => { 213 // Using two queries like this isn't as efficient as, e.g., 214 // UPDATE `rules` 215 // SET `daily_actions_run` = 216 // IF(last_action_date != $1, 1, daily_actions_run + 1) 217 // SET `last_action_date` = $1 218 // WHERE `id` IN (...); 219 // But it lets us keep the code in Sequelize, which is probably worth it. 220 await db.Rule.increment( 221 { dailyActionsRun: 1 }, 222 { where: { id: { [Op.in]: ruleIds }, lastActionDate: today } }, 223 ); 224 225 await db.Rule.update( 226 { dailyActionsRun: 1, lastActionDate: today }, 227 { 228 where: { 229 id: { [Op.in]: ruleIds }, 230 lastActionDate: { [Op.ne]: today }, 231 }, 232 }, 233 ); 234 }); 235 } 236 237 return tracer.traced( 238 { 239 resource: 'ruleEngine', 240 operation: 'recordActionLimitUsage', 241 attributesFromArgs: ([ruleIds]) => ({ ruleIds }), 242 }, 243 recordRuleActionLimitUsage, 244 ); 245 }, 246); 247 248export type RecordRuleActionLimitUsage = ( 249 ruleIds: readonly string[], 250) => Promise<void>;