Mirror of https://github.com/roostorg/coop
github.com/roostorg/coop
1import { AuthenticationError } from 'apollo-server-express';
2
3import {
4 hasPermission,
5 UserPermission,
6} from '../../models/types/permissioning.js';
7import { isCoopErrorOfType } from '../../utils/errors.js';
8import {
9 isNonEmptyArray,
10 isNonEmptyString,
11 type NonEmptyArray,
12 type NonEmptyString,
13} from '../../utils/typescript-types.js';
14import { transformConditionForDB } from '../datasources/RuleApi.js';
15import {
16 type GQLMutationResolvers,
17 type GQLQueryResolvers,
18 type GQLRoutingRuleResolvers,
19} from '../generated.js';
20import { gqlErrorResult, gqlSuccessResult } from '../utils/gqlResult.js';
21
22const typeDefs = /* GraphQL */ `
23 type RoutingRule {
24 id: ID!
25 name: String!
26 creatorId: String!
27 description: String
28 itemTypes: [ItemType!]!
29 status: RoutingRuleStatus!
30 conditionSet: ConditionSet!
31 destinationQueue: ManualReviewQueue!
32 }
33
34 enum RoutingRuleStatus {
35 LIVE
36 }
37
38 input CreateRoutingRuleInput {
39 name: String!
40 description: String
41 status: RoutingRuleStatus!
42 itemTypeIds: [ID!]!
43 conditionSet: ConditionSetInput!
44 destinationQueueId: ID!
45 sequenceNumber: Int
46 isAppealsRule: Boolean
47 }
48
49 input UpdateRoutingRuleInput {
50 id: ID!
51 name: String
52 description: String
53 status: RoutingRuleStatus
54 itemTypeIds: [ID!]
55 conditionSet: ConditionSetInput
56 destinationQueueId: ID
57 sequenceNumber: Int
58 isAppealsRule: Boolean
59 }
60
61 input ReorderRoutingRulesInput {
62 order: [ID!]!
63 isAppealsRule: Boolean
64 }
65
66 input DeleteRoutingRuleInput {
67 id: ID!
68 isAppealsRule: Boolean
69 }
70
71 type RoutingRuleNameExistsError implements Error {
72 title: String!
73 status: Int!
74 type: [String!]!
75 pointer: String
76 detail: String
77 requestId: String
78 }
79
80 type QueueDoesNotExistError implements Error {
81 title: String!
82 status: Int!
83 type: [String!]!
84 pointer: String
85 detail: String
86 requestId: String
87 }
88
89 type MutateRoutingRuleSuccessResponse {
90 data: RoutingRule!
91 }
92
93 type MutateRoutingRulesOrderSuccessResponse {
94 data: [RoutingRule!]!
95 }
96
97 union CreateRoutingRuleResponse =
98 MutateRoutingRuleSuccessResponse
99 | RoutingRuleNameExistsError
100 | QueueDoesNotExistError
101
102 union UpdateRoutingRuleResponse =
103 MutateRoutingRuleSuccessResponse
104 | RoutingRuleNameExistsError
105 | NotFoundError
106 | QueueDoesNotExistError
107
108 union ReorderRoutingRulesResponse = MutateRoutingRulesOrderSuccessResponse
109
110 type Mutation {
111 createRoutingRule(
112 input: CreateRoutingRuleInput!
113 ): CreateRoutingRuleResponse!
114 updateRoutingRule(
115 input: UpdateRoutingRuleInput!
116 ): UpdateRoutingRuleResponse!
117 deleteRoutingRule(input: DeleteRoutingRuleInput!): Boolean!
118 reorderRoutingRules(
119 input: ReorderRoutingRulesInput!
120 ): ReorderRoutingRulesResponse!
121 }
122`;
123
124const RoutingRule: GQLRoutingRuleResolvers = {
125 async destinationQueue(routingRule, _, context) {
126 const user = context.getUser();
127 if (!user || user.orgId !== routingRule.orgId) {
128 throw new AuthenticationError('User required');
129 }
130
131 const userCanEditMRTQueues = hasPermission(
132 UserPermission.EDIT_MRT_QUEUES,
133 user.role,
134 );
135
136 const queueSelector = {
137 orgId: user.orgId,
138 queueId: routingRule.destinationQueueId,
139 };
140
141 const queue = userCanEditMRTQueues
142 ? await context.services.ManualReviewToolService.getQueueForOrgAndDangerouslyBypassPermissioning(
143 queueSelector,
144 )
145 : await context.services.ManualReviewToolService.getQueueForOrg({
146 userId: user.id,
147 ...queueSelector,
148 });
149
150 // Assume the queue won't be missing, as the db requires routing rules to
151 // point to existing queues -- although technically the queue could've been
152 // deleted between loading the rule and querying for the queue.
153 return queue!;
154 },
155 async itemTypes(routingRule, _, context) {
156 const user = context.getUser();
157 if (!user || user.orgId !== routingRule.orgId) {
158 throw new AuthenticationError('User required');
159 }
160
161 const itemTypes =
162 await context.services.ModerationConfigService.getItemTypes({
163 orgId: user.orgId,
164 });
165
166 return itemTypes.filter((itemType) =>
167 routingRule.itemTypeIds.includes(itemType.id),
168 );
169 },
170};
171
172const Query: GQLQueryResolvers = {};
173
174const Mutation: GQLMutationResolvers = {
175 async createRoutingRule(_, params, context) {
176 const user = context.getUser();
177 const { itemTypeIds } = params.input;
178
179 if (user == null) {
180 throw new AuthenticationError('User required.');
181 }
182
183 if (!itemTypeIdsAreValid(itemTypeIds)) {
184 throw new Error('itemTypeIds must be a non-empty array');
185 }
186
187 try {
188 const routingRule =
189 await context.services.ManualReviewToolService.createRoutingRule({
190 ...params.input,
191 itemTypeIds,
192 orgId: user.orgId,
193 creatorId: user.id,
194 conditionSet: transformConditionForDB(params.input.conditionSet),
195 isAppealsRule: params.input.isAppealsRule ?? false,
196 });
197
198 return gqlSuccessResult(
199 { data: routingRule },
200 'MutateRoutingRuleSuccessResponse',
201 );
202 } catch (e: unknown) {
203 if (
204 isCoopErrorOfType(e, [
205 'RoutingRuleNameExistsError',
206 'QueueDoesNotExistError',
207 ])
208 ) {
209 return gqlErrorResult(e);
210 }
211
212 throw e;
213 }
214 },
215 async updateRoutingRule(_, params, context) {
216 const user = context.getUser();
217 const { itemTypeIds } = params.input;
218 if (user == null) {
219 throw new AuthenticationError('User required.');
220 }
221
222 if (itemTypeIds && !itemTypeIdsAreValid(itemTypeIds)) {
223 throw new Error('itemTypeIds must be a non-empty array');
224 }
225
226 try {
227 const routingRule =
228 await context.services.ManualReviewToolService.updateRoutingRule({
229 id: params.input.id,
230 orgId: user.orgId,
231 name: params.input.name ?? undefined,
232 description: params.input.description ?? undefined,
233 status: params.input.status ?? undefined,
234 itemTypeIds: itemTypeIds ?? undefined,
235 destinationQueueId: params.input.destinationQueueId ?? undefined,
236 conditionSet: params.input.conditionSet
237 ? transformConditionForDB(params.input.conditionSet)
238 : undefined,
239 sequenceNumber: params.input.sequenceNumber ?? undefined,
240 isAppealsRule: params.input.isAppealsRule ?? false,
241 });
242
243 return gqlSuccessResult(
244 { data: routingRule },
245 'MutateRoutingRuleSuccessResponse',
246 );
247 } catch (e: unknown) {
248 if (
249 isCoopErrorOfType(e, [
250 'RoutingRuleNameExistsError',
251 'NotFoundError',
252 'QueueDoesNotExistError',
253 ])
254 ) {
255 return gqlErrorResult(e);
256 }
257
258 throw e;
259 }
260 },
261 async deleteRoutingRule(_, params, context) {
262 const user = context.getUser();
263 if (user == null) {
264 throw new AuthenticationError('User required.');
265 }
266
267 return context.services.ManualReviewToolService.deleteRoutingRule({
268 id: params.input.id,
269 isAppealsRule: params.input.isAppealsRule ?? false,
270 });
271 },
272 async reorderRoutingRules(_, params, context) {
273 const user = context.getUser();
274 if (user == null) {
275 throw new AuthenticationError('User required.');
276 }
277
278 const { order } = params.input;
279 const reorderedRules =
280 await context.services.ManualReviewToolService.reorderRoutingRules({
281 orgId: user.orgId,
282 order,
283 isAppealsRule: params.input.isAppealsRule ?? false,
284 });
285
286 return gqlSuccessResult(
287 { data: reorderedRules },
288 'MutateRoutingRulesOrderSuccessResponse',
289 );
290 },
291};
292
293const resolvers = {
294 RoutingRule,
295 Query,
296 Mutation,
297};
298
299export { typeDefs, resolvers };
300
301function itemTypeIdsAreValid(
302 arr: readonly string[],
303): arr is NonEmptyArray<NonEmptyString> {
304 return isNonEmptyArray(arr) && arr.every(isNonEmptyString);
305}