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 main 195 lines 6.3 kB view raw
1import { type Kysely, sql } from 'kysely'; 2 3import { 4 type RuleAlarmStatus, 5 RuleStatus, 6 RuleType, 7 type ConditionSet, 8} from '../index.js'; 9import { type ModerationConfigServicePg } from '../dbTypes.js'; 10import { getUtcDateOnlyString } from '../../../utils/time.js'; 11import { 12 type PlainRuleWithLatestVersion, 13 computeRuleStatusFromRow, 14} from '../../../models/rules/ruleTypes.js'; 15 16const ruleSelect = [ 17 'r.id', 18 'r.name', 19 'r.description', 20 'r.status_if_unexpired as statusIfUnexpired', 21 'r.tags', 22 'r.max_daily_actions as maxDailyActions', 23 'r.daily_actions_run as dailyActionsRun', 24 'r.last_action_date as lastActionDate', 25 'r.created_at as createdAt', 26 'r.updated_at as updatedAt', 27 'r.org_id as orgId', 28 'r.creator_id as creatorId', 29 'r.expiration_time as expirationTime', 30 'r.condition_set as conditionSet', 31 'r.alarm_status as alarmStatus', 32 'r.alarm_status_set_at as alarmStatusSetAt', 33 'r.rule_type as ruleType', 34 'r.parent_id as parentId', 35 'rlv.version as latestVersionString', 36] as const; 37 38type RuleRow = { 39 id: string; 40 name: string; 41 description: string | null; 42 statusIfUnexpired: Exclude<RuleStatus, typeof RuleStatus.EXPIRED>; 43 tags: string[]; 44 maxDailyActions: number | null; 45 dailyActionsRun: number; 46 lastActionDate: string | null; 47 createdAt: Date; 48 updatedAt: Date; 49 orgId: string; 50 creatorId: string; 51 expirationTime: Date | null; 52 conditionSet: ConditionSet; 53 // Kysely returns the Postgres enum as a plain string; cast in rowToPlainRuleWithLatest. 54 alarmStatus: string; 55 alarmStatusSetAt: Date; 56 ruleType: RuleType; 57 parentId: string | null; 58 latestVersionString: string | null; 59}; 60 61function enabledQuotaWhere(today: string) { 62 return sql<boolean>`(r.max_daily_actions is null or r.last_action_date is distinct from ${today}::date or (r.max_daily_actions is not null and r.daily_actions_run < r.max_daily_actions))`; 63} 64 65function rowToPlainRuleWithLatest(row: RuleRow): PlainRuleWithLatestVersion { 66 const status = computeRuleStatusFromRow(row.expirationTime, row.statusIfUnexpired); 67 const version = row.latestVersionString ?? ''; 68 return { 69 id: row.id, 70 name: row.name, 71 description: row.description, 72 statusIfUnexpired: row.statusIfUnexpired, 73 status, 74 tags: row.tags, 75 maxDailyActions: row.maxDailyActions, 76 dailyActionsRun: row.dailyActionsRun, 77 lastActionDate: row.lastActionDate, 78 createdAt: row.createdAt, 79 updatedAt: row.updatedAt, 80 orgId: row.orgId, 81 creatorId: row.creatorId, 82 expirationTime: row.expirationTime, 83 conditionSet: row.conditionSet, 84 alarmStatus: row.alarmStatus as RuleAlarmStatus, 85 alarmStatusSetAt: row.alarmStatusSetAt, 86 ruleType: row.ruleType, 87 parentId: row.parentId, 88 latestVersion: { ruleId: row.id, version }, 89 }; 90} 91 92export default class RuleReadOperations { 93 constructor( 94 private readonly pgQuery: Kysely<ModerationConfigServicePg>, 95 private readonly pgQueryReplica: Kysely<ModerationConfigServicePg>, 96 ) {} 97 98 async getEnabledRulesForItemType(itemTypeId: string) { 99 const today = String(getUtcDateOnlyString()); 100 const rows = (await this.pgQueryReplica 101 .selectFrom('public.rules as r') 102 .innerJoin('public.rules_and_item_types as rit', 'rit.rule_id', 'r.id') 103 .leftJoin('public.rules_latest_versions as rlv', 'rlv.rule_id', 'r.id') 104 .select(ruleSelect) 105 .where('rit.item_type_id', '=', itemTypeId) 106 .where((eb) => 107 eb.or([ 108 eb('r.expiration_time', 'is', null), 109 eb('r.expiration_time', '>', sql<Date>`now()`), 110 ]), 111 ) 112 .where('r.status_if_unexpired', 'in', [ 113 RuleStatus.LIVE, 114 RuleStatus.BACKGROUND, 115 ]) 116 .where(enabledQuotaWhere(today)) 117 .execute()) as RuleRow[]; 118 119 return rows.map(rowToPlainRuleWithLatest); 120 } 121 122 /** 123 * Loads a single rule (with latest version row) scoped to an org. 124 * Used for GraphQL rule parents and permission checks without Sequelize. 125 * 126 * @param opts.readFromReplica — When false, reads from the primary (e.g. immediately after writes). 127 */ 128 async getRuleByIdAndOrg( 129 ruleId: string, 130 orgId: string, 131 opts?: { readFromReplica?: boolean }, 132 ): Promise<PlainRuleWithLatestVersion | null> { 133 const readFromReplica = opts?.readFromReplica ?? true; 134 const pg = readFromReplica ? this.pgQueryReplica : this.pgQuery; 135 const row = (await pg 136 .selectFrom('public.rules as r') 137 .leftJoin('public.rules_latest_versions as rlv', 'rlv.rule_id', 'r.id') 138 .select(ruleSelect) 139 .where('r.id', '=', ruleId) 140 .where('r.org_id', '=', orgId) 141 .executeTakeFirst()) as RuleRow | undefined; 142 143 if (row == null) { 144 return null; 145 } 146 return rowToPlainRuleWithLatest(row); 147 } 148 149 /** 150 * All rules for an org (latest version string), for GraphQL org.rules and 151 * similar list surfaces. Not filtered by enabled status. 152 */ 153 async getRulesForOrg( 154 orgId: string, 155 opts?: { readFromReplica?: boolean }, 156 ): Promise<PlainRuleWithLatestVersion[]> { 157 const readFromReplica = opts?.readFromReplica ?? true; 158 const pg = readFromReplica ? this.pgQueryReplica : this.pgQuery; 159 const rows = (await pg 160 .selectFrom('public.rules as r') 161 .leftJoin('public.rules_latest_versions as rlv', 'rlv.rule_id', 'r.id') 162 .select(ruleSelect) 163 .where('r.org_id', '=', orgId) 164 .orderBy('r.name', 'asc') 165 .execute()) as RuleRow[]; 166 167 return rows.map(rowToPlainRuleWithLatest); 168 } 169 170 async findEnabledUserRules() { 171 const today = String(getUtcDateOnlyString()); 172 const rows = (await this.pgQueryReplica 173 .selectFrom('public.rules as r') 174 .leftJoin('public.rules_latest_versions as rlv', 'rlv.rule_id', 'r.id') 175 .select(ruleSelect) 176 .where('r.rule_type', '=', RuleType.USER) 177 .where( 178 sql<boolean>`not exists (select 1 from public.rules_and_item_types rit where rit.rule_id = r.id)`, 179 ) 180 .where((eb) => 181 eb.or([ 182 eb('r.expiration_time', 'is', null), 183 eb('r.expiration_time', '>', sql<Date>`now()`), 184 ]), 185 ) 186 .where('r.status_if_unexpired', 'in', [ 187 RuleStatus.LIVE, 188 RuleStatus.BACKGROUND, 189 ]) 190 .where(enabledQuotaWhere(today)) 191 .execute()) as RuleRow[]; 192 193 return rows.map(rowToPlainRuleWithLatest); 194 } 195}