Mirror of https://github.com/roostorg/coop
github.com/roostorg/coop
1import type { Kysely } from 'kysely';
2import { uid } from 'uid';
3
4import {
5 CoopError,
6 ErrorType,
7 type ErrorInstanceData,
8} from '../../../utils/errors.js';
9import { isUniqueViolationError } from '../../../utils/kysely.js';
10import type { ModerationConfigServicePg } from '../dbTypes.js';
11
12const TEXT_BANK_COLUMNS = [
13 'id',
14 'name',
15 'description',
16 'type',
17 'strings',
18 'org_id as orgId',
19 'created_at as createdAt',
20 'updated_at as updatedAt',
21 'owner_id as ownerId',
22] as const;
23
24export default class MatchingBankOperations {
25 constructor(
26 private readonly pgQuery: Kysely<ModerationConfigServicePg>,
27 private readonly pgQueryReplica: Kysely<ModerationConfigServicePg>,
28 ) {}
29
30 async getTextBank(opts: {
31 orgId: string;
32 id: string;
33 readFromReplica?: boolean;
34 }) {
35 const { orgId, id, readFromReplica } = opts;
36 const pgQuery = readFromReplica ? this.pgQueryReplica : this.pgQuery;
37
38 const textBank = await pgQuery
39 .selectFrom('public.text_banks')
40 .select(TEXT_BANK_COLUMNS)
41 .where('org_id', '=', orgId)
42 .where('id', '=', id)
43 .executeTakeFirst();
44
45 if (!textBank) {
46 throw new CoopError({
47 status: 404,
48 type: [ErrorType.NotFound],
49 title: 'Text bank not found',
50 name: 'MatchingBankNotFoundError',
51 shouldErrorSpan: true,
52 });
53 }
54
55 return textBank;
56 }
57
58 async getTextBanks(opts: { orgId: string; readFromReplica?: boolean }) {
59 const { orgId, readFromReplica } = opts;
60 const pgQuery = readFromReplica ? this.pgQueryReplica : this.pgQuery;
61
62 const textBanks = await pgQuery
63 .selectFrom('public.text_banks')
64 .select(TEXT_BANK_COLUMNS)
65 .where('org_id', '=', orgId)
66 .execute();
67
68 return textBanks;
69 }
70
71 async createTextBank(
72 orgId: string,
73 input: {
74 name: string;
75 description: string | null;
76 type: 'STRING' | 'REGEX';
77 ownerId?: string | null;
78 strings: string[];
79 },
80 ) {
81 const { name, description, type, strings, ownerId } = input;
82
83 try {
84 const newTextBank = await this.pgQuery
85 .insertInto('public.text_banks')
86 .values({
87 id: uid(),
88 name,
89 description,
90 type,
91 strings,
92 org_id: orgId,
93 updated_at: new Date(),
94 owner_id: ownerId,
95 })
96 .returning(TEXT_BANK_COLUMNS)
97 .executeTakeFirstOrThrow();
98
99 return newTextBank;
100 } catch (error) {
101 if (isUniqueViolationError(error)) {
102 throw makeMatchingBankNameExistsError({ shouldErrorSpan: true });
103 }
104 throw error;
105 }
106 }
107
108 async updateTextBank(
109 orgId: string,
110 input: {
111 id: string;
112 name?: string;
113 description?: string | null;
114 type?: 'STRING' | 'REGEX';
115 ownerId?: string | null;
116 strings?: string[];
117 },
118 ) {
119 const { id, name, description, type, strings, ownerId } = input;
120
121 try {
122 const updatedTextBank = await this.pgQuery
123 .updateTable('public.text_banks')
124 .set({
125 name,
126 description,
127 type,
128 strings,
129 owner_id: ownerId,
130 updated_at: new Date(),
131 })
132 .where('id', '=', id)
133 .where('org_id', '=', orgId)
134 .returning(TEXT_BANK_COLUMNS)
135 .executeTakeFirstOrThrow();
136
137 return updatedTextBank;
138 } catch (error) {
139 if (isUniqueViolationError(error)) {
140 throw makeMatchingBankNameExistsError({ shouldErrorSpan: true });
141 }
142 throw error;
143 }
144 }
145
146 async deleteTextBank(orgId: string, id: string) {
147 const rowsDeleted = await this.pgQuery
148 .deleteFrom('public.text_banks')
149 .where('id', '=', id)
150 .where('org_id', '=', orgId)
151 .execute();
152
153 return rowsDeleted.length === 1;
154 }
155}
156
157export type MatchingBankErrorType =
158 | 'MatchingBankNameExistsError'
159 | 'MatchingBankNotFoundError';
160
161export const makeMatchingBankNameExistsError = (data: ErrorInstanceData) =>
162 new CoopError({
163 status: 409,
164 type: [ErrorType.UniqueViolation],
165 title:
166 'A matching bank with that name already exists in this organization.',
167 name: 'MatchingBankNameExistsError',
168 ...data,
169 });