Mirror of https://github.com/roostorg/coop
github.com/roostorg/coop
1import { type Kysely } from 'kysely';
2
3import {
4 CoopError,
5 ErrorType,
6 type ErrorInstanceData,
7} from '../../../utils/errors.js';
8import { isUniqueViolationError } from '../../../utils/kysely.js';
9import { removeUndefinedKeys } from '../../../utils/misc.js';
10import { type ModerationConfigServicePg } from '../dbTypes.js';
11
12const userStrikeThresholdSelection = [
13 'id',
14 'org_id as orgId',
15 'threshold',
16 'actions',
17] as const;
18
19export default class UserStrikeOperations {
20 constructor(
21 private readonly pgQuery: Kysely<ModerationConfigServicePg>,
22 private readonly pgQueryReplica: Kysely<ModerationConfigServicePg>,
23 ) {}
24
25 async getUserStrikeThresholds(opts: {
26 orgId: string;
27 readFromReplica?: boolean;
28 }) {
29 const { orgId, readFromReplica } = opts;
30 const pgQuery = this.#getPgQuery(readFromReplica);
31 const query = pgQuery
32 .selectFrom('public.user_strike_thresholds')
33 .select(userStrikeThresholdSelection)
34 .where('org_id', '=', orgId);
35 return query.execute();
36 }
37
38 async createUserStrikeThreshold(opts: {
39 orgId: string;
40 thresholdSettings: {
41 threshold: number;
42 actions: string[];
43 };
44 }) {
45 const { orgId: org_id, thresholdSettings } = opts;
46
47 try {
48 return await this.pgQuery
49 .insertInto('public.user_strike_thresholds')
50 .values({
51 org_id,
52 threshold: thresholdSettings.threshold,
53 actions: thresholdSettings.actions,
54 })
55 .returning(userStrikeThresholdSelection)
56 .executeTakeFirstOrThrow();
57 } catch (e: unknown) {
58 throw isUniqueViolationError(e)
59 ? makeThresholdAlreadyExistsError({ shouldErrorSpan: true })
60 : e;
61 }
62 }
63
64 async updateUserStrikeThreshold(opts: {
65 orgId: string;
66 thresholdSettings: {
67 id: string;
68 threshold?: number;
69 actions?: string[];
70 };
71 }) {
72 const { orgId, thresholdSettings } = opts;
73
74 try {
75 return await this.pgQuery
76 .updateTable('public.user_strike_thresholds')
77 .set(
78 removeUndefinedKeys({
79 threshold: thresholdSettings.threshold,
80 actions: thresholdSettings.actions,
81 }),
82 )
83 .where('id', '=', thresholdSettings.id)
84 .where('org_id', '=', orgId)
85 .returning(userStrikeThresholdSelection)
86 .executeTakeFirstOrThrow();
87 } catch (e: unknown) {
88 throw isUniqueViolationError(e)
89 ? makeThresholdAlreadyExistsError({ shouldErrorSpan: true })
90 : e;
91 }
92 }
93 /**
94 * Set all user strike thresholds for an organization.
95 * this replaces all existing thresholds with the new set
96 */
97 async setAllUserStrikeThresholds(opts: {
98 orgId: string;
99 thresholds: readonly {
100 threshold: number;
101 actions: readonly string[];
102 }[];
103 }) {
104 await this.pgQuery.transaction().execute(async (trx) => {
105 await trx
106 .deleteFrom('public.user_strike_thresholds')
107 .where('org_id', '=', opts.orgId)
108 .execute();
109 for (const threshold of opts.thresholds) {
110 await trx
111 .insertInto('public.user_strike_thresholds')
112 .values({
113 org_id: opts.orgId,
114 threshold: threshold.threshold,
115 actions: [...threshold.actions],
116 })
117 .onConflict((oc) =>
118 oc.columns(['org_id', 'threshold']).doUpdateSet({
119 actions: [...threshold.actions],
120 }),
121 )
122 .execute();
123 }
124 });
125 }
126
127 async deleteUserStrikeThreshold(opts: {
128 orgId: string;
129 threshold: number;
130 id: string;
131 }) {
132 const { orgId, id, threshold } = opts;
133
134 const rowsDeleted = await this.pgQuery
135 .deleteFrom('public.user_strike_thresholds')
136 .where('org_id', '=', orgId)
137 .where('id', '=', id)
138 .where('threshold', '=', threshold)
139 .execute();
140
141 return rowsDeleted.length === 1;
142 }
143
144 #getPgQuery(readFromReplica: boolean = false) {
145 return readFromReplica ? this.pgQueryReplica : this.pgQuery;
146 }
147}
148
149export type UserStrikeThresholdErrorType =
150 'UserStrikeThresholdAlreadyExistsError';
151
152// TODO: throw this error on failed policy creation/update when appropriate.
153export const makeThresholdAlreadyExistsError = (data: ErrorInstanceData) =>
154 new CoopError({
155 status: 409,
156 type: [ErrorType.UniqueViolation],
157 title:
158 'A rule with that threshold value already exists in this organization.',
159 name: 'UserStrikeThresholdAlreadyExistsError',
160 ...data,
161 });