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.

[Vulnerabilities] Remove lodash by migrating from better mutation to plugin-functional and fix minor lint warnings (#216)

* [Vulnerabilities] Remove lodash by migrating from better mutation to plugin-functional and fix minor lint wwarnings

* remove bad merge main

authored by

Juan Mrad and committed by
GitHub
6883a148 dab8cace

+923 -761
+1 -1
client/src/components/ErrorBoundary.tsx
··· 13 13 }; 14 14 15 15 // This is used twice so it has to be extracted into an interface 16 - // eslint-disable-next-line no-restricted-syntax 16 + 17 17 interface ErrorBoundaryProps { 18 18 children: React.ReactNode; 19 19 buttonTitle?: string;
+1 -1
client/src/utils/itemUtils.ts
··· 82 82 if (Array.isArray(fieldValue)) { 83 83 throw new Error('Unexpected array when getting field value'); 84 84 } 85 - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion 85 + 86 86 return fieldValue.value satisfies ScalarTypeRuntimeType<ScalarType> as ScalarTypeRuntimeType< 87 87 FieldRoleToScalarType[Role & keyof FieldRoleToScalarType] 88 88 >;
+1 -1
client/src/utils/tree.test.ts
··· 43 43 const filteredTree = tree.filterTree((node) => node.includes(filterString)); 44 44 45 45 expect(filteredTree).not.toBeNull(); 46 - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion 46 + 47 47 const filteredTreeValues = convertTreeToList(filteredTree!); 48 48 expect( 49 49 filteredTreeValues
+1 -1
client/src/utils/tree.ts
··· 89 89 90 90 size() { 91 91 let size = 0; 92 - // eslint-disable-next-line @typescript-eslint/no-unused-vars 92 + 93 93 for (const _ of this.preOrderTraversal()) { 94 94 size++; 95 95 }
+10 -6
client/src/webpages/App.tsx
··· 1 - /* eslint-disable react/jsx-key */ 2 1 import React, { Suspense } from 'react'; 3 2 import { 4 3 createBrowserRouter, ··· 15 14 import { RequireAuth, RequireLoggedOut } from '../routing/auth'; 16 15 import AwaitingApproval from './auth/AwaitingApproval'; 17 16 import RejectedByAdmin from './auth/RejectedByAdmin'; 17 + 18 18 import './dashboard/Dashboard.css'; 19 19 20 20 const Login = React.lazy(async () => import('./auth/Login')); 21 - const ForgotPassword = React.lazy(async () => import('./auth/forgot_password/ForgotPassword')); 22 - const ResetPassword = React.lazy(async () => import('./auth/forgot_password/ResetPassword')); 21 + const ForgotPassword = React.lazy( 22 + async () => import('./auth/forgot_password/ForgotPassword'), 23 + ); 24 + const ResetPassword = React.lazy( 25 + async () => import('./auth/forgot_password/ResetPassword'), 26 + ); 23 27 const SignUp = React.lazy(async () => import('./auth/SignUp')); 24 28 const LoginSSO = React.lazy(async () => import('./auth/LoginSSO')); 25 29 /** ··· 37 41 38 42 function RootRedirect() { 39 43 const { loading, data } = useGQLUserAndOrgQuery(); 40 - 44 + 41 45 if (loading) { 42 46 return <FullScreenLoading />; 43 47 } 44 - 48 + 45 49 // If user is logged in, redirect to dashboard, otherwise to login 46 50 if (data?.me) { 47 51 return <Navigate to="/dashboard" replace />; 48 52 } 49 - 53 + 50 54 return <Navigate to="/login" replace />; 51 55 } 52 56
+1 -1
client/src/webpages/dashboard/item_types/ItemTypeRequestCodeSamples.tsx
··· 99 99 }} 100 100 style={{ 101 101 ...atelierSulphurpoolLight, 102 - // eslint-disable-next-line id-denylist 102 + 103 103 'hljs-string': { 104 104 color: '#75787B', 105 105 },
+16 -9
client/src/webpages/dashboard/mrt/queue_routing/condition/matching_values/ManualReviewQueueRuleConditionMediaMatchingValues.tsx
··· 1 - /* eslint-disable no-console */ 2 1 import { Select } from 'antd'; 3 2 4 3 import ComponentLoading from '../../../../../../components/common/ComponentLoading'; 5 4 import { selectFilterByLabelOption } from '@/webpages/dashboard/components/antDesignUtils'; 5 + 6 6 import { useGQLHashBanksQuery } from '../../../../../../graphql/generated'; 7 7 import { RuleFormLeafCondition } from '../../../../rules/types'; 8 8 import { ManualReviewQueueRoutingStaticTokenField } from '../../ManualReviewQueueRoutingStaticField'; ··· 15 15 onUpdateSelectedBankIds(imageBankIds: readonly string[]): void; 16 16 allConditions?: RuleFormLeafCondition[]; 17 17 }) { 18 - const { condition, editing, onUpdateSelectedBankIds, allConditions = [] } = props; 18 + const { 19 + condition, 20 + editing, 21 + onUpdateSelectedBankIds, 22 + allConditions = [], 23 + } = props; 19 24 20 25 const { loading, error, data } = useGQLHashBanksQuery(); 21 26 const hashBanks = data?.hashBanks ?? []; ··· 24 29 const selectedBankIds = new Set( 25 30 allConditions 26 31 .filter((c) => c !== condition) // Exclude current condition by reference 27 - .flatMap((c) => c.matchingValues?.imageBankIds ?? []) 32 + .flatMap((c) => c.matchingValues?.imageBankIds ?? []), 28 33 ); 29 34 30 35 if (loading) { ··· 48 53 dropdownMatchSelectWidth={false} 49 54 > 50 55 {hashBanks.map((bank) => ( 51 - <Option 52 - key={bank.id} 53 - value={bank.id} 56 + <Option 57 + key={bank.id} 58 + value={bank.id} 54 59 label={bank.name} 55 60 disabled={selectedBankIds.has(bank.id)} 56 61 > ··· 60 65 </Select> 61 66 ) : ( 62 67 <ManualReviewQueueRoutingStaticTokenField 63 - tokens={condition.matchingValues?.imageBankIds?.map(id => 64 - hashBanks.find(bank => bank.id === id)?.name ?? id 65 - ) ?? []} 68 + tokens={ 69 + condition.matchingValues?.imageBankIds?.map( 70 + (id) => hashBanks.find((bank) => bank.id === id)?.name ?? id, 71 + ) ?? [] 72 + } 66 73 /> 67 74 )} 68 75 </div>
+2 -2
client/src/webpages/dashboard/rules/info/insights/ReportingRuleInsightsActionsChart.tsx
··· 2 2 import { InvestmentFilled } from '@/icons'; 3 3 import { BarChartOutlined, LineChartOutlined } from '@ant-design/icons'; 4 4 import { gql } from '@apollo/client'; 5 + import { format } from 'date-fns'; 5 6 import last from 'lodash/last'; 6 7 import orderBy from 'lodash/orderBy'; 7 8 import sortBy from 'lodash/sortBy'; 8 9 import sumBy from 'lodash/sumBy'; 9 - import { format } from 'date-fns'; 10 10 import { ReactNode, useCallback, useMemo, useState } from 'react'; 11 11 import { 12 12 Area, ··· 109 109 .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()) 110 110 .map((actionData) => { 111 111 // change actionData.date format from YYYY-MM-DD to MM/DD 112 - // eslint-disable-next-line @typescript-eslint/no-unused-vars 112 + 113 113 const [year, month, date] = actionData.date.split('-'); 114 114 115 115 return {
+7 -2
client/src/webpages/dashboard/rules/rule_form/condition/matching_values/RuleFormConditionMatchingValues.tsx
··· 1 - /* eslint-disable no-console */ 2 1 import { GQLScalarType } from '../../../../../../graphql/generated'; 3 2 import { ConditionLocation, RuleFormLeafCondition } from '../../../types'; 4 3 import RuleFormConditionLocationMatchingValues from './RuleFormConditionLocationMatchingValues'; ··· 14 13 ) => void; 15 14 allConditions?: RuleFormLeafCondition[]; 16 15 }) { 17 - const { condition, location, inputScalarType, onUpdateMatchingValues, allConditions = [] } = props; 16 + const { 17 + condition, 18 + location, 19 + inputScalarType, 20 + onUpdateMatchingValues, 21 + allConditions = [], 22 + } = props; 18 23 19 24 if ( 20 25 !condition.input ||
+15 -14
client/src/webpages/dashboard/rules/rule_form/signal_modal/RuleFormSignalModalSubcategoryGallery.tsx
··· 47 47 // So we need to allow a search term like "snake case" match against the subcategory 48 48 // "snake_case". To do this, we add a snake case search term. 49 49 const snakeCaseSearchTerm = searchTerm.replaceAll('_', ' '); 50 - const eligibleSubcategories = 51 - rebuildSubcategoryTreeFromGraphQLResponse(stripped) 52 - // First filter out subcategories that don't include the search term. 53 - // eslint-disable-next-line array-callback-return 54 - .filter( 55 - (subcategory) => 56 - subcategory.id.includes(searchTerm) || 57 - subcategory.id.includes(snakeCaseSearchTerm) || 58 - subcategory.label.includes(searchTerm) || 59 - subcategory.label.includes(snakeCaseSearchTerm) || 60 - (subcategory.description && 61 - (subcategory.description.includes(searchTerm) || 62 - subcategory.description.includes(snakeCaseSearchTerm))), 63 - ); 50 + const eligibleSubcategories = rebuildSubcategoryTreeFromGraphQLResponse( 51 + stripped, 52 + ) 53 + // First filter out subcategories that don't include the search term. 54 + 55 + .filter( 56 + (subcategory) => 57 + subcategory.id.includes(searchTerm) || 58 + subcategory.id.includes(snakeCaseSearchTerm) || 59 + subcategory.label.includes(searchTerm) || 60 + subcategory.label.includes(snakeCaseSearchTerm) || 61 + (subcategory.description && 62 + (subcategory.description.includes(searchTerm) || 63 + subcategory.description.includes(snakeCaseSearchTerm))), 64 + ); 64 65 65 66 return ( 66 67 <div className="flex flex-col">
+17 -2
server/.eslintrc.cjs
··· 395 395 '@typescript-eslint', 396 396 'jsdoc', 397 397 'import', 398 - 'better-mutation', 398 + 'functional', 399 399 'switch-statement', 400 400 ], 401 401 rules: { ··· 659 659 { 660 660 files: ['test/**/*.ts', './**/*.{spec,test}.ts'], 661 661 rules: { 662 - 'better-mutation/no-mutation': ['error', { allowThis: true }], 662 + // Match prior test-only mutation policy: allow `this`, class internals, 663 + // and `process.env.*`; production code is not in this override. 664 + 'functional/immutable-data': [ 665 + 'error', 666 + { 667 + ignoreImmediateMutation: true, 668 + ignoreClasses: true, 669 + ignoreAccessorPattern: [ 670 + 'this', 671 + 'this.*', 672 + 'this.*.*', 673 + // Tests toggle env vars and clean up with `delete process.env.*`. 674 + 'process.env.*', 675 + ], 676 + }, 677 + ], 663 678 'no-console': 'off', 664 679 }, 665 680 },
+2 -2
server/api.ts
··· 7 7 import { expressMiddleware } from '@as-integrations/express4'; 8 8 import { makeExecutableSchema } from '@graphql-tools/schema'; 9 9 import { MapperKind, mapSchema } from '@graphql-tools/utils'; 10 + import { MultiSamlStrategy } from '@node-saml/passport-saml'; 10 11 import { SpanStatusCode } from '@opentelemetry/api'; 11 12 import { 12 13 SEMATTRS_EXCEPTION_MESSAGE, ··· 18 19 import cors from 'cors'; 19 20 import express, { type ErrorRequestHandler } from 'express'; 20 21 import session from 'express-session'; 21 - import { buildContext, GraphQLLocalStrategy } from 'graphql-passport'; 22 22 import depthLimit from 'graphql-depth-limit'; 23 + import { buildContext, GraphQLLocalStrategy } from 'graphql-passport'; 23 24 import helmet from 'helmet'; 24 25 import passport from 'passport'; 25 - import { MultiSamlStrategy } from '@node-saml/passport-saml'; 26 26 27 27 import { 28 28 makeLoginIncorrectPasswordError,
+13 -16
server/condition_evaluator/conditionSet.ts
··· 121 121 ? await getConditionSetResults(condition, evaluationContext, tracer) 122 122 : { 123 123 ...condition, 124 - result: await runLeafCondition( 125 - condition, 126 - evaluationContext, 127 - // eslint-disable-next-line no-loop-func 128 - ).catch((e) => { 129 - // If evaluating a condition fails, we're eventually going to want 130 - // to retry before we give up but, for now, we just mark the result 131 - // as failed and move on. 132 - const activeSpan = tracer.getActiveSpan(); 133 - if (activeSpan?.isRecording()) { 134 - activeSpan.recordException(e); 135 - } 136 - return { outcome: ConditionFailureOutcome.ERRORED }; 137 - }), 124 + result: await runLeafCondition(condition, evaluationContext).catch( 125 + (e) => { 126 + // If evaluating a condition fails, we're eventually going to want 127 + // to retry before we give up but, for now, we just mark the result 128 + // as failed and move on. 129 + const activeSpan = tracer.getActiveSpan(); 130 + if (activeSpan?.isRecording()) { 131 + activeSpan.recordException(e); 132 + } 133 + return { outcome: ConditionFailureOutcome.ERRORED }; 134 + }, 135 + ), 138 136 }; 139 137 140 138 result.conditions.push(conditionWithResult as any); ··· 266 264 return getAllAggregationsInConditionSet(condition); 267 265 } 268 266 const sig = condition.signal; 269 - const args = 270 - sig?.type === 'AGGREGATION' ? sig.args : undefined; 267 + const args = sig?.type === 'AGGREGATION' ? sig.args : undefined; 271 268 return args != null ? [args.aggregationClause] : []; 272 269 }); 273 270 }
+12 -2
server/eslint.config.mjs
··· 1 1 import { createRequire } from 'node:module'; 2 2 import { FlatCompat } from '@eslint/eslintrc'; 3 - import { fixupConfigRules } from '@eslint/compat'; 3 + import { fixupConfigRules, fixupPluginRules } from '@eslint/compat'; 4 + import functional from 'eslint-plugin-functional'; 4 5 5 6 const require = createRequire(import.meta.url); 6 7 const { ignorePatterns: _, ...legacyConfig } = require('./.eslintrc.cjs'); ··· 9 10 baseDirectory: import.meta.dirname, 10 11 }); 11 12 12 - const flatConfigs = fixupConfigRules(compat.config(legacyConfig)); 13 + const functionalPlugin = fixupPluginRules(functional); 14 + 15 + const flatConfigs = fixupConfigRules(compat.config(legacyConfig)).map((config) => 16 + config.plugins?.functional 17 + ? { 18 + ...config, 19 + plugins: { ...config.plugins, functional: functionalPlugin }, 20 + } 21 + : config, 22 + ); 13 23 14 24 export default [ 15 25 {
+15 -14
server/graphql/datasources/RuleApi.ts
··· 15 15 import { type User } from '../../models/UserModel.js'; 16 16 import { type ActionCountsInput } from '../../services/actionStatisticsService/index.js'; 17 17 import { type AggregationClause } from '../../services/aggregationsService/index.js'; 18 + import { type ConditionSetWithResultAsLogged } from '../../services/analyticsLoggers/index.js'; 18 19 import { 19 20 RuleType, 20 21 type Condition, 21 22 type ConditionInput, 22 23 type ConditionSet, 23 - type LeafCondition, 24 24 type CoopInput, 25 + type LeafCondition, 25 26 type RuleStatus, 26 27 } from '../../services/moderationConfigService/index.js'; 27 28 import { ··· 37 38 signalIsExternal, 38 39 type SignalId, 39 40 } from '../../services/signalsService/index.js'; 40 - import { type ConditionSetWithResultAsLogged } from '../../services/analyticsLoggers/index.js'; 41 41 import { type DataWarehousePublicSchema } from '../../storage/dataWarehouse/warehouseSchema.js'; 42 42 import { toCorrelationId } from '../../utils/correlationIds.js'; 43 43 import { ··· 172 172 subcategory, 173 173 }; 174 174 175 - // eslint-disable-next-line switch-statement/require-appropriate-default-case 176 175 switch (type) { 177 176 case 'AGGREGATION': 178 177 const aggregationClauseInput = ··· 493 492 494 493 // Validate that signals used in automated rules are allowed 495 494 // Check if the rule will have actions meaning automated rule. 496 - // This ensures we don't allow creating automated rules with signals 495 + // This ensures we don't allow creating automated rules with signals 497 496 // that are restricted to routing rules only. 498 - const willHaveActions = actionIds 499 - ? actionIds.length > 0 497 + const willHaveActions = actionIds 498 + ? actionIds.length > 0 500 499 : (await rule.getActions()).length > 0; 501 - 500 + 502 501 if (willHaveActions && conditionSet) { 503 502 await this.validateSignalsAllowedInAutomatedRules(conditionSet, orgId); 504 503 } ··· 579 578 this.ruleInsights.getContentSubmissionCountsByDay(orgId), 580 579 ]); 581 580 582 - const valueOrEmpty = <T,>(r: PromiseSettledResult<readonly T[]>): readonly T[] => 583 - r.status === 'fulfilled' ? r.value : []; 581 + const valueOrEmpty = <T>( 582 + r: PromiseSettledResult<readonly T[]>, 583 + ): readonly T[] => (r.status === 'fulfilled' ? r.value : []); 584 584 585 585 return { 586 586 actionedSubmissionsByDay: valueOrEmpty(results[0]), ··· 588 588 actionedSubmissionsByTagByDay: valueOrEmpty(results[2]), 589 589 actionedSubmissionsByActionByDay: valueOrEmpty(results[3]), 590 590 totalSubmissionsByDay: valueOrEmpty( 591 - results[4] as PromiseSettledResult<readonly { date: string; count: number }[]>, 591 + results[4] as PromiseSettledResult< 592 + readonly { date: string; count: number }[] 593 + >, 592 594 ), 593 595 }; 594 596 } ··· 848 850 orgId: string, 849 851 ): Promise<void> { 850 852 const signalIds = this.extractSignalIdsFromConditionSet(conditionSet); 851 - 853 + 852 854 for (const signalId of signalIds) { 853 855 const signal = await this.signalsService.getSignal({ 854 856 signalId, 855 857 orgId, 856 858 }); 857 - 859 + 858 860 if (signal && !signal.allowedInAutomatedRules) { 859 861 throw new Error( 860 862 `Signal "${signal.displayName}" cannot be used in automated rules with actions. ` + 861 - `This signal is restricted to routing rules only.` 863 + `This signal is restricted to routing rules only.`, 862 864 ); 863 865 } 864 866 } ··· 950 952 'conditions' | 'conjunction' 951 953 > & 952 954 RequiredWithoutNull<Pick<GQLConditionInput, 'input'>>; 953 -
+9 -10
server/graphql/datasources/UserApi.ts
··· 1 - /* eslint-disable max-classes-per-file */ 2 - 3 1 import { type Exception } from '@opentelemetry/api'; 4 2 import { type PassportContext } from 'graphql-passport'; 5 3 import { uid } from 'uid'; ··· 175 173 }); 176 174 } 177 175 178 - const isCurrentPasswordValid = await this.sequelize.User.passwordMatchesHash( 179 - currentPassword, 180 - user.password, 181 - ); 176 + const isCurrentPasswordValid = 177 + await this.sequelize.User.passwordMatchesHash( 178 + currentPassword, 179 + user.password, 180 + ); 182 181 183 182 if (!isCurrentPasswordValid) { 184 183 throw makeChangePasswordIncorrectPasswordError({ ··· 216 215 const user = await this.sequelize.User.findByPk(id, { 217 216 rejectOnEmpty: true, 218 217 }); 219 - 218 + 220 219 // Security check: ensure admin can only approve users in their own org 221 220 if (user.orgId !== invokerOrgId) { 222 221 throw makeUnauthorizedError( ··· 224 223 { shouldErrorSpan: true }, 225 224 ); 226 225 } 227 - 226 + 228 227 user.approvedByAdmin = true; 229 228 await user.save(); 230 229 return true; ··· 234 233 const user = await this.sequelize.User.findByPk(id, { 235 234 rejectOnEmpty: true, 236 235 }); 237 - 236 + 238 237 // Security check: ensure admin can only reject users in their own org 239 238 if (user.orgId !== invokerOrgId) { 240 239 throw makeUnauthorizedError( ··· 242 241 { shouldErrorSpan: true }, 243 242 ); 244 243 } 245 - 244 + 246 245 user.rejectedByAdmin = true; 247 246 await user.save(); 248 247 return true;
+8 -2
server/graphql/modules/actionStatistics.ts
··· 114 114 }); 115 115 } catch (e) { 116 116 // eslint-disable-next-line no-console 117 - console.error('actionStatistics: warehouse query failed:', (e as Error).message); 117 + console.error( 118 + 'actionStatistics: warehouse query failed:', 119 + (e as Error).message, 120 + ); 118 121 return []; 119 122 } 120 123 }, ··· 141 144 })); 142 145 } catch (e) { 143 146 // eslint-disable-next-line no-console 144 - console.error('topPolicyViolations: warehouse query failed:', (e as Error).message); 147 + console.error( 148 + 'topPolicyViolations: warehouse query failed:', 149 + (e as Error).message, 150 + ); 145 151 return []; 146 152 } 147 153 },
+1 -1
server/graphql/modules/insights.ts
··· 351 351 // here, meaning that graphql queries would throw an exception if asking for 352 352 // those fields. To provide them, we'll have to update (and add better 353 353 // typings for) getRulePassingContentSamples. 354 - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion 354 + 355 355 { 356 356 ...result, 357 357 itemId: result.contentId,
-1
server/graphql/modules/signal.test.ts
··· 9 9 id: 'Hate Groups', 10 10 label: 'Hate Groups', 11 11 children: [ 12 - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions 13 12 { 14 13 id: 'yes_nazi' as const, 15 14 label: 'Nazi Content',
+78 -74
server/iocContainer/index.ts
··· 20 20 import { v1 as uuidv1 } from 'uuid'; 21 21 22 22 import makeDb from '../models/index.js'; 23 + import { type PolicyActionPenalties } from '../models/OrgModel.js'; 24 + import type { IActionExecutionsAdapter } from '../plugins/warehouse/queries/IActionExecutionsAdapter.js'; 25 + import type { IActionStatisticsAdapter } from '../plugins/warehouse/queries/IActionStatisticsAdapter.js'; 26 + import type { IContentApiRequestsAdapter } from '../plugins/warehouse/queries/IContentApiRequestsAdapter.js'; 23 27 import { 28 + ClickhouseActionExecutionsAdapter, 29 + ClickhouseActionStatisticsAdapter, 30 + ClickhouseContentApiRequestsAdapter, 31 + ClickhouseOrgCreationAdapter, 32 + ClickhouseReportingAnalyticsAdapter, 33 + } from '../plugins/warehouse/queries/index.js'; 34 + import type { IOrgCreationAdapter } from '../plugins/warehouse/queries/IOrgCreationAdapter.js'; 35 + import type { IReportingAnalyticsAdapter } from '../plugins/warehouse/queries/IReportingAnalyticsAdapter.js'; 36 + import { 37 + ITEM_SUBMISSION_DLQ_NAME, 38 + ITEM_SUBMISSION_QUEUE_NAME, 24 39 makeItemSubmissionBulkWrite, 25 40 type ItemSubmissionBulkWrite, 26 - ITEM_SUBMISSION_QUEUE_NAME, 27 - ITEM_SUBMISSION_DLQ_NAME, 28 41 } from '../queues/itemSubmissionQueue.js'; 29 - import { type PolicyActionPenalties } from '../models/OrgModel.js'; 30 - import { type HashBank, HashBankService } from '../services/hmaService/index.js'; 31 42 import makeActionPublisher, { 32 43 type ActionPublisher, 33 44 type ActionTargetItem, ··· 66 77 type StringNumberKeyValueStore, 67 78 } from '../services/aggregationsService/index.js'; 68 79 import { 80 + makeActionExecutionLogger, 81 + makeContentApiLogger, 82 + makeItemModelScoreLogger, 83 + makeOrgCreationLogger, 84 + makeReportingRuleExecutionLogger, 85 + makeRoutingRuleExecutionLogger, 86 + makeRuleExecutionLogger, 87 + type ActionExecutionLogger, 88 + type ContentApiLogger, 89 + type ItemModelScoreLogger, 90 + type OrgCreationLogger, 91 + type ReportingRuleExecutionLogger, 92 + type RoutingRuleExecutionLogger, 93 + type RuleExecutionLogger, 94 + } from '../services/analyticsLoggers/index.js'; 95 + import { 96 + makeItemHistoryQueries, 97 + makeRuleActionInsights, 98 + makeUserHistoryQueries, 99 + type ItemHistoryQueries, 100 + type RuleActionInsights, 101 + type UserHistoryQueries, 102 + } from '../services/analyticsQueries/index.js'; 103 + import { 69 104 makeApiKeyService, 70 105 type ApiKeyService, 71 106 } from '../services/apiKeyService/index.js'; 72 - import makeHmaService from '../services/hmaService/index.js'; 73 107 import { type CombinedPg } from '../services/combinedDbTypes.js'; 74 108 import { 75 109 makeDerivedFieldsService, 76 110 type DerivedFieldsService, 77 111 } from '../services/derivedFieldsService/index.js'; 112 + import makeHmaService, { 113 + HashBankService, 114 + type HashBank, 115 + } from '../services/hmaService/index.js'; 78 116 import { ItemInvestigationService } from '../services/itemInvestigationService/index.js'; 79 117 import { 80 118 getFieldValueForRole, ··· 158 196 } from '../services/sendEmailService/index.js'; 159 197 import { 160 198 makeSignalAuthService, 161 - 162 199 type SignalAuthService, 163 200 } from '../services/signalAuthService/index.js'; 164 201 import { ··· 184 221 type UserScore, 185 222 } from '../services/userStatisticsService/index.js'; 186 223 import { UserStrikeService } from '../services/userStrikeService/index.js'; 187 - import { 188 - makeActionExecutionLogger, 189 - makeContentApiLogger, 190 - makeItemModelScoreLogger, 191 - makeOrgCreationLogger, 192 - makeReportingRuleExecutionLogger, 193 - makeRoutingRuleExecutionLogger, 194 - makeRuleExecutionLogger, 195 - type ActionExecutionLogger, 196 - type ContentApiLogger, 197 - type ItemModelScoreLogger, 198 - type OrgCreationLogger, 199 - type ReportingRuleExecutionLogger, 200 - type RoutingRuleExecutionLogger, 201 - type RuleExecutionLogger, 202 - } from '../services/analyticsLoggers/index.js'; 203 - import { 204 - makeItemHistoryQueries, 205 - makeRuleActionInsights, 206 - makeUserHistoryQueries, 207 - type ItemHistoryQueries, 208 - type RuleActionInsights, 209 - type UserHistoryQueries, 210 - } from '../services/analyticsQueries/index.js'; 224 + import type { IDataWarehouseAnalytics } from '../storage/dataWarehouse/IDataWarehouseAnalytics.js'; 211 225 import { 212 226 DataWarehouseFactory, 213 227 type IDataWarehouse, 214 228 type IDataWarehouseDialect, 215 229 } from '../storage/dataWarehouse/index.js'; 216 - import type { IDataWarehouseAnalytics } from '../storage/dataWarehouse/IDataWarehouseAnalytics.js'; 217 - import { 218 - ClickhouseActionStatisticsAdapter, 219 - ClickhouseReportingAnalyticsAdapter, 220 - ClickhouseActionExecutionsAdapter, 221 - ClickhouseContentApiRequestsAdapter, 222 - ClickhouseOrgCreationAdapter, 223 - } from '../plugins/warehouse/queries/index.js'; 224 - import type { IActionStatisticsAdapter } from '../plugins/warehouse/queries/IActionStatisticsAdapter.js'; 225 - import type { IReportingAnalyticsAdapter } from '../plugins/warehouse/queries/IReportingAnalyticsAdapter.js'; 226 - import type { IActionExecutionsAdapter } from '../plugins/warehouse/queries/IActionExecutionsAdapter.js'; 227 - import type { IContentApiRequestsAdapter } from '../plugins/warehouse/queries/IContentApiRequestsAdapter.js'; 228 - import type { IOrgCreationAdapter } from '../plugins/warehouse/queries/IOrgCreationAdapter.js'; 229 230 import { cached, type Cached } from '../utils/caching.js'; 231 + import { CoopMeter } from '../utils/CoopMeter.js'; 230 232 import { 231 233 toCorrelationId, 232 234 type CorrelationId, 233 235 } from '../utils/correlationIds.js'; 234 - import { CoopMeter } from '../utils/CoopMeter.js'; 235 236 import { getUsableCoreCount } from '../utils/cpu-helpers.js'; 236 237 import { jsonStringify, type JsonOf } from '../utils/encoding.js'; 237 238 import { __throw, assertUnreachable } from '../utils/misc.js'; ··· 532 533 }), 533 534 ); 534 535 535 - 536 536 bottle.factory('Sequelize', () => makeDb()); 537 537 bottle.factory('OrgModel', ({ Sequelize }) => Sequelize.Org); 538 538 bottle.factory('RuleModel', ({ Sequelize }) => Sequelize.Rule); ··· 554 554 // Switch warehouse providers via WAREHOUSE_ADAPTER. 555 555 // 556 556 // - 'DataWarehouse' - Core queries and transactions 557 - // - 'DataWarehouseDialect' - Type-safe Kysely queries 557 + // - 'DataWarehouseDialect' - Type-safe Kysely queries 558 558 // - 'DataWarehouseAnalytics' - Bulk writes, CDC, logging 559 559 bottle.factory('DataWarehouse', () => { 560 560 const config = DataWarehouseFactory.createConfigFromEnv(); ··· 987 987 // could provide that would have security implications when blindly fed 988 988 // in here -- like something that would somehow lead fetch to do something 989 989 // unexpected. 990 - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion 990 + 991 991 ...((appealHeaders as JsonObject | undefined) ?? undefined), 992 992 // Put this header last so customHeaders can't override it, which I 993 993 // think makes sense, since there's no way for users to effect the ··· 1029 1029 // API is in place 1030 1030 const additionalPayload = job.payload.reportedForReasons 1031 1031 ? { 1032 - // eslint-disable-next-line no-restricted-syntax 1033 1032 reportHistory: job.payload.reportedForReasons.map( 1034 1033 (it) => ({ 1035 1034 reason: it.reason, ··· 1153 1152 incidentType: decision.incidentType, 1154 1153 ...(decision.escalateToHighPriority != null && 1155 1154 decision.escalateToHighPriority.trim() !== '' 1156 - ? { escalateToHighPriority: decision.escalateToHighPriority.trim() } 1155 + ? { 1156 + escalateToHighPriority: 1157 + decision.escalateToHighPriority.trim(), 1158 + } 1157 1159 : {}), 1158 1160 }, 1159 1161 isTest, ··· 1354 1356 }, 1355 1357 ); 1356 1358 1357 - bottle.factory( 1358 - 'getImageBankEventuallyConsistent', 1359 - (container) => { 1360 - const kyselyPg = container.KyselyPg; 1361 - const hashBankService = new HashBankService(kyselyPg); 1362 - 1363 - return cached({ 1364 - async producer({ orgId, bankId }) { 1365 - // bankId could be either database ID or bank name 1366 - // Check if bankId is numeric (database ID) 1367 - const numericBankId = parseInt(bankId); 1368 - if (!isNaN(numericBankId)) { 1369 - // Get by database ID 1370 - return hashBankService.findById(numericBankId, orgId); 1371 - } else { 1372 - // Get by name 1373 - return hashBankService.findByName(bankId, orgId); 1374 - } 1375 - }, 1376 - directives: { freshUntilAge: 300 }, // 5 minutes cache for image banks 1377 - }); 1378 - }, 1379 - ); 1359 + bottle.factory('getImageBankEventuallyConsistent', (container) => { 1360 + const kyselyPg = container.KyselyPg; 1361 + const hashBankService = new HashBankService(kyselyPg); 1380 1362 1363 + return cached({ 1364 + async producer({ orgId, bankId }) { 1365 + // bankId could be either database ID or bank name 1366 + // Check if bankId is numeric (database ID) 1367 + const numericBankId = parseInt(bankId); 1368 + if (!isNaN(numericBankId)) { 1369 + // Get by database ID 1370 + return hashBankService.findById(numericBankId, orgId); 1371 + } else { 1372 + // Get by name 1373 + return hashBankService.findByName(bankId, orgId); 1374 + } 1375 + }, 1376 + directives: { freshUntilAge: 300 }, // 5 minutes cache for image banks 1377 + }); 1378 + }); 1381 1379 1382 1380 bottle.factory('getUserStrikeTTLInDaysEventuallyConsistent', (container) => { 1383 1381 return cached({ ··· 1622 1620 async () => { 1623 1621 if ('close' in it && typeof it.close === 'function') { 1624 1622 await it.close(); 1625 - } else if ('destroy' in it && typeof it.destroy === 'function') { 1623 + } else if ( 1624 + 'destroy' in it && 1625 + typeof it.destroy === 'function' 1626 + ) { 1626 1627 await it.destroy(); 1627 1628 } else if ('quit' in it && typeof it.quit === 'function') { 1628 1629 await it.quit(); 1629 - } else if ('flushPendingWrites' in it && typeof it.flushPendingWrites === 'function') { 1630 + } else if ( 1631 + 'flushPendingWrites' in it && 1632 + typeof it.flushPendingWrites === 'function' 1633 + ) { 1630 1634 await it.flushPendingWrites(); 1631 1635 } 1632 1636 },
+126 -66
server/package-lock.json
··· 117 117 "dotenv": "^10.0.0", 118 118 "eslint": "^9.39.4", 119 119 "eslint-import-resolver-typescript": "^3.6.0", 120 - "eslint-plugin-better-mutation": "^1.4.0", 120 + "eslint-plugin-functional": "^9.0.4", 121 121 "eslint-plugin-import": "^2.28.1", 122 122 "eslint-plugin-jsdoc": "^62.5.4", 123 123 "eslint-plugin-node": "^11.1.0", ··· 13332 13332 "node": ">= 0.10" 13333 13333 } 13334 13334 }, 13335 - "node_modules/create-eslint-index": { 13336 - "version": "1.0.0", 13337 - "resolved": "https://registry.npmjs.org/create-eslint-index/-/create-eslint-index-1.0.0.tgz", 13338 - "integrity": "sha512-nXvJjnfDytOOaPOonX0h0a1ggMoqrhdekGeZkD6hkcWYvlCWhU719tKFVh8eU04CnMwu3uwe1JjwuUF2C3k2qg==", 13339 - "dev": true, 13340 - "dependencies": { 13341 - "lodash.get": "^4.3.0" 13342 - }, 13343 - "engines": { 13344 - "node": ">=4.0.0" 13345 - } 13346 - }, 13347 13335 "node_modules/create-jest": { 13348 13336 "version": "29.7.0", 13349 13337 "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", ··· 13555 13543 "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", 13556 13544 "engines": { 13557 13545 "node": ">=0.10.0" 13546 + } 13547 + }, 13548 + "node_modules/deepmerge-ts": { 13549 + "version": "7.1.5", 13550 + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", 13551 + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", 13552 + "dev": true, 13553 + "license": "BSD-3-Clause", 13554 + "engines": { 13555 + "node": ">=16.0.0" 13558 13556 } 13559 13557 }, 13560 13558 "node_modules/define-data-property": { ··· 14071 14069 } 14072 14070 } 14073 14071 }, 14074 - "node_modules/eslint-ast-utils": { 14075 - "version": "1.1.0", 14076 - "resolved": "https://registry.npmjs.org/eslint-ast-utils/-/eslint-ast-utils-1.1.0.tgz", 14077 - "integrity": "sha512-otzzTim2/1+lVrlH19EfQQJEhVJSu0zOb9ygb3iapN6UlyaDtyRq4b5U1FuW0v1lRa9Fp/GJyHkSwm6NqABgCA==", 14078 - "dev": true, 14079 - "dependencies": { 14080 - "lodash.get": "^4.4.2", 14081 - "lodash.zip": "^4.2.0" 14082 - }, 14083 - "engines": { 14084 - "node": ">=4" 14085 - } 14086 - }, 14087 14072 "node_modules/eslint-import-resolver-node": { 14088 14073 "version": "0.3.9", 14089 14074 "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", ··· 14158 14143 "ms": "^2.1.1" 14159 14144 } 14160 14145 }, 14161 - "node_modules/eslint-plugin-better-mutation": { 14162 - "version": "1.4.0", 14163 - "resolved": "https://registry.npmjs.org/eslint-plugin-better-mutation/-/eslint-plugin-better-mutation-1.4.0.tgz", 14164 - "integrity": "sha512-Oj0C/fHKFRHtg/7rtZLD3oHk3TjaedMwQqbXQnhf81QgqAZp/IHzL9U3r4vElPlZT7h/1aY7GRsNz5XFwD+MnA==", 14165 - "dev": true, 14166 - "dependencies": { 14167 - "create-eslint-index": "~1.0.0", 14168 - "debug": "~4.3.1", 14169 - "eslint-ast-utils": "~1.1.0", 14170 - "import-modules": "~2.0.0", 14171 - "lodash": "~4.17.20" 14172 - }, 14173 - "engines": { 14174 - "node": ">=10.0.0" 14175 - }, 14176 - "peerDependencies": { 14177 - "eslint": ">=6" 14178 - } 14179 - }, 14180 14146 "node_modules/eslint-plugin-es": { 14181 14147 "version": "3.0.1", 14182 14148 "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", ··· 14196 14162 "eslint": ">=4.19.1" 14197 14163 } 14198 14164 }, 14165 + "node_modules/eslint-plugin-functional": { 14166 + "version": "9.0.4", 14167 + "resolved": "https://registry.npmjs.org/eslint-plugin-functional/-/eslint-plugin-functional-9.0.4.tgz", 14168 + "integrity": "sha512-zm4qaoqb2r50V4WXxt0Mj92buXGMECYvMxGQ6sSb+XeJ+Eec6zCHuMY2+AWK1mqiApvUz2tCtp1P3zcEPU0huw==", 14169 + "dev": true, 14170 + "funding": [ 14171 + { 14172 + "type": "ko-fi", 14173 + "url": "https://ko-fi.com/rebeccastevens" 14174 + }, 14175 + { 14176 + "type": "tidelift", 14177 + "url": "https://tidelift.com/funding/github/npm/eslint-plugin-functional" 14178 + } 14179 + ], 14180 + "license": "MIT", 14181 + "dependencies": { 14182 + "@typescript-eslint/utils": "^8.26.0", 14183 + "deepmerge-ts": "^7.1.5", 14184 + "escape-string-regexp": "^5.0.0", 14185 + "is-immutable-type": "^5.0.1", 14186 + "ts-api-utils": "^2.0.1", 14187 + "ts-declaration-location": "^1.0.6" 14188 + }, 14189 + "engines": { 14190 + "node": ">=v18.18.0" 14191 + }, 14192 + "peerDependencies": { 14193 + "eslint": "^9.0.0 || ^10.0.0", 14194 + "typescript": ">=4.7.4" 14195 + }, 14196 + "peerDependenciesMeta": { 14197 + "typescript": { 14198 + "optional": true 14199 + } 14200 + } 14201 + }, 14202 + "node_modules/eslint-plugin-functional/node_modules/escape-string-regexp": { 14203 + "version": "5.0.0", 14204 + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", 14205 + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", 14206 + "dev": true, 14207 + "license": "MIT", 14208 + "engines": { 14209 + "node": ">=12" 14210 + }, 14211 + "funding": { 14212 + "url": "https://github.com/sponsors/sindresorhus" 14213 + } 14214 + }, 14215 + "node_modules/eslint-plugin-functional/node_modules/ts-api-utils": { 14216 + "version": "2.5.0", 14217 + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", 14218 + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", 14219 + "dev": true, 14220 + "license": "MIT", 14221 + "engines": { 14222 + "node": ">=18.12" 14223 + }, 14224 + "peerDependencies": { 14225 + "typescript": ">=4.8.4" 14226 + } 14227 + }, 14199 14228 "node_modules/eslint-plugin-import": { 14200 14229 "version": "2.32.0", 14201 14230 "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", ··· 16023 16052 "url": "https://github.com/sponsors/sindresorhus" 16024 16053 } 16025 16054 }, 16026 - "node_modules/import-modules": { 16027 - "version": "2.0.0", 16028 - "resolved": "https://registry.npmjs.org/import-modules/-/import-modules-2.0.0.tgz", 16029 - "integrity": "sha512-iczM/v9drffdNnABOKwj0f9G3cFDon99VcG1mxeBsdqnbd+vnQ5c2uAiCHNQITqFTOPaEvwg3VjoWCur0uHLEw==", 16030 - "dev": true, 16031 - "engines": { 16032 - "node": ">=8" 16033 - } 16034 - }, 16035 16055 "node_modules/imurmurhash": { 16036 16056 "version": "0.1.4", 16037 16057 "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", ··· 16337 16357 "node": ">=8" 16338 16358 } 16339 16359 }, 16360 + "node_modules/is-immutable-type": { 16361 + "version": "5.0.1", 16362 + "resolved": "https://registry.npmjs.org/is-immutable-type/-/is-immutable-type-5.0.1.tgz", 16363 + "integrity": "sha512-LkHEOGVZZXxGl8vDs+10k3DvP++SEoYEAJLRk6buTFi6kD7QekThV7xHS0j6gpnUCQ0zpud/gMDGiV4dQneLTg==", 16364 + "dev": true, 16365 + "license": "BSD-3-Clause", 16366 + "dependencies": { 16367 + "@typescript-eslint/type-utils": "^8.0.0", 16368 + "ts-api-utils": "^2.0.0", 16369 + "ts-declaration-location": "^1.0.4" 16370 + }, 16371 + "peerDependencies": { 16372 + "eslint": "*", 16373 + "typescript": ">=4.7.4" 16374 + } 16375 + }, 16376 + "node_modules/is-immutable-type/node_modules/ts-api-utils": { 16377 + "version": "2.5.0", 16378 + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", 16379 + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", 16380 + "dev": true, 16381 + "license": "MIT", 16382 + "engines": { 16383 + "node": ">=18.12" 16384 + }, 16385 + "peerDependencies": { 16386 + "typescript": ">=4.8.4" 16387 + } 16388 + }, 16340 16389 "node_modules/is-map": { 16341 16390 "version": "2.0.3", 16342 16391 "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", ··· 17691 17740 "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", 17692 17741 "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" 17693 17742 }, 17694 - "node_modules/lodash.get": { 17695 - "version": "4.4.2", 17696 - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", 17697 - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", 17698 - "dev": true 17699 - }, 17700 17743 "node_modules/lodash.includes": { 17701 17744 "version": "4.3.0", 17702 17745 "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", ··· 17746 17789 "version": "4.7.0", 17747 17790 "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", 17748 17791 "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==" 17749 - }, 17750 - "node_modules/lodash.zip": { 17751 - "version": "4.2.0", 17752 - "resolved": "https://registry.npmjs.org/lodash.zip/-/lodash.zip-4.2.0.tgz", 17753 - "integrity": "sha512-C7IOaBBK/0gMORRBd8OETNx3kmOkgIWIPvyDpZSCTwUrpYmgZwJkjZeOD8ww4xbOUOs4/attY+pciKvadNfFbg==", 17754 - "dev": true 17755 17792 }, 17756 17793 "node_modules/loglevel": { 17757 17794 "version": "1.8.1", ··· 20796 20833 }, 20797 20834 "peerDependencies": { 20798 20835 "typescript": ">=4.2.0" 20836 + } 20837 + }, 20838 + "node_modules/ts-declaration-location": { 20839 + "version": "1.0.7", 20840 + "resolved": "https://registry.npmjs.org/ts-declaration-location/-/ts-declaration-location-1.0.7.tgz", 20841 + "integrity": "sha512-EDyGAwH1gO0Ausm9gV6T2nUvBgXT5kGoCMJPllOaooZ+4VvJiKBdZE7wK18N1deEowhcUptS+5GXZK8U/fvpwA==", 20842 + "dev": true, 20843 + "funding": [ 20844 + { 20845 + "type": "ko-fi", 20846 + "url": "https://ko-fi.com/rebeccastevens" 20847 + }, 20848 + { 20849 + "type": "tidelift", 20850 + "url": "https://tidelift.com/funding/github/npm/ts-declaration-location" 20851 + } 20852 + ], 20853 + "license": "BSD-3-Clause", 20854 + "dependencies": { 20855 + "picomatch": "^4.0.2" 20856 + }, 20857 + "peerDependencies": { 20858 + "typescript": ">=4.0.0" 20799 20859 } 20800 20860 }, 20801 20861 "node_modules/ts-node": {
+1 -4
server/package.json
··· 131 131 "dotenv": "^10.0.0", 132 132 "eslint": "^9.39.4", 133 133 "eslint-import-resolver-typescript": "^3.6.0", 134 - "eslint-plugin-better-mutation": "^1.4.0", 134 + "eslint-plugin-functional": "^9.0.4", 135 135 "eslint-plugin-import": "^2.28.1", 136 136 "eslint-plugin-jsdoc": "^62.5.4", 137 137 "eslint-plugin-node": "^11.1.0", ··· 162 162 "@types/restify": "npm:pino@8.6.0", 163 163 "@googlemaps/google-maps-services-js@^3.3.16": { 164 164 "retry-axios": "npm:@ethanresnick/retry-axios@2.6.1" 165 - }, 166 - "eslint-plugin-better-mutation": { 167 - "lodash": "^4.18.1" 168 165 } 169 166 } 170 167 }
+8 -5
server/routes/content/ContentRoutes.test.ts
··· 26 26 analytics: Dependencies['DataWarehouseAnalytics']; 27 27 28 28 beforeAll(async () => { 29 - // eslint-disable-next-line better-mutation/no-mutation 30 29 ({ 31 30 request, 32 31 shutdown, ··· 39 38 } = await makeMockedServer()); 40 39 41 40 const { User, Org } = models; 42 - // eslint-disable-next-line better-mutation/no-mutation 43 - ({ apiKey } = await createOrg({ Org }, ModerationConfigService, ApiKeyService, orgId)); 44 - // eslint-disable-next-line better-mutation/no-mutation 41 + 42 + ({ apiKey } = await createOrg( 43 + { Org }, 44 + ModerationConfigService, 45 + ApiKeyService, 46 + orgId, 47 + )); 48 + 45 49 contentType1 = await ModerationConfigService.createContentType(orgId, { 46 50 name: 'test', 47 51 description: faker.datatype.string(), ··· 62 66 schemaFieldRoles: {}, 63 67 }); 64 68 65 - // eslint-disable-next-line better-mutation/no-mutation 66 69 contentType2 = await ModerationConfigService.createContentType(orgId, { 67 70 name: 'tes333t', 68 71 description: faker.datatype.string(),
+8 -4
server/routes/gdpr/gdprRoutes.test.ts
··· 19 19 20 20 beforeAll(async () => { 21 21 try { 22 - // eslint-disable-next-line better-mutation/no-mutation 23 22 ({ 24 23 request, 25 24 shutdown, ··· 27 26 } = await makeMockedServer()); 28 27 29 28 const { Org } = models; 30 - // eslint-disable-next-line better-mutation/no-mutation 31 - ({ apiKey } = await createOrg({ Org }, ModerationConfigService, ApiKeyService, orgId)); 32 - // eslint-disable-next-line better-mutation/no-mutation 29 + 30 + ({ apiKey } = await createOrg( 31 + { Org }, 32 + ModerationConfigService, 33 + ApiKeyService, 34 + orgId, 35 + )); 36 + 33 37 contentType = await ModerationConfigService.createContentType(orgId, { 34 38 name: 'TestUser', 35 39 description: 'user type',
+12 -3
server/routes/index.test.ts
··· 5 5 shutdown: Awaited<ReturnType<typeof makeMockedServer>>['shutdown']; 6 6 7 7 beforeAll(async () => { 8 - // eslint-disable-next-line better-mutation/no-mutation 9 8 ({ request, shutdown } = await makeMockedServer()); 10 9 }); 11 10 ··· 51 50 // express adds automatically (to parse query params, create the request 52 51 // object, and (iiuc) normalize trailing path slashes), but before all our 53 52 // handlers. 54 - const newlyAddedErrorRoute = server._router.stack.pop(); 55 - (server._router.stack as unknown[]).splice(3, 0, newlyAddedErrorRoute); 53 + const stack = server._router.stack as unknown[]; 54 + const newlyAddedErrorRoute = stack.at(-1); 55 + if (newlyAddedErrorRoute === undefined) { 56 + throw new Error('expected route on stack'); 57 + } 58 + const withoutLast = stack.slice(0, -1); 59 + // eslint-disable-next-line functional/immutable-data -- express test-only router reordering 60 + server._router.stack = [ 61 + ...withoutLast.slice(0, 3), 62 + newlyAddedErrorRoute, 63 + ...withoutLast.slice(3), 64 + ] as typeof server._router.stack; 56 65 57 66 try { 58 67 const resp = await request.get('/api/v1/error');
-1
server/routes/index.ts
··· 19 19 routes: ControllerRouteList; 20 20 }; 21 21 22 - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions 23 22 export default { 24 23 Items: ItemRoutes, 25 24 Content: ContentRoutes,
+5 -3
server/routes/integration_logos/serveIntegrationLogo.ts
··· 18 18 makeNotFoundError('Missing integration id.', { shouldErrorSpan: true }), 19 19 ); 20 20 } 21 - const filePath = getIntegrationRegistry().getPluginLogoFilePath(integrationId); 22 - if (filePath === undefined) { // eslint-disable-line @typescript-eslint/no-unnecessary-condition -- runtime guard for missing plugin logo 21 + const filePath = 22 + getIntegrationRegistry().getPluginLogoFilePath(integrationId); 23 + if (filePath === undefined) { 23 24 return next( 24 25 makeNotFoundError('Integration logo not found.', { 25 26 shouldErrorSpan: true, ··· 29 30 // Path was validated at plugin load (under package root); safe to send. 30 31 res.setHeader('Cache-Control', 'public, max-age=86400'); 31 32 res.sendFile(filePath, (err) => { 32 - if (err != null && !res.headersSent) { // eslint-disable-line @typescript-eslint/no-unnecessary-condition -- sendFile callback err is Error | null per types 33 + if (err != null && !res.headersSent) { 34 + 33 35 next(err); 34 36 } 35 37 });
+7 -3
server/routes/integration_logos/serveIntegrationLogoWithBackground.ts
··· 17 17 makeNotFoundError('Missing integration id.', { shouldErrorSpan: true }), 18 18 ); 19 19 } 20 - const filePath = getIntegrationRegistry().getPluginLogoWithBackgroundFilePath(integrationId); 21 - if (filePath === undefined) { // eslint-disable-line @typescript-eslint/no-unnecessary-condition -- runtime guard for missing plugin logo 20 + const filePath = 21 + getIntegrationRegistry().getPluginLogoWithBackgroundFilePath( 22 + integrationId, 23 + ); 24 + if (filePath === undefined) { 22 25 return next( 23 26 makeNotFoundError('Integration logo (with-background) not found.', { 24 27 shouldErrorSpan: true, ··· 27 30 } 28 31 res.setHeader('Cache-Control', 'public, max-age=86400'); 29 32 res.sendFile(filePath, (err) => { 30 - if (err != null && !res.headersSent) { // eslint-disable-line @typescript-eslint/no-unnecessary-condition -- sendFile callback err is Error | null per types 33 + if (err != null && !res.headersSent) { 34 + 31 35 next(err); 32 36 } 33 37 });
+8 -4
server/routes/items/ItemRoutes.test.ts
··· 21 21 analytics: Dependencies['DataWarehouseAnalytics']; 22 22 23 23 beforeAll(async () => { 24 - // eslint-disable-next-line better-mutation/no-mutation 25 24 ({ 26 25 request, 27 26 shutdown, ··· 34 33 } = await makeMockedServer()); 35 34 36 35 const { User, Org } = models; 37 - // eslint-disable-next-line better-mutation/no-mutation 38 - ({ apiKey } = await createOrg({ Org }, ModerationConfigService, ApiKeyService, orgId)); 39 - // eslint-disable-next-line better-mutation/no-mutation 36 + 37 + ({ apiKey } = await createOrg( 38 + { Org }, 39 + ModerationConfigService, 40 + ApiKeyService, 41 + orgId, 42 + )); 43 + 40 44 contentType = await ModerationConfigService.createContentType(orgId, { 41 45 name: 'test', 42 46 description: faker.datatype.string(),
+7 -3
server/routes/policies/PoliciesRoutes.test.ts
··· 18 18 ApiKeyService: Dependencies['ApiKeyService']; 19 19 20 20 beforeAll(async () => { 21 - // eslint-disable-next-line better-mutation/no-mutation 22 21 ({ 23 22 request, 24 23 shutdown, ··· 26 25 } = await makeMockedServer()); 27 26 28 27 const { Org } = models; 29 - // eslint-disable-next-line better-mutation/no-mutation 30 - ({ apiKey } = await createOrg({ Org }, ModerationConfigService, ApiKeyService, orgId)); 28 + 29 + ({ apiKey } = await createOrg( 30 + { Org }, 31 + ModerationConfigService, 32 + ApiKeyService, 33 + orgId, 34 + )); 31 35 }); 32 36 33 37 afterAll(async () => {
+3 -6
server/routes/reporting/ReportingRoutes.test.ts
··· 25 25 >; 26 26 27 27 beforeAll(async () => { 28 - // eslint-disable-next-line better-mutation/no-mutation 29 28 ({ deps, request, shutdown } = await makeMockedServer()); 30 29 31 - // eslint-disable-next-line better-mutation/no-mutation 32 30 models = deps.Sequelize; 33 31 34 - // eslint-disable-next-line better-mutation/no-mutation 35 32 ({ apiKey } = await createOrg( 36 33 models, 37 34 deps.ModerationConfigService, ··· 103 100 schemaFieldRoles: {}, 104 101 }, 105 102 ); 106 - // eslint-disable-next-line better-mutation/no-mutation 103 + 107 104 contentTypeId = contentType.id; 108 - // eslint-disable-next-line better-mutation/no-mutation 105 + 109 106 userTypeId = userType.id; 110 - // eslint-disable-next-line better-mutation/no-mutation 107 + 111 108 threadTypeId = threadType.id; 112 109 113 110 await models.User.create({
+2 -3
server/routes/reporting/submitAppeal.ts
··· 8 8 rawItemSubmissionToItemSubmission, 9 9 type ItemSubmission, 10 10 } from '../../services/itemProcessingService/index.js'; 11 + import { hasOrgId } from '../../utils/apiKeyMiddleware.js'; 11 12 import { 12 13 fromCorrelationId, 13 14 toCorrelationId, ··· 19 20 import { withRetries } from '../../utils/misc.js'; 20 21 import { type RequestHandlerWithBodies } from '../../utils/route-helpers.js'; 21 22 import { isValidDate } from '../../utils/time.js'; 22 - import { hasOrgId } from '../../utils/apiKeyMiddleware.js'; 23 23 import { 24 24 type AppealItemInput, 25 25 type AppealItemOutput, ··· 61 61 }), 62 62 ); 63 63 } 64 - 64 + 65 65 const { orgId } = req; 66 66 67 67 const toItemSubmission = rawItemSubmissionToItemSubmission.bind( ··· 137 137 138 138 if ( 139 139 submittedItemIsInvalid || 140 - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing 141 140 threadOrAdditionalItemsHadInvalidOrIllegalItems || 142 141 hasAdditionalItemsOnThreadSubmission || 143 142 isInvalidAppealedAtDate ||
+37 -27
server/routes/reporting/submitReport.ts
··· 8 8 rawItemSubmissionToItemSubmission, 9 9 type ItemSubmission, 10 10 } from '../../services/itemProcessingService/index.js'; 11 + import { hasOrgId } from '../../utils/apiKeyMiddleware.js'; 11 12 import { 12 13 fromCorrelationId, 13 14 toCorrelationId, ··· 16 17 makeBadRequestError, 17 18 makeInternalServerError, 18 19 } from '../../utils/errors.js'; 19 - import { hasOrgId } from '../../utils/apiKeyMiddleware.js'; 20 20 import { withRetries } from '../../utils/misc.js'; 21 21 import { type RequestHandlerWithBodies } from '../../utils/route-helpers.js'; 22 22 import { isValidDate } from '../../utils/time.js'; ··· 67 67 }), 68 68 ); 69 69 } 70 - 70 + 71 71 const { orgId } = req; 72 72 73 73 const toItemSubmission = rawItemSubmissionToItemSubmission.bind( ··· 102 102 ) { 103 103 try { 104 104 const images = reportedItem.data.images as string[]; 105 - 105 + 106 106 // Get all hash banks for this org once 107 107 const allBanks = await HMAHashBankService.listBanks(orgId); 108 - const allBankNames = allBanks.map(bank => bank.hma_name); 109 - 108 + const allBankNames = allBanks.map((bank) => bank.hma_name); 109 + 110 110 const imageHashes = await Promise.all( 111 111 images.map(async (url) => { 112 112 if (typeof url === 'string' && url) { ··· 120 120 }, 121 121 async () => { 122 122 return HMAHashBankService.hashContentFromUrl(url); 123 - } 123 + }, 124 124 ); 125 125 const hashes = await hmaHashWithRetries(); 126 - 126 + 127 127 // Check which banks match this image 128 128 const matchedBankNames: string[] = []; 129 - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 130 - if (hashes && Object.keys(hashes).length > 0 && allBankNames.length > 0) { 129 + 130 + if ( 131 + hashes && 132 + Object.keys(hashes).length > 0 && 133 + allBankNames.length > 0 134 + ) { 131 135 const matchResults = await Promise.all( 132 136 Object.entries(hashes).map(async ([signalType, hash]) => 133 - HMAHashBankService.checkImageMatchWithDetails(allBankNames, signalType, hash) 134 - ) 137 + HMAHashBankService.checkImageMatchWithDetails( 138 + allBankNames, 139 + signalType, 140 + hash, 141 + ), 142 + ), 135 143 ); 136 - 144 + 137 145 // Collect all matched banks 138 146 const allMatchedHmaBanks = new Set<string>(); 139 - matchResults.forEach(result => { 140 - result.matchedBanks.forEach(bank => allMatchedHmaBanks.add(bank)); 147 + matchResults.forEach((result) => { 148 + result.matchedBanks.forEach((bank) => 149 + allMatchedHmaBanks.add(bank), 150 + ); 141 151 }); 142 - 152 + 143 153 // Map HMA bank names to user-friendly names 144 - allMatchedHmaBanks.forEach(hmaName => { 145 - const bank = allBanks.find(b => b.hma_name === hmaName); 154 + allMatchedHmaBanks.forEach((hmaName) => { 155 + const bank = allBanks.find((b) => b.hma_name === hmaName); 146 156 if (bank) { 147 157 matchedBankNames.push(bank.name); 148 158 } 149 159 }); 150 160 } 151 - 161 + 152 162 return { 153 163 url, 154 164 hashes, 155 - matchedBanks: matchedBankNames.length > 0 ? matchedBankNames : undefined 165 + matchedBanks: 166 + matchedBankNames.length > 0 167 + ? matchedBankNames 168 + : undefined, 156 169 }; 157 170 } catch (e) { 158 171 return { 159 172 url, 160 - hashes: {} 173 + hashes: {}, 161 174 }; 162 175 } 163 176 } 164 177 return null; 165 - }) 178 + }), 166 179 ); 167 180 // Attach the hashes array to the item submission data 168 181 // eslint-disable-next-line @typescript-eslint/no-explicit-any 169 - (reportedItemSubmission.itemSubmission.data as any).images = imageHashes; 182 + (reportedItemSubmission.itemSubmission.data as any).images = 183 + imageHashes; 170 184 } catch (error) { 171 185 // eslint-disable-next-line no-console 172 186 console.error('Failed to get HMA hashes for images:', error); ··· 213 227 // analysis. 214 228 const threadOrAdditionalItemsHadInvalidOrIllegalItems = 215 229 (reportedThreadSubmission && 216 - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing 217 230 !isAllValidContentItems(reportedThreadSubmission)) || 218 231 (additionalItemSubmissions && 219 232 !isAllValidContentItems(additionalItemSubmissions)); ··· 223 236 ); 224 237 if ( 225 238 submittedItemIsInvalid || 226 - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing 227 239 threadOrAdditionalItemsHadInvalidOrIllegalItems || 228 240 hasAdditionalItemsOnThreadSubmission || 229 241 isInvalidReportedAtDate ··· 371 383 ? [reportedForReason.policyId] 372 384 : [], 373 385 }; 374 - if ( 375 - req.body.reportedForReason?.csam 376 - ) { 386 + if (req.body.reportedForReason?.csam) { 377 387 await NcmecService.enqueueForHumanReviewIfApplicable({ 378 388 ...commonEnqueueInput, 379 389 item,
+110 -108
server/rule_engine/ActionPublisher.test.ts
··· 6 6 * on each iteration instead of just the current one. 7 7 */ 8 8 9 - import getBottle, { type Dependencies } from "../iocContainer/index.js"; 10 - import { ActionType } from "../services/moderationConfigService/index.js"; 11 - import { type CorrelationId } from "../utils/correlationIds.js"; 12 - import { type ActionPublisher } from "./ActionPublisher.js"; 13 - import { RuleEnvironment } from "./RuleEngine.js"; 9 + import getBottle, { type Dependencies } from '../iocContainer/index.js'; 10 + import { ActionType } from '../services/moderationConfigService/index.js'; 11 + import { type CorrelationId } from '../utils/correlationIds.js'; 12 + import { type ActionPublisher } from './ActionPublisher.js'; 13 + import { RuleEnvironment } from './RuleEngine.js'; 14 14 15 - describe("ActionPublisher", () => { 16 - let container: Dependencies; 17 - let actionPublisher: ActionPublisher; 15 + describe('ActionPublisher', () => { 16 + let container: Dependencies; 17 + let actionPublisher: ActionPublisher; 18 18 19 - beforeAll(async () => { 20 - /* eslint-disable better-mutation/no-mutation */ 21 - ({ container } = await getBottle()); 22 - actionPublisher = container.ActionPublisher; 23 - /* eslint-enable better-mutation/no-mutation */ 24 - }); 19 + beforeAll(async () => { 20 + ({ container } = await getBottle()); 21 + actionPublisher = container.ActionPublisher; 22 + }); 25 23 26 - afterAll(async () => { 27 - await container.closeSharedResourcesForShutdown(); 28 - }); 24 + afterAll(async () => { 25 + await container.closeSharedResourcesForShutdown(); 26 + }); 29 27 30 - describe("publishActions", () => { 31 - it("should log each action execution exactly once (not N² times)", async () => { 32 - const logSpy = jest.spyOn( 33 - container.ActionExecutionLogger, 34 - "logActionExecutions", 35 - ); 28 + describe('publishActions', () => { 29 + it('should log each action execution exactly once (not N² times)', async () => { 30 + const logSpy = jest.spyOn( 31 + container.ActionExecutionLogger, 32 + 'logActionExecutions', 33 + ); 36 34 37 - // Use 2 actions to catch the N² bug 38 - // With the bug: 2 actions → 2² = 4 log entries 39 - // With the fix: 2 actions → 2 log entries 40 - const triggeredActions = [ 41 - { 42 - action: { 43 - id: "action-1", 44 - orgId: "org-123", 45 - name: "Action 1", 46 - applyUserStrikes: false, 47 - actionType: ActionType.CUSTOM_ACTION, 48 - callbackUrl: "https://example.com/action1", 49 - callbackUrlHeaders: null, 50 - callbackUrlBody: null, 51 - customMrtApiParams: null, 52 - }, 53 - policies: [ 54 - { 55 - id: "policy-1", 56 - name: "Policy 1", 57 - penalty: "NONE" as const, 58 - userStrikeCount: 0, 59 - }, 60 - ], 61 - matchingRules: [ 62 - { 63 - id: "rule-1", 64 - name: "Rule 1", 65 - version: "1", 66 - tags: [], 67 - policies: [], 68 - }, 69 - ], 70 - ruleEnvironment: RuleEnvironment.LIVE, 71 - }, 72 - { 73 - action: { 74 - id: "action-2", 75 - orgId: "org-123", 76 - name: "Action 2", 77 - applyUserStrikes: false, 78 - actionType: ActionType.CUSTOM_ACTION, 79 - callbackUrl: "https://example.com/action2", 80 - callbackUrlHeaders: null, 81 - callbackUrlBody: null, 82 - customMrtApiParams: null, 83 - }, 84 - policies: [ 85 - { 86 - id: "policy-2", 87 - name: "Policy 2", 88 - penalty: "NONE" as const, 89 - userStrikeCount: 0, 90 - }, 91 - ], 92 - matchingRules: [ 93 - { 94 - id: "rule-2", 95 - name: "Rule 2", 96 - version: "1", 97 - tags: [], 98 - policies: [], 99 - }, 100 - ], 101 - ruleEnvironment: RuleEnvironment.LIVE, 102 - }, 103 - ]; 35 + // Use 2 actions to catch the N² bug 36 + // With the bug: 2 actions → 2² = 4 log entries 37 + // With the fix: 2 actions → 2 log entries 38 + const triggeredActions = [ 39 + { 40 + action: { 41 + id: 'action-1', 42 + orgId: 'org-123', 43 + name: 'Action 1', 44 + applyUserStrikes: false, 45 + actionType: ActionType.CUSTOM_ACTION, 46 + callbackUrl: 'https://example.com/action1', 47 + callbackUrlHeaders: null, 48 + callbackUrlBody: null, 49 + customMrtApiParams: null, 50 + }, 51 + policies: [ 52 + { 53 + id: 'policy-1', 54 + name: 'Policy 1', 55 + penalty: 'NONE' as const, 56 + userStrikeCount: 0, 57 + }, 58 + ], 59 + matchingRules: [ 60 + { 61 + id: 'rule-1', 62 + name: 'Rule 1', 63 + version: '1', 64 + tags: [], 65 + policies: [], 66 + }, 67 + ], 68 + ruleEnvironment: RuleEnvironment.LIVE, 69 + }, 70 + { 71 + action: { 72 + id: 'action-2', 73 + orgId: 'org-123', 74 + name: 'Action 2', 75 + applyUserStrikes: false, 76 + actionType: ActionType.CUSTOM_ACTION, 77 + callbackUrl: 'https://example.com/action2', 78 + callbackUrlHeaders: null, 79 + callbackUrlBody: null, 80 + customMrtApiParams: null, 81 + }, 82 + policies: [ 83 + { 84 + id: 'policy-2', 85 + name: 'Policy 2', 86 + penalty: 'NONE' as const, 87 + userStrikeCount: 0, 88 + }, 89 + ], 90 + matchingRules: [ 91 + { 92 + id: 'rule-2', 93 + name: 'Rule 2', 94 + version: '1', 95 + tags: [], 96 + policies: [], 97 + }, 98 + ], 99 + ruleEnvironment: RuleEnvironment.LIVE, 100 + }, 101 + ]; 104 102 105 - const executionContext = { 106 - orgId: "org-123", 107 - correlationId: "post-content:abc123" as CorrelationId<"post-content">, 108 - targetItem: { 109 - itemId: "item-123", 110 - itemType: { id: "type-123", kind: "CONTENT" as const, name: "Social Post" }, 111 - }, 112 - }; 103 + const executionContext = { 104 + orgId: 'org-123', 105 + correlationId: 'post-content:abc123' as CorrelationId<'post-content'>, 106 + targetItem: { 107 + itemId: 'item-123', 108 + itemType: { 109 + id: 'type-123', 110 + kind: 'CONTENT' as const, 111 + name: 'Social Post', 112 + }, 113 + }, 114 + }; 113 115 114 - await actionPublisher.publishActions(triggeredActions, executionContext); 116 + await actionPublisher.publishActions(triggeredActions, executionContext); 115 117 116 - // With the fix: called 2 times (once per action) 117 - expect(logSpy).toHaveBeenCalledTimes(2); 118 + // With the fix: called 2 times (once per action) 119 + expect(logSpy).toHaveBeenCalledTimes(2); 118 120 119 - // Each call should log exactly one execution 120 - logSpy.mock.calls.forEach((call) => { 121 - expect(call[0].executions).toHaveLength(1); 122 - }); 121 + // Each call should log exactly one execution 122 + logSpy.mock.calls.forEach((call) => { 123 + expect(call[0].executions).toHaveLength(1); 124 + }); 123 125 124 - logSpy.mockRestore(); 125 - }); 126 - }); 126 + logSpy.mockRestore(); 127 + }); 128 + }); 127 129 });
+13 -9
server/rule_engine/ActionPublisher.ts
··· 6 6 import { type Dependencies } from '../iocContainer/index.js'; 7 7 import { inject } from '../iocContainer/utils.js'; 8 8 import { 9 + type ActionExecutionCorrelationId, 10 + type ActionExecutionSourceType, 11 + type MatchingRule, 12 + type Policy, 13 + } from '../services/analyticsLoggers/index.js'; 14 + import { 9 15 getFieldValueForRole, 10 16 itemSubmissionToItemSubmissionWithTypeIdentifier, 11 17 type ItemSubmission, ··· 15 21 type Action, 16 22 type ItemType, 17 23 } from '../services/moderationConfigService/index.js'; 18 - import { 19 - type ActionExecutionCorrelationId, 20 - type ActionExecutionSourceType, 21 - type MatchingRule, 22 - type Policy, 23 - } from '../services/analyticsLoggers/index.js'; 24 24 import { asyncIterableToArray } from '../utils/collections.js'; 25 25 import { getSourceType, type CorrelationId } from '../utils/correlationIds.js'; 26 26 import { jsonStringify } from '../utils/encoding.js'; ··· 229 229 matchingRules, 230 230 ruleEnvironment, 231 231 policies: policies.map((policy) => 232 - safePick(policy, ['id', 'name', 'userStrikeCount', 'penalty']), 232 + safePick(policy, [ 233 + 'id', 234 + 'name', 235 + 'userStrikeCount', 236 + 'penalty', 237 + ]), 233 238 ), 234 239 orgId, 235 240 targetItem, ··· 305 310 ...customMrtApiParamDecisionPayload, 306 311 }; 307 312 308 - 309 313 const body = { 310 314 item: { 311 315 id: targetItem.itemId, ··· 329 333 // could provide that would have security implications when blindly fed 330 334 // in here -- like something that would somehow lead fetch to do something 331 335 // unexpected. 332 - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion 336 + 333 337 ...((customHeaders as JsonObject | undefined) ?? undefined), 334 338 // Put this header last so customHeaders can't override it, which I 335 339 // think makes sense, since there's no way for users to effect the
+3 -9
server/services/actionStatisticsService/actionStatisticsService.ts
··· 1 - /* eslint-disable max-lines */ 2 1 import { inject } from '../../iocContainer/utils.js'; 3 2 import { 4 - type IActionStatisticsAdapter, 5 3 type ActionCountsInput, 4 + type IActionStatisticsAdapter, 6 5 } from '../../plugins/warehouse/queries/IActionStatisticsAdapter.js'; 7 6 import { YEAR_MS } from '../../utils/time.js'; 8 7 9 8 class ActionStatisticsService { 10 - constructor( 11 - private readonly adapter: IActionStatisticsAdapter, 12 - ) {} 9 + constructor(private readonly adapter: IActionStatisticsAdapter) {} 13 10 14 11 /** 15 12 * Returns the total number of content submissions actioned on each day, ··· 33 30 orgId: string, 34 31 startAt: Date = new Date(Date.now() - YEAR_MS), 35 32 ) { 36 - return this.adapter.getActionedSubmissionCountsByTagByDay( 37 - orgId, 38 - startAt, 39 - ); 33 + return this.adapter.getActionedSubmissionCountsByTagByDay(orgId, startAt); 40 34 } 41 35 42 36 async getActionedSubmissionCountsByPolicyByDay(
+1 -1
server/services/analyticsLoggers/ContentApiLogger.ts
··· 75 75 org_id: data.orgId, 76 76 request_id: fromCorrelationId(data.requestId), 77 77 submission_id: itemSubmission.submissionId, 78 - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 78 + 79 79 ...(failureReason != null 80 80 ? { 81 81 event: 'REQUEST_FAILED' as const,
+1 -1
server/services/analyticsLoggers/ItemModelScoreLogger.ts
··· 77 77 model_score: data.model.score, 78 78 } 79 79 : {}), 80 - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 80 + 81 81 ...(failureReason != null 82 82 ? { 83 83 event: 'REQUEST_FAILED' as const,
+1 -2
server/services/itemInvestigationService/itemInvestigationService.test.ts
··· 19 19 beforeAll(async () => { 20 20 // The mutation should be ok here since this is initial setup in a 21 21 // beforeAll; it doesn't involve reset state for each test in the suite 22 - /* eslint-disable better-mutation/no-mutation */ 22 + 23 23 ({ container } = await getBottle()); 24 24 itemInvestigationService = container.ItemInvestigationService; 25 - /* eslint-enable better-mutation/no-mutation */ 26 25 }); 27 26 afterAll(async () => { 28 27 await container.closeSharedResourcesForShutdown();
+1 -1
server/services/itemProcessingService/extractItemDataValues.ts
··· 56 56 // because the type that TS infers when trying to generate a single, 57 57 // callable signature for `getValues` is not correct (and has never as 58 58 // its argument types). 59 - fieldTypeHandlers[type as FieldType] 59 + fieldTypeHandlers[type] 60 60 .getValues(data[name] as never, container as never) 61 61 .map( 62 62 (value) =>
-1
server/services/itemProcessingService/fieldTypeHandlers.test.ts
··· 28 28 ) as fc.Arbitrary<Field<ContainerType>>; 29 29 30 30 fc.assert( 31 - // eslint-disable-next-line no-loop-func 32 31 fc.property(dummyContainerFieldArb, (containerField) => { 33 32 expect( 34 33 (handlers as (typeof fieldTypeHandlers)[ContainerType]).coerce(
+1 -2
server/services/manualReviewToolService/manualReviewToolService.test.ts
··· 19 19 beforeAll(async () => { 20 20 // The mutation should be ok here since this is initial setup in a 21 21 // beforeAll; it doesn't involve reset state for each test in the suite 22 - /* eslint-disable better-mutation/no-mutation */ 22 + 23 23 ({ container } = await getBottle()); 24 24 mrtService = container.ManualReviewToolService; 25 - /* eslint-enable better-mutation/no-mutation */ 26 25 }); 27 26 28 27 afterAll(async () => {
+19 -18
server/services/manualReviewToolService/manualReviewToolService.ts
··· 1 1 /* eslint-disable max-lines */ 2 2 3 3 import { SpanStatusCode } from '@opentelemetry/api'; 4 - import { type ConsumerDirectives } from '../../lib/cache/index.js'; 5 4 import { type ItemIdentifier } from '@roostorg/types'; 6 5 import { type Kysely } from 'kysely'; 7 6 import _ from 'lodash'; 8 7 import { type Opaque } from 'type-fest'; 9 8 10 9 import { type Dependencies } from '../../iocContainer/index.js'; 10 + import { type ConsumerDirectives } from '../../lib/cache/index.js'; 11 11 import { 12 12 type Invoker, 13 13 type UserPermission, ··· 461 461 }, 462 462 }); 463 463 464 - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 465 464 if (!job) { 466 465 // this means that we tried to update a job that was deleted 467 466 // between when we looked up the existing job and did the update. ··· 684 683 if (newJob.kind === 'DEFAULT' && existingJob.kind === 'DEFAULT') { 685 684 // Merge itemThreadContentItems (conversation context) 686 685 const mergedThreadItems = 687 - ('itemThreadContentItems' in newJob || 'itemThreadContentItems' in existingJob) 686 + 'itemThreadContentItems' in newJob || 687 + 'itemThreadContentItems' in existingJob 688 688 ? _.uniqBy( 689 689 [ 690 690 ...('itemThreadContentItems' in newJob 691 - ? (newJob.itemThreadContentItems ?? []) 691 + ? newJob.itemThreadContentItems ?? [] 692 692 : []), 693 693 ...('itemThreadContentItems' in existingJob 694 - ? (existingJob.itemThreadContentItems ?? []) 694 + ? existingJob.itemThreadContentItems ?? [] 695 695 : []), 696 696 ], 697 697 (it) => jsonStringify([it.itemId, it.itemTypeIdentifier.id]), ··· 700 700 701 701 // Merge reportedItems (list of items reported by users) 702 702 const mergedReportedItems = 703 - ('reportedItems' in newJob || 'reportedItems' in existingJob) 703 + 'reportedItems' in newJob || 'reportedItems' in existingJob 704 704 ? _.uniqBy( 705 705 [ 706 - ...('reportedItems' in newJob ? (newJob.reportedItems ?? []) : []), 706 + ...('reportedItems' in newJob 707 + ? newJob.reportedItems ?? [] 708 + : []), 707 709 ...('reportedItems' in existingJob 708 - ? (existingJob.reportedItems ?? []) 710 + ? existingJob.reportedItems ?? [] 709 711 : []), 710 712 ], 711 713 (it) => jsonStringify([it.id, it.typeId]), ··· 963 965 return this.queueOps.getPendingJobCount(opts); 964 966 } 965 967 966 - async getTotalPendingJobCountForQueues( 967 - orgId: string, 968 - queueIds: string[], 969 - ) { 968 + async getTotalPendingJobCountForQueues(orgId: string, queueIds: string[]) { 970 969 return this.queueOps.getTotalPendingJobCountForQueues(orgId, queueIds); 971 970 } 972 971 ··· 1252 1251 }) { 1253 1252 // As releaseJobLock is a public method, we assume the passed in jobId is an 1254 1253 // external id (which are the only kind that should leave the mrt service). 1255 - await this.queueOps.releaseJobLock(opts as { 1256 - orgId: string; 1257 - queueId: string; 1258 - jobId: JobId; 1259 - lockToken: string; 1260 - }); 1254 + await this.queueOps.releaseJobLock( 1255 + opts as { 1256 + orgId: string; 1257 + queueId: string; 1258 + jobId: JobId; 1259 + lockToken: string; 1260 + }, 1261 + ); 1261 1262 } 1262 1263 1263 1264 async close() {
+1 -1
server/services/manualReviewToolService/modules/JobDecisioning.ts
··· 167 167 // if there's a decision to discriminate between these cases, but that's 168 168 // overkill. (Because we're not doing that, the NoJobWithIdInQueueError 169 169 // error case is unused currently/for now.) 170 - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion 170 + 171 171 if ((job as typeof job | undefined) == null) { 172 172 throw makeJobHasAlreadyBeenSubmittedError({ 173 173 detail: `Job ${jobId} has already been acted on (or this job never existed).`,
+3 -4
server/services/manualReviewToolService/modules/JobEnrichment.ts
··· 1 1 import { type ItemIdentifier } from '@roostorg/types'; 2 2 3 + import { assertUnreachable } from '../../../utils/misc.js'; 4 + import { type Satisfies } from '../../../utils/typescript-types.js'; 3 5 import { type ActionExecutionCorrelationId } from '../../analyticsLoggers/ActionExecutionLogger.js'; 4 6 import { type RuleExecutionCorrelationId } from '../../analyticsLoggers/ruleExecutionLoggingUtils.js'; 5 - import { assertUnreachable } from '../../../utils/misc.js'; 6 - import { type Satisfies } from '../../../utils/typescript-types.js'; 7 7 import { 8 8 getFieldValueForRole, 9 9 getFieldValueOrValues, ··· 171 171 return commonPayload; 172 172 } 173 173 174 - // eslint-disable-next-line complexity 175 174 async #genericEnrichJobPayload( 176 175 input: ManualReviewJobInput, 177 176 type: ItemType, ··· 299 298 }, 300 299 }; 301 300 } 302 - } 301 + }
+9 -9
server/services/manualReviewToolService/modules/JobRouting.test.ts
··· 1 1 /* eslint-disable max-lines */ 2 - /* eslint-disable better-mutation/no-mutation */ 3 2 import { ScalarTypes } from '@roostorg/types'; 4 3 import { uid } from 'uid'; 5 4 ··· 444 443 orgId: org.id, 445 444 queueId: defaultQueue.id, 446 445 }); 447 - const initialAnother = 448 - await manualReviewToolService.getPendingJobCount({ 449 - orgId: org.id, 450 - queueId: anotherQueue.id, 451 - }); 446 + const initialAnother = await manualReviewToolService.getPendingJobCount({ 447 + orgId: org.id, 448 + queueId: anotherQueue.id, 449 + }); 452 450 453 451 const normalizedDataOrError = toNormalizedItemDataOrErrors( 454 452 [itemType.id], ··· 497 495 anotherQueue.id, 498 496 ); 499 497 500 - const defaultQueueCount = await manualReviewToolService.getPendingJobCount( 501 - { orgId: org.id, queueId: defaultQueue.id }, 502 - ); 498 + const defaultQueueCount = 499 + await manualReviewToolService.getPendingJobCount({ 500 + orgId: org.id, 501 + queueId: defaultQueue.id, 502 + }); 503 503 const anotherQueueCount = 504 504 await manualReviewToolService.getPendingJobCount({ 505 505 orgId: org.id,
+16 -16
server/services/moderationConfigService/moderationConfigService.test.ts
··· 34 34 // overkill), we have to track entities added in each write test, by adding 35 35 // them to the variables below, so that we can assert on the results when 36 36 // reading. 37 + let allCreatedItemTypes = [] as ItemType[]; 37 38 const createdItemTypes = { 38 - ALL: [] as ItemType[], 39 + get ALL() { 40 + return allCreatedItemTypes; 41 + }, 39 42 get USER() { 40 - return this.ALL.filter((it) => it.kind === 'USER'); 43 + return allCreatedItemTypes.filter((it) => it.kind === 'USER'); 41 44 }, 42 45 get CONTENT() { 43 - return this.ALL.filter((it) => it.kind === 'CONTENT'); 46 + return allCreatedItemTypes.filter((it) => it.kind === 'CONTENT'); 44 47 }, 45 48 get THREAD() { 46 - return this.ALL.filter((it) => it.kind === 'THREAD'); 49 + return allCreatedItemTypes.filter((it) => it.kind === 'THREAD'); 47 50 }, 48 51 }; 49 52 50 - const createdActions = [] as Action[]; 53 + let createdActions = [] as Action[]; 51 54 52 55 const createdPolicies = [ 53 56 { ··· 81 84 // underlying db tables, which makes the tests more brittle/harder to maintain 82 85 // than I'd like if the service is refactored. 83 86 beforeAll(async () => { 84 - // eslint-disable-next-line better-mutation/no-mutation 85 87 container = (await getBottle()).container; 86 88 87 89 // An instance of kysely that will throw if any queries are run through it; ··· 97 99 // In order to test that the correct db is queried (i.e., replicas vs the 98 100 // primary), we'll just use different instances of the service, where each 99 101 // only has access to the db we expect to be hit. 100 - // eslint-disable-next-line better-mutation/no-mutation 102 + 101 103 sutWithPrimary = new ModerationConfigService( 102 104 container.KyselyPg, 103 105 kyselyShouldBeUnused, 104 106 async () => {}, 105 107 ); 106 108 107 - // eslint-disable-next-line better-mutation/no-mutation 108 109 sutWithReadReplica = new ModerationConfigService( 109 110 kyselyShouldBeUnused, 110 111 container.KyselyPgReadReplica, ··· 118 119 dummyOrgId, 119 120 ); 120 121 121 - // eslint-disable-next-line better-mutation/no-mutation 122 122 defaultUserItemType = createOrgResult.defaultUserItemType; 123 - createdItemTypes.ALL.push(defaultUserItemType); 123 + allCreatedItemTypes = [...allCreatedItemTypes, defaultUserItemType]; 124 124 }); 125 125 126 126 afterAll(async () => { ··· 228 228 ); 229 229 expect(saved.orgId).toBe(dummyOrgId); 230 230 expect(saved).toEqual(fetched); 231 - createdItemTypes.ALL.push(saved); 231 + allCreatedItemTypes = [...allCreatedItemTypes, saved]; 232 232 }); 233 233 }); 234 234 ··· 278 278 ); 279 279 expect(saved.orgId).toBe(dummyOrgId); 280 280 expect(saved).toEqual(fetched); 281 - createdItemTypes.ALL.push(saved); 281 + allCreatedItemTypes = [...allCreatedItemTypes, saved]; 282 282 }); 283 283 }); 284 284 ··· 330 330 ); 331 331 expect(saved.orgId).toBe(dummyOrgId); 332 332 expect(saved).toEqual(fetched); 333 - createdItemTypes.ALL.push(saved); 333 + allCreatedItemTypes = [...allCreatedItemTypes, saved]; 334 334 }); 335 335 }); 336 336 }); ··· 457 457 ); 458 458 expect(saved.orgId).toBe(dummyOrgId); 459 459 expect(saved).toEqual(fetched); 460 - createdActions.push(saved); 460 + createdActions = [...createdActions, saved]; 461 461 }); 462 462 }); 463 463 }); ··· 669 669 }); 670 670 }); 671 671 describe('TextBank-returning methods', () => { 672 - const createdTextBanks = [] as { 672 + let createdTextBanks = [] as { 673 673 id: string; 674 674 orgId: string; 675 675 name: string; ··· 706 706 ); 707 707 708 708 expect(textBank.orgId).toBe(dummyOrgId); 709 - createdTextBanks.push(textBank); 709 + createdTextBanks = [...createdTextBanks, textBank]; 710 710 }); 711 711 }); 712 712 });
+1 -3
server/services/moderationConfigService/moderationConfigService.ts
··· 1 - /* eslint-disable max-lines */ 2 - import { type ConsumerDirectives } from '../../lib/cache/index.js'; 3 1 import { type Kysely } from 'kysely'; 4 2 import _ from 'lodash'; 5 3 import { type JsonObject, type ReadonlyDeep } from 'type-fest'; 6 4 7 - // eslint-disable-next-line import/no-restricted-paths 5 + import { type ConsumerDirectives } from '../../lib/cache/index.js'; 8 6 import type { Invoker } from '../../models/types/permissioning.js'; 9 7 import { 10 8 CoopError,
+49 -40
server/services/ncmecService/ncmecEnqueueToMrt.ts
··· 7 7 import { match } from 'ts-pattern'; 8 8 9 9 import { type Dependencies } from '../../iocContainer/index.js'; 10 - import { type ActionExecutionCorrelationId } from '../analyticsLoggers/ActionExecutionLogger.js'; 11 - import { type RuleExecutionCorrelationId } from '../analyticsLoggers/ruleExecutionLoggingUtils.js'; 12 10 import { asyncIterableToArray } from '../../utils/collections.js'; 13 11 import { jsonStringify } from '../../utils/encoding.js'; 14 12 import { __throw, safePick, withRetries } from '../../utils/misc.js'; 15 13 import { instantiateOpaqueType } from '../../utils/typescript-types.js'; 14 + import { type ActionExecutionCorrelationId } from '../analyticsLoggers/ActionExecutionLogger.js'; 15 + import { type RuleExecutionCorrelationId } from '../analyticsLoggers/ruleExecutionLoggingUtils.js'; 16 16 import { RETURN_UNLIMITED_RESULTS_AND_POTENTIALLY_HANG_DB } from '../itemInvestigationService/index.js'; 17 17 import { 18 18 getFieldValueForRole, 19 19 getValuesFromFields, 20 20 } from '../itemProcessingService/extractItemDataValues.js'; 21 - import { 21 + import { 22 22 type ItemSubmission, 23 23 type NormalizedItemData, 24 24 } from '../itemProcessingService/index.js'; 25 - import { type ItemType } from '../moderationConfigService/types/itemTypes.js'; 26 25 import { 27 26 itemSubmissionToItemSubmissionWithTypeIdentifier, 28 27 itemSubmissionWithTypeIdentifierToItemSubmission, ··· 36 35 type ReportEnqueueSourceInfo, 37 36 type RuleExecutionEnqueueSourceInfo, 38 37 } from '../manualReviewToolService/manualReviewToolService.js'; 39 - 38 + import { type ItemType } from '../moderationConfigService/types/itemTypes.js'; 40 39 import type NcmecReporting from './ncmecReporting.js'; 41 40 42 41 export default class NcmecEnqueueToMrt { ··· 83 82 ), 84 83 ) { 85 84 const { orgId, createdAt } = input; 86 - 85 + 87 86 // Fetch as much info about the reported user as we can get from 88 87 // the organization's partial items endpoint, and if the reported item is 89 88 // content, then convert it to the user who created the content because ··· 177 176 input.item, 178 177 reportedItemType, 179 178 ); 180 - 179 + 181 180 if (allMediaItems.length === 0) { 182 181 return { status: 'SKIPPED' }; 183 182 } ··· 196 195 policyIds: [], 197 196 payload: { 198 197 kind: 'NCMEC', 199 - item: itemSubmissionToItemSubmissionWithTypeIdentifier(userSubmission), 198 + item: itemSubmissionToItemSubmissionWithTypeIdentifier( 199 + userSubmission, 200 + ), 200 201 allMediaItems, 201 202 reportHistory: [], 202 203 }, ··· 222 223 const mediaFields = reportedItemType.schema.filter((field) => 223 224 isMediaType(getScalarType(field)), 224 225 ); 225 - 226 + 226 227 if (mediaFields.length === 0) { 227 228 return []; 228 229 } 229 - 230 + 230 231 const mediaValues = getValuesFromFields(reportedItem.data, mediaFields); 231 - 232 + 232 233 if (mediaValues.length === 0) { 233 234 return []; 234 235 } 235 - 236 - return [{ 237 - contentItem: reportedItem, 238 - isConfirmedCSAM: false, 239 - isReported: true, 240 - }]; 236 + 237 + return [ 238 + { 239 + contentItem: reportedItem, 240 + isConfirmedCSAM: false, 241 + isReported: true, 242 + }, 243 + ]; 241 244 } 242 245 243 246 async #getAllMediaForUser( ··· 251 254 latestSubmission: ItemSubmission; 252 255 priorSubmissions?: ItemSubmission[]; 253 256 }> = []; 254 - 257 + 255 258 try { 256 259 itemsWithPossibleMedia = await asyncIterableToArray( 257 260 this.itemInvestigationService.getItemSubmissionsByCreator({ ··· 265 268 // Database not available (e.g., local dev without Cassandra/Scylla) 266 269 // Use the reported item directly 267 270 if (reportedItemSubmission && reportedItemType) { 268 - return await this.#getMediaFromReportedItem(reportedItemSubmission, reportedItemType); 271 + return this.#getMediaFromReportedItem( 272 + reportedItemSubmission, 273 + reportedItemType, 274 + ); 269 275 } 270 276 } 271 277 ··· 286 292 orgId, 287 293 itemTypeSelector: { id: reportedItemIdentifier.typeId }, 288 294 }); 289 - 295 + 290 296 if (itemType) { 291 - const reportedItemAsSubmission = itemSubmissionWithTypeIdentifierToItemSubmission( 292 - reportedItemSubmission, 293 - itemType, 294 - ); 297 + const reportedItemAsSubmission = 298 + itemSubmissionWithTypeIdentifierToItemSubmission( 299 + reportedItemSubmission, 300 + itemType, 301 + ); 295 302 latestItems = [reportedItemAsSubmission, ...latestItems]; 296 303 } 297 304 } else { ··· 363 370 | { itemSubmission: ItemSubmission; userIdentifier?: undefined } 364 371 | { itemSubmission?: undefined; userIdentifier: ItemIdentifier } 365 372 ), 366 - ): Promise<{ success: true; submission: ItemSubmission } | { success: false }> { 373 + ): Promise< 374 + { success: true; submission: ItemSubmission } | { success: false } 375 + > { 367 376 const { orgId, itemSubmission, userIdentifier } = opts; 368 377 369 378 const userItem = await (async (): Promise<ItemSubmission | null> => { ··· 399 408 orgId, 400 409 userItemId, 401 410 ); 402 - 411 + 403 412 // If partial items endpoint is not available, try fallbacks 404 413 if (!fetchedUser) { 405 414 // Fallback: Check item investigation service for previously submitted user data ··· 409 418 itemIdentifier: userItemId, 410 419 latestSubmissionOnly: true, 411 420 }); 412 - 421 + 413 422 if (investigatedUserResult?.latestSubmission) { 414 423 // The adapter already returns ItemSubmission, not ItemSubmissionWithTypeIdentifier 415 424 return investigatedUserResult.latestSubmission; 416 425 } 417 - 426 + 418 427 // User data not available from any source 419 428 return null; 420 429 } 421 - 430 + 422 431 return fetchedUser; 423 432 })(); 424 433 ··· 435 444 reportedItem: ItemSubmission; 436 445 }): Promise<ItemSubmission> { 437 446 const { orgId, reportedItemType, reportedItem } = opts; 438 - 447 + 439 448 // If the reported item is already a USER, use it 440 449 if (reportedItemType.kind === 'USER') { 441 450 return reportedItem; 442 451 } 443 - 452 + 444 453 // For CONTENT, try to extract the creator ID 445 454 if (reportedItemType.kind === 'CONTENT') { 446 455 const creatorId = getFieldValueForRole( ··· 449 458 'creatorId', 450 459 reportedItem.data, 451 460 ); 452 - 461 + 453 462 if (!creatorId) { 454 463 throw new Error( 455 464 'Cannot create NCMEC job: Content item does not have a creatorId field configured. ' + 456 - 'Please add the creatorId role to the owner/creator field in your item type schema.', 465 + 'Please add the creatorId role to the owner/creator field in your item type schema.', 457 466 ); 458 467 } 459 - 468 + 460 469 // Get the user item type 461 470 const userItemType = await this.moderationConfigService.getItemType({ 462 471 orgId, 463 472 itemTypeSelector: { id: creatorId.typeId }, 464 473 }); 465 - 474 + 466 475 if (!userItemType) { 467 476 throw new Error( 468 477 `Cannot create NCMEC job: User item type ${creatorId.typeId} not found.`, 469 478 ); 470 479 } 471 - 480 + 472 481 if (userItemType.kind !== 'USER') { 473 482 throw new Error( 474 483 `Cannot create NCMEC job: Item type ${creatorId.typeId} is not a USER type (it's ${userItemType.kind}).`, 475 484 ); 476 485 } 477 - 486 + 478 487 // Create a minimal user submission with just the ID 479 488 // The human reviewer will need to manually add more info 480 489 const minimalData: Record<string, unknown> = { 481 490 userId: creatorId.id, 482 491 }; 483 - 492 + 484 493 return instantiateOpaqueType<ItemSubmission>({ 485 494 itemId: creatorId.id, 486 495 itemType: userItemType, ··· 490 499 creator: undefined, 491 500 }); 492 501 } 493 - 502 + 494 503 // For THREAD or other types, we can't determine the user 495 504 throw new Error( 496 505 `Cannot create NCMEC job: Cannot determine user from item type ${reportedItemType.kind}. ` + 497 - 'Please report the USER directly.', 506 + 'Please report the USER directly.', 498 507 ); 499 508 } 500 509
+44 -32
server/services/ncmecService/ncmecReporting.ts
··· 390 390 if (!defaultInternetDetailType?.trim()) { 391 391 return undefined; 392 392 } 393 - const type = defaultInternetDetailType.trim() as NcmecInternetDetailTypeSetting; 393 + const type = 394 + defaultInternetDetailType.trim() as NcmecInternetDetailTypeSetting; 394 395 if (!NCMEC_INTERNET_DETAIL_TYPES.includes(type)) { 395 396 return undefined; 396 397 } ··· 794 795 return ncmecOrgSettings?.org_id != null; 795 796 } 796 797 797 - async getNCMECConfig(orgId: string): Promise<NcmecReportingServicePg['ncmec_reporting.ncmec_org_settings'] | undefined> { 798 + async getNCMECConfig( 799 + orgId: string, 800 + ): Promise< 801 + NcmecReportingServicePg['ncmec_reporting.ncmec_org_settings'] | undefined 802 + > { 798 803 const row = await this.pgQuery 799 804 .selectFrom('ncmec_reporting.ncmec_org_settings') 800 805 .where('org_id', '=', orgId) ··· 899 904 const additionalInfoEndpoint = await this.ncmecAdditionalInfoEndpoint( 900 905 orgId, 901 906 ); 902 - 907 + 903 908 // If no additional info endpoint is configured, return minimal default data 904 909 if (!additionalInfoEndpoint) { 905 910 return { ··· 919 924 })), 920 925 }; 921 926 } 922 - 927 + 923 928 const response = await this.fetchHTTP({ 924 929 url: additionalInfoEndpoint, 925 930 method: 'post', ··· 1081 1086 ); 1082 1087 1083 1088 if (ncmecPreservationEndpoint == null) { 1084 - throw new Error('Organization does not have a NCMEC preservation endpoint'); 1089 + throw new Error( 1090 + 'Organization does not have a NCMEC preservation endpoint', 1091 + ); 1085 1092 } 1086 1093 1087 1094 const fetchWithRetries = withRetries( ··· 1173 1180 // they should be able to click "Send to NCMEC" in the UI, but no 1174 1181 // NCMEC report should actually be created. 1175 1182 const testOrgs = ['4def6a77d6a', 'acc701627cb']; 1176 - 1183 + 1177 1184 if (!(await this.hasNCMECReportingEnabled(reportParams.orgId))) { 1178 1185 throw new Error( 1179 1186 `NCMEC reports are not enabled for org ${reportParams.orgId}`, 1180 1187 ); 1181 1188 } 1182 - 1189 + 1183 1190 if (testOrgs.includes(reportParams.orgId)) { 1184 1191 return 'UNSUPPORTED_ORG'; 1185 1192 } ··· 1270 1277 const ncmecConfig = await this.getNCMECConfig(reportParams.orgId); 1271 1278 1272 1279 // Use the incident type from the report params 1273 - const incidentType = NCMECIncidentType[reportParams.incidentType as NCMECIncidentType]; 1280 + const incidentType = 1281 + NCMECIncidentType[reportParams.incidentType as NCMECIncidentType]; 1274 1282 1275 1283 const escalateToHighPriority = 1276 1284 reportParams.escalateToHighPriority != null ··· 1278 1286 : undefined; 1279 1287 if ( 1280 1288 escalateToHighPriority !== undefined && 1281 - (escalateToHighPriority === '' || escalateToHighPriority.length > 3000) 1289 + (escalateToHighPriority === '' || 1290 + escalateToHighPriority.length > 3000) 1282 1291 ) { 1283 1292 throw new Error( 1284 1293 'escalateToHighPriority must be non-blank when supplied and at most 3000 characters', ··· 1295 1304 incidentSummary: { 1296 1305 incidentType, 1297 1306 incidentDateTime: maxCreatedAt, 1298 - ...(escalateToHighPriority 1299 - ? { escalateToHighPriority } 1300 - : {}), 1307 + ...(escalateToHighPriority ? { escalateToHighPriority } : {}), 1301 1308 }, 1302 1309 ...(internetDetails ? { internetDetails } : {}), 1303 1310 reporter: { 1304 1311 reportingPerson: { 1305 - email: [emailStringToNCMECEmail(ncmecConfig?.contact_email ?? '')], 1312 + email: [ 1313 + emailStringToNCMECEmail(ncmecConfig?.contact_email ?? ''), 1314 + ], 1306 1315 }, 1307 1316 companyTemplate: queryResponse.companyTemplate, 1308 1317 legalURL: queryResponse.legalURL, ··· 1312 1321 ? { termsOfService: queryResponse.termsOfService.trim() } 1313 1322 : {}), 1314 1323 // Use || so we only add contactPerson when at least one field is non-empty (?? would use first non-null even if empty) 1315 - /* eslint-disable @typescript-eslint/prefer-nullish-coalescing -- intentional: treat empty string as absent */ 1316 - ...((queryResponse.contactPersonEmail?.trim() || 1317 - queryResponse.contactPersonFirstName?.trim() || 1318 - queryResponse.contactPersonLastName?.trim() || 1319 - queryResponse.contactPersonPhone?.trim()) 1324 + 1325 + ...(queryResponse.contactPersonEmail?.trim() || 1326 + queryResponse.contactPersonFirstName?.trim() || 1327 + queryResponse.contactPersonLastName?.trim() || 1328 + queryResponse.contactPersonPhone?.trim() 1320 1329 ? { 1321 1330 contactPerson: { 1322 1331 ...(queryResponse.contactPersonEmail?.trim() ··· 1330 1339 : {}), 1331 1340 ...(queryResponse.contactPersonFirstName?.trim() 1332 1341 ? { 1333 - firstName: queryResponse.contactPersonFirstName.trim(), 1342 + firstName: 1343 + queryResponse.contactPersonFirstName.trim(), 1334 1344 } 1335 1345 : {}), 1336 1346 ...(queryResponse.contactPersonLastName?.trim() 1337 1347 ? { 1338 - lastName: queryResponse.contactPersonLastName.trim(), 1348 + lastName: 1349 + queryResponse.contactPersonLastName.trim(), 1339 1350 } 1340 1351 : {}), 1341 1352 ...(queryResponse.contactPersonPhone?.trim() ··· 1348 1359 }, 1349 1360 } 1350 1361 : {}), 1351 - /* eslint-enable @typescript-eslint/prefer-nullish-coalescing */ 1352 1362 }, 1353 1363 personOrUserReported: { 1354 1364 personOrUserReportedPerson: { ··· 1476 1486 // safeTracer's logging because those logs are sampled in DD. For 1477 1487 // NCMEC submission errors we need to record all failures and be 1478 1488 // able to see the logs 1479 - 1489 + 1480 1490 // eslint-disable-next-line no-console 1481 1491 console.error('[NCMEC] ❌ Error during report submission:', e); 1482 1492 // eslint-disable-next-line no-console ··· 1484 1494 message: e instanceof Error ? e.message : String(e), 1485 1495 stack: e instanceof Error ? e.stack : undefined, 1486 1496 }); 1487 - 1497 + 1488 1498 // eslint-disable-next-line no-restricted-syntax 1489 1499 logErrorJson({ 1490 1500 error: e, ··· 1578 1588 isTest: boolean, 1579 1589 ) { 1580 1590 const reportXML = js2xml(report, { compact: true }); 1581 - 1591 + 1582 1592 // Save XML to file for review (development only) 1583 1593 if (process.env.NODE_ENV === 'development') { 1584 1594 try { 1585 1595 const fs = await import('fs/promises'); 1586 1596 const path = await import('path'); 1587 - 1597 + 1588 1598 // Create ncmec-reports directory if it doesn't exist 1589 1599 const reportsDir = path.join(process.cwd(), 'ncmec-reports'); 1590 1600 await fs.mkdir(reportsDir, { recursive: true }); 1591 - 1601 + 1592 1602 // Generate filename with timestamp and test indicator 1593 1603 const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); 1594 1604 const testPrefix = isTest ? 'TEST-' : 'PROD-'; 1595 1605 const filename = `${testPrefix}${timestamp}.xml`; 1596 1606 const filepath = path.join(reportsDir, filename); 1597 - 1607 + 1598 1608 // Write XML to file 1599 1609 await fs.writeFile(filepath, reportXML, 'utf-8'); 1600 - 1601 1610 } catch (e) { 1602 1611 // Silent fail - don't let file saving break the submission 1603 1612 } 1604 1613 } 1605 - 1614 + 1606 1615 const response = await this.#sendCyberTipRequest({ 1607 1616 cybertipAuthenticationCredentials, 1608 1617 body: reportXML, ··· 1614 1623 if (responseJson.reportResponse.responseCode._text !== '0') { 1615 1624 throw new Error('NCMEC report submission failed.'); 1616 1625 } 1617 - 1626 + 1618 1627 // eslint-disable-next-line no-console 1619 - console.log('[NCMEC] ✅ Report submitted successfully! Report ID:', responseJson.reportResponse.reportId._text); 1620 - 1628 + console.log( 1629 + '[NCMEC] ✅ Report submitted successfully! Report ID:', 1630 + responseJson.reportResponse.reportId._text, 1631 + ); 1632 + 1621 1633 return { 1622 1634 reportId: responseJson.reportResponse.reportId._text, 1623 1635 xml: reportXML,
+17 -7
server/services/orgAwareSignalExecutionService/signalExecutionService.test.ts
··· 14 14 bankId === '1' ? ['a', 'b', 'c'] : bankId === '2' ? ['d', 'e', 'f'] : [], 15 15 ); 16 16 const mockGetImageBank = jest.fn(async ({ bankId }) => 17 - bankId === 'test-bank' ? { id: 1, name: 'test-bank', hma_name: 'org_test-bank', description: null, enabled_ratio: 1.0, org_id: 'test-org', created_at: new Date(), updated_at: new Date() } : null, 17 + bankId === 'test-bank' 18 + ? { 19 + id: 1, 20 + name: 'test-bank', 21 + hma_name: 'org_test-bank', 22 + description: null, 23 + enabled_ratio: 1.0, 24 + org_id: 'test-org', 25 + created_at: new Date(), 26 + updated_at: new Date(), 27 + } 28 + : null, 18 29 ); 19 30 20 - // eslint-disable-next-line better-mutation/no-mutation 31 + // eslint-disable-next-line functional/immutable-data 21 32 mockLocationsLoader.close = jest.fn(); 22 - // eslint-disable-next-line better-mutation/no-mutation 33 + // eslint-disable-next-line functional/immutable-data 23 34 (mockTextBankStringsLoader as any).close = jest.fn(); 24 - // eslint-disable-next-line better-mutation/no-mutation, @typescript-eslint/no-explicit-any 35 + // eslint-disable-next-line functional/immutable-data, @typescript-eslint/no-explicit-any 25 36 (mockGetImageBank as any).close = jest.fn(); 26 37 27 38 describe('getTransientRunSignalWithCache', () => { ··· 31 42 // `let` vars to defer the work until the test suite is actually running 32 43 // (so we don't bother w/ it if this test suite is skipped, e.g., in which 33 44 // case cleanup wouldn't happen). 34 - /* eslint-disable better-mutation/no-mutation */ 45 + 35 46 container = await getBottleContainerWithIOMocks(); 36 47 signalsService = container.SignalsService; 37 48 getPolicyActionPenalties = 38 49 container.getPolicyActionPenaltiesEventuallyConsistent; 39 - /* eslint-enable better-mutation/no-mutation */ 40 50 }); 41 51 42 52 afterAll(async () => { ··· 123 133 // signals don't hit the network. (The point of the mock is just so we can 124 134 // spy on how many times runSignal was called.) 125 135 const signalsServiceSpy = (await getBottle()).container.SignalsService; 126 - // eslint-disable-next-line better-mutation/no-mutation 136 + // eslint-disable-next-line functional/immutable-data 127 137 signalsServiceSpy.runSignal = jest.fn( 128 138 signalsServiceSpy.runSignal.bind(signalsServiceSpy), 129 139 ) as any;
+7 -3
server/services/ruleAnomalyDetectionService/detectRulePassRateAnomaliesJob.test.ts
··· 27 27 >; 28 28 29 29 beforeAll(async () => { 30 - /* eslint-disable better-mutation/no-mutation */ 30 + /* eslint-disable functional/immutable-data */ 31 31 const { 32 32 Sequelize: models, 33 33 ModerationConfigService, ··· 39 39 // in different initial alarm statuses, to test all 9 combinations [i.e., 40 40 // starting and ending at one of (OK, ALARM, or INSUFFICENT_DATA), where 41 41 // the start and end states can be the same]. 42 - const { org } = await createOrg(models, ModerationConfigService, ApiKeyService); 42 + const { org } = await createOrg( 43 + models, 44 + ModerationConfigService, 45 + ApiKeyService, 46 + ); 43 47 const { org: org2 } = await createOrg( 44 48 models, 45 49 ModerationConfigService, ··· 150 154 await Promise.all([org.destroy(), org2.destroy()]); 151 155 await models.sequelize.close(); 152 156 }; 153 - /* eslint-enable better-mutation/no-mutation */ 157 + /* eslint-enable functional/immutable-data */ 154 158 }); 155 159 156 160 afterAll(async () => {
+9 -3
server/services/ruleAnomalyDetectionService/getRuleAnomalyDetectionStatistics.test.ts
··· 10 10 * least makes sure we can't change inadvertently change the generated queries. 11 11 */ 12 12 describe('getRuleAnomalyDetectionStatistics', () => { 13 - let queryMock: MockedFn<(query: string, tracer: any, binds?: readonly unknown[]) => Promise<unknown[]>>; 13 + let queryMock: MockedFn< 14 + ( 15 + query: string, 16 + tracer: any, 17 + binds?: readonly unknown[], 18 + ) => Promise<unknown[]> 19 + >; 14 20 let getRulePassStatistics: Dependencies['getRuleAnomalyDetectionStatistics']; 15 21 16 22 beforeAll(() => { ··· 26 32 ]; 27 33 28 34 // Scope of this is just the test suite, so mutation should be ok. 29 - // eslint-disable-next-line better-mutation/no-mutation 35 + 30 36 queryMock = jest.fn() as any; 31 37 queryMock.mockResolvedValue(queryResult); 32 38 ··· 39 45 }; 40 46 41 47 // Scope of this is just the test suite, so mutation should be ok. 42 - // eslint-disable-next-line better-mutation/no-mutation 48 + 43 49 getRulePassStatistics = makeGetRuleAnomalyDetectionStatistics( 44 50 dataWarehouseMock, 45 51 {} as any,
+1 -1
server/services/ruleHistoryService/ruleHistoryService.test.ts
··· 12 12 const deps = await getBottle(); 13 13 14 14 // Scope of this is just the test suite, so reassignment should be ok. 15 - // eslint-disable-next-line better-mutation/no-mutation 15 + 16 16 db = deps.container.KyselyPg; 17 17 }); 18 18
+8 -8
server/services/signalAuthService/signalAuthService.ts
··· 1 - /* eslint-disable max-lines */ 2 1 import { type Kysely } from 'kysely'; 3 2 4 3 import { inject } from '../../iocContainer/utils.js'; 5 4 import { type Cached } from '../../utils/caching.js'; 6 - import { type JsonOf } from '../../utils/encoding.js'; 7 - import { jsonParse, jsonStringify } from '../../utils/encoding.js'; 5 + import { jsonParse, jsonStringify, type JsonOf } from '../../utils/encoding.js'; 8 6 import { type NonEmptyString } from '../../utils/typescript-types.js'; 9 7 import { Integration } from '../signalsService/index.js'; 10 8 import { type SignalAuthServicePg } from './dbTypes.js'; ··· 136 134 } 137 135 if (integrationId === Integration.ZENTROPI) { 138 136 const c = await this.get(Integration.ZENTROPI, orgId); 139 - return c != null ? { apiKey: c.apiKey, labelerVersions: c.labelerVersions } : undefined; 137 + return c != null 138 + ? { apiKey: c.apiKey, labelerVersions: c.labelerVersions } 139 + : undefined; 140 140 } 141 141 const row = await this.pg 142 142 .selectFrom('signal_auth_service.integration_configs') ··· 277 277 labelerVersions: Array.isArray(labelerVersions) 278 278 ? (labelerVersions as ZentropiLabelerVersion[]) 279 279 : typeof labelerVersions === 'string' 280 - ? (jsonParse(labelerVersions as JsonOf<ZentropiLabelerVersion[]>)) 281 - : [], 280 + ? jsonParse(labelerVersions as JsonOf<ZentropiLabelerVersion[]>) 281 + : [], 282 282 }; 283 283 }, 284 284 set: async (orgId: string, credential: ZentropiCredential) => { ··· 308 308 labelerVersions: Array.isArray(returnedVersions) 309 309 ? (returnedVersions as ZentropiLabelerVersion[]) 310 310 : typeof returnedVersions === 'string' 311 - ? (jsonParse(returnedVersions as JsonOf<ZentropiLabelerVersion[]>)) 312 - : [], 311 + ? jsonParse(returnedVersions as JsonOf<ZentropiLabelerVersion[]>) 312 + : [], 313 313 }; 314 314 }, 315 315 delete: async (orgId: string) => {
+12 -14
server/services/signalsService/SignalsService.ts
··· 1 - /* eslint-disable max-lines */ 2 - import { type ConsumerDirectives } from '../../lib/cache/index.js'; 3 1 import { type ReadonlyDeep, type Simplify } from 'type-fest'; 4 2 5 3 import { inject, type Dependencies } from '../../iocContainer/index.js'; 4 + import { type ConsumerDirectives } from '../../lib/cache/index.js'; 6 5 import { isTaggedItemData } from '../../models/rules/item-type-fields.js'; 7 6 import { jsonStringify } from '../../utils/encoding.js'; 8 7 import { CoopError, ErrorType, makeNotFoundError } from '../../utils/errors.js'; ··· 60 59 */ 61 60 export type Signal = Simplify< 62 61 Pick< 63 - SignalBase< 64 - SignalInputType, 65 - SignalOutputType, 66 - unknown, 67 - SignalType | string 68 - >, 62 + SignalBase<SignalInputType, SignalOutputType, unknown, SignalType | string>, 69 63 (typeof publicSignalProps)[number] 70 64 > 71 65 >; ··· 138 132 const cachedCredentialGetters = 139 133 makeCachedCredentialGetters(signalAuthService); 140 134 141 - const cachedFetchers = makeCachedFetchers( 142 - fetchHTTP, 143 - tracer, 144 - ); 135 + const cachedFetchers = makeCachedFetchers(fetchHTTP, tracer); 145 136 146 137 this.builtInSignalsByType = instantiateBuiltInSignals( 147 138 cachedCredentialGetters, ··· 156 147 const pluginEntries = getIntegrationRegistry().getPluginEntries(); 157 148 const pluginSignals = loadPluginSignals(pluginEntries, signalAuthService); 158 149 const builtInIds = new Set(Object.keys(this.builtInSignalsByType)); 159 - const collision = Object.keys(pluginSignals).find((id) => builtInIds.has(id)); 150 + const collision = Object.keys(pluginSignals).find((id) => 151 + builtInIds.has(id), 152 + ); 160 153 if (collision != null) { 161 154 throw new Error( 162 155 `Plugin signal type "${collision}" collides with a built-in signal; use a different signalTypeId.`, ··· 334 327 335 328 #signalInstanceToPublicSignal( 336 329 it: ReadonlyDeep< 337 - SignalBase<SignalInputType, SignalOutputType, unknown, SignalType | string> 330 + SignalBase< 331 + SignalInputType, 332 + SignalOutputType, 333 + unknown, 334 + SignalType | string 335 + > 338 336 >, 339 337 ) { 340 338 // This used to be implemented as simply `safePick(it, publicSignalProps)`,
-1
server/services/signalsService/helpers/instantiateBuiltInSignals.ts
··· 1 - /* eslint-disable max-lines */ 2 1 import { type ItemIdentifier } from '@roostorg/types'; 3 2 4 3 import type { AggregationsService } from '../../aggregationsService/index.js';
+9 -9
server/services/signalsService/signals/aggregation/AggregationSignal.test.ts
··· 46 46 47 47 // Spy on aggregation service functions. 48 48 const aggregationsServiceSpy = AggregationsService; 49 - // eslint-disable-next-line better-mutation/no-mutation 49 + // eslint-disable-next-line functional/immutable-data 50 50 aggregationsServiceSpy.updateAggregation = jest.fn( 51 51 aggregationsServiceSpy.updateAggregation.bind(aggregationsServiceSpy), 52 52 ); 53 - // eslint-disable-next-line better-mutation/no-mutation 53 + // eslint-disable-next-line functional/immutable-data 54 54 aggregationsServiceSpy.evaluateAggregation = jest.fn( 55 55 aggregationsServiceSpy.evaluateAggregation.bind(aggregationsServiceSpy), 56 56 ); 57 57 58 58 const dateProvider = new TestDateProvider(); 59 - // eslint-disable-next-line better-mutation/no-mutation 59 + // eslint-disable-next-line functional/immutable-data 60 60 deps['ActionPublisher'].publishAction = jest.fn().mockReturnValue(true); 61 61 62 62 const rule = await RuleAPIDataSource.createContentRule( ··· 148 148 async ({ itemType, org, dateProvider, deps }) => { 149 149 // Clear Redis to ensure clean state for aggregation counters 150 150 await deps.IORedis.flushdb(); 151 - 151 + 152 152 const creatorId = 'some-creator-id'; 153 153 const creatorTypeId = 'some-creator-type-id'; 154 154 ··· 236 236 creatorTypeId, 237 237 }, 238 238 ); 239 - 239 + 240 240 const ruleResultsAfter2 = await deps.RuleEngine.runEnabledRules( 241 241 itemSubmission2, 242 242 toCorrelationId({ ··· 262 262 creatorTypeId, 263 263 }, 264 264 ); 265 - 265 + 266 266 const ruleResultsAnotherUser = await deps.RuleEngine.runEnabledRules( 267 267 itemSubmissionAnotherUser, 268 268 toCorrelationId({ ··· 292 292 creatorTypeId, 293 293 }, 294 294 ); 295 - 295 + 296 296 const ruleResultsFailCondition = await deps.RuleEngine.runEnabledRules( 297 297 itemSubmissionFailCondition, 298 298 toCorrelationId({ ··· 319 319 creatorTypeId, 320 320 }, 321 321 ); 322 - 322 + 323 323 const ruleResultsAfter3 = await deps.RuleEngine.runEnabledRules( 324 324 itemSubmission3, 325 325 toCorrelationId({ ··· 345 345 creatorTypeId, 346 346 }, 347 347 ); 348 - 348 + 349 349 const actionsTriggeredAfter4 = await ( 350 350 await deps.RuleEngine.runEnabledRules( 351 351 itemSubmission4,
+1 -1
server/services/signalsService/signals/third_party_signals/google/content_safety/GoogleContentSafetyImageSignal.ts
··· 76 76 77 77 override async getDisabledInfo(orgId: string) { 78 78 const credential = await this.getGoogleContentSafetyCredentials(orgId); 79 - // eslint-disable-next-line security/detect-possible-timing-attacks 79 + 80 80 return !credential?.apiKey 81 81 ? { 82 82 disabled: true as const,
+2 -7
server/services/signalsService/signals/third_party_signals/google/content_safety/googleContentSafetyLib.ts
··· 88 88 url, 89 89 { 90 90 method: 'post', 91 - headers: {'Content-Type': 'application/json'}, 91 + headers: { 'Content-Type': 'application/json' }, 92 92 body: jsonStringify(reqBody), 93 93 }, 94 94 this.timeoutMs, ··· 130 130 const { value, orgId } = input; 131 131 const credential = await getGoogleContentSafetyCredentials(orgId); 132 132 133 - // eslint-disable-next-line security/detect-possible-timing-attacks 134 133 if (!credential?.apiKey) { 135 134 throw new Error('Missing API credentials'); 136 135 } ··· 174 173 175 174 let imageBuffer: Buffer; 176 175 try { 177 - imageBuffer = await fetchImage( 178 - fetchHTTP, 179 - imageUrl, 180 - IMAGE_FETCH_TIMEOUT_MS, 181 - ); 176 + imageBuffer = await fetchImage(fetchHTTP, imageUrl, IMAGE_FETCH_TIMEOUT_MS); 182 177 } catch (e) { 183 178 if (safeGet(e, ['name']) === 'ResponseExceededMaxSizeError') { 184 179 throw makeSignalPermanentError('Response too large', {
-1
server/services/signalsService/signals/third_party_signals/open_ai/moderation/openAIModerationUtils.ts
··· 85 85 const { value, orgId } = input; 86 86 const credential = await getOpenAiCredentials(orgId); 87 87 88 - // eslint-disable-next-line security/detect-possible-timing-attacks 89 88 if (!credential?.apiKey) { 90 89 throw new Error('Missing API credentials'); 91 90 }
-1
server/services/signalsService/signals/third_party_signals/open_ai/whisper/OpenAiWhisperTranscriptionSignal.ts
··· 186 186 const { value } = input; 187 187 const credential = await this.getOpenAiCredentials(input.orgId); 188 188 189 - // eslint-disable-next-line security/detect-possible-timing-attacks 190 189 if (!credential || !credential.apiKey) { 191 190 throw new Error('Missing API credentials'); 192 191 }
+2 -6
server/services/signalsService/signals/third_party_signals/zentropi/zentropiUtils.ts
··· 13 13 explanation?: string; 14 14 } 15 15 16 - export type FetchZentropiScores = Bind1< 17 - typeof getZentropiScores, 18 - FetchHTTP 19 - >; 16 + export type FetchZentropiScores = Bind1<typeof getZentropiScores, FetchHTTP>; 20 17 21 18 export async function getZentropiScores( 22 19 fetchHTTP: FetchHTTP, ··· 66 63 const { value, orgId, subcategory } = input; 67 64 68 65 const credential = await getZentropiCredentials(orgId); 69 - // eslint-disable-next-line security/detect-possible-timing-attacks 66 + 70 67 if (!credential?.apiKey) { 71 68 throw new Error('Missing Zentropi API credentials'); 72 69 } ··· 96 93 outputType: { scalarType: ScalarTypes.NUMBER }, 97 94 }; 98 95 } 99 -
-1
server/services/userStatisticsService/computeUserScore.test.ts
··· 1 - // eslint-disable-next-line import/no-extraneous-dependencies 2 1 import { computeUserScore } from './computeUserScore.js'; 3 2 4 3 describe('computeUserScore', () => {
-2
server/services/userStatisticsService/computeUserScore.ts
··· 72 72 const { itemSubmissionIds, actionId, policyId } = stats; 73 73 74 74 for (const submissionId of itemSubmissionIds) { 75 - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 76 75 res[submissionId] = (res[submissionId] ?? []).concat( 77 76 getPenaltyKey({ actionId, policyId }), 78 77 ); ··· 106 105 .concat(itemSubmissionMostSeverePenaltyKeys.map((it) => [it, 1] as const)) 107 106 .reduce( 108 107 (acc, [penaltyKey, count]) => { 109 - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 110 108 acc[penaltyKey] = (acc[penaltyKey] ?? 0) + count; 111 109 return acc; 112 110 },
+3 -4
server/services/userStatisticsService/fetchUserActionStatistics.test.ts
··· 7 7 } from 'kysely'; 8 8 9 9 import { type Dependencies } from '../../iocContainer/index.js'; 10 - import { makeMockWarehouseDialect } from '../../test/stubs/makeMockWarehouseKyselyDialect.js'; 11 10 import { type MockedFn } from '../../test/mockHelpers/jestMocks.js'; 11 + import { makeMockWarehouseDialect } from '../../test/stubs/makeMockWarehouseKyselyDialect.js'; 12 12 import { safePick } from '../../utils/misc.js'; 13 13 import { makeFetchUserActionStatistics } from './fetchUserActionStatistics.js'; 14 14 ··· 26 26 // This mutation is safe (while we're not running tests concurrently) as 27 27 // it's local to the test suite. Consider using the `makeTestWithFixture` 28 28 // helper instead to make a local copy of this state for each test. 29 - // eslint-disable-next-line better-mutation/no-mutation 29 + 30 30 warehouseMock = jest 31 31 .fn<DatabaseConnection['executeQuery']>() 32 32 .mockResolvedValue({ rows: [] }); ··· 34 34 // This mutation is safe (while we're not running tests concurrently) as 35 35 // it's local to the test suite. Consider using the `makeTestWithFixture` 36 36 // helper instead to make a local copy of this state for each test. 37 - // eslint-disable-next-line better-mutation/no-mutation 37 + 38 38 const kysely = new Kysely({ 39 39 dialect: makeMockWarehouseDialect(warehouseMock), 40 40 }); ··· 43 43 destroy: jest.fn(async () => {}), 44 44 }; 45 45 46 - // eslint-disable-next-line better-mutation/no-mutation 47 46 sut = makeFetchUserActionStatistics(dialectMock); 48 47 }); 49 48
+3 -4
server/services/userStatisticsService/fetchUserSubmissionStatistics.test.ts
··· 2 2 import { Kysely, type CompiledQuery, type QueryResult } from 'kysely'; 3 3 4 4 import { type Dependencies } from '../../iocContainer/index.js'; 5 - import { makeMockWarehouseDialect } from '../../test/stubs/makeMockWarehouseKyselyDialect.js'; 6 5 import { type MockedFn } from '../../test/mockHelpers/jestMocks.js'; 6 + import { makeMockWarehouseDialect } from '../../test/stubs/makeMockWarehouseKyselyDialect.js'; 7 7 import { safePick } from '../../utils/misc.js'; 8 8 import { makeFetchUserSubmissionStatistics } from './fetchUserSubmissionStatistics.js'; 9 9 ··· 17 17 // This mutation is safe (while we're not running tests concurrently) as 18 18 // it's local to the test suite. Consider using the `makeTestWithFixture` 19 19 // helper instead to make a local copy of this state for each test. 20 - // eslint-disable-next-line better-mutation/no-mutation 20 + 21 21 warehouseMock = jest.fn(async (_it) => Promise.resolve({ rows: [] })); 22 22 23 23 // This mutation is safe (while we're not running tests concurrently) as 24 24 // it's local to the test suite. Consider using the `makeTestWithFixture` 25 25 // helper instead to make a local copy of this state for each test. 26 - // eslint-disable-next-line better-mutation/no-mutation 26 + 27 27 const kysely = new Kysely({ 28 28 dialect: makeMockWarehouseDialect(warehouseMock), 29 29 }); ··· 32 32 destroy: jest.fn(async () => {}), 33 33 }; 34 34 35 - // eslint-disable-next-line better-mutation/no-mutation 36 35 sut = makeFetchUserSubmissionStatistics(dialectMock); 37 36 }); 38 37
+1 -2
server/services/userStrikeService/userStrikeService.test.ts
··· 10 10 beforeAll(async () => { 11 11 // The mutation should be ok here since this is initial setup in a 12 12 // beforeAll; it doesn't involve reset state for each test in the suite 13 - /* eslint-disable better-mutation/no-mutation */ 13 + 14 14 ({ container } = await getBottle()); 15 15 userStrikeService = container.UserStrikeService; 16 - /* eslint-enable better-mutation/no-mutation */ 17 16 }); 18 17 afterAll(async () => { 19 18 await container.closeSharedResourcesForShutdown();
+22 -10
server/storage/dataWarehouse/ClickhouseAdapter.ts
··· 1 - // eslint-disable-next-line import/no-extraneous-dependencies 2 1 import { createClient, type ClickHouseClient } from '@clickhouse/client'; 3 2 import { 4 3 Kysely, ··· 17 16 18 17 import { formatClickhouseQuery } from '../../plugins/warehouse/utils/clickhouseSql.js'; 19 18 import type { 20 - IDataWarehouseDialect, 21 19 DataWarehousePoolSettings, 20 + IDataWarehouseDialect, 22 21 } from './IDataWarehouse.js'; 23 22 24 23 export interface ClickhouseConnectionSettings { ··· 31 30 } 32 31 33 32 function createConnection(client: ClickHouseClient): DatabaseConnection { 34 - const execute = async <R>(compiledQuery: CompiledQuery): Promise<QueryResult<R>> => { 35 - const statement = formatClickhouseQuery(compiledQuery.sql, compiledQuery.parameters); 33 + const execute = async <R>( 34 + compiledQuery: CompiledQuery, 35 + ): Promise<QueryResult<R>> => { 36 + const statement = formatClickhouseQuery( 37 + compiledQuery.sql, 38 + compiledQuery.parameters, 39 + ); 36 40 const result = await client.query({ 37 41 query: statement, 38 42 format: 'JSONEachRow', ··· 45 49 return { 46 50 executeQuery: execute, 47 51 streamQuery<R>(compiledQuery: CompiledQuery) { 48 - return (async function* iterator(): AsyncIterableIterator<QueryResult<R>> { 52 + return (async function* iterator(): AsyncIterableIterator< 53 + QueryResult<R> 54 + > { 49 55 yield await execute<R>(compiledQuery); 50 56 })(); 51 57 }, ··· 64 70 _connection: DatabaseConnection, 65 71 _settings: TransactionSettings, 66 72 ) { 67 - throw new Error('ClickHouse does not support multi-statement transactions'); 73 + throw new Error( 74 + 'ClickHouse does not support multi-statement transactions', 75 + ); 68 76 }, 69 77 async commitTransaction() { 70 - throw new Error('ClickHouse does not support multi-statement transactions'); 78 + throw new Error( 79 + 'ClickHouse does not support multi-statement transactions', 80 + ); 71 81 }, 72 82 async rollbackTransaction() { 73 - throw new Error('ClickHouse does not support multi-statement transactions'); 83 + throw new Error( 84 + 'ClickHouse does not support multi-statement transactions', 85 + ); 74 86 }, 75 87 async releaseConnection(_connection: DatabaseConnection) { 76 88 // Nothing to release; HTTP client pools internally. ··· 112 124 113 125 const url = `${protocol}://${connectionSettings.host}:${port}`; 114 126 const rawPassword = connectionSettings.password; 115 - const password = rawPassword && rawPassword.length > 0 ? rawPassword : undefined; 127 + const password = 128 + rawPassword && rawPassword.length > 0 ? rawPassword : undefined; 116 129 this.client = createClient({ 117 130 url, 118 131 username: connectionSettings.username, ··· 137 150 await this.kysely.destroy(); 138 151 } 139 152 } 140 -
+36 -36
server/storage/dataWarehouse/DataWarehouseFactory.ts
··· 4 4 5 5 /* eslint-disable max-classes-per-file */ 6 6 import { 7 + ClickhouseAnalyticsAdapter as ClickhouseAnalyticsPlugin, 8 + NoOpAnalyticsAdapter, 9 + type AnalyticsEventInput, 10 + type IAnalyticsAdapter, 11 + } from '../../plugins/analytics/index.js'; 12 + import { 13 + ClickhouseWarehouseAdapter, 14 + NoOpWarehouseAdapter, 15 + type IWarehouseAdapter, 16 + } from '../../plugins/warehouse/index.js'; 17 + import { assertUnreachable } from '../../utils/misc.js'; 18 + import type SafeTracer from '../../utils/SafeTracer.js'; 19 + import { 7 20 ClickhouseKyselyAdapter, 8 21 type ClickhouseConnectionSettings, 9 22 } from './ClickhouseAdapter.js'; 10 23 import { 24 + type DataWarehousePoolSettings, 11 25 type IDataWarehouse, 12 26 type IDataWarehouseDialect, 13 - type DataWarehousePoolSettings, 14 27 type DataWarehouseProvider as IDataWarehouseProvider, 15 28 type TransactionFunction, 16 29 } from './IDataWarehouse.js'; ··· 22 35 IDataWarehouseAnalytics, 23 36 } from './IDataWarehouseAnalytics.js'; 24 37 import { PostgresAnalyticsAdapter } from './PostgresAnalyticsAdapter.js'; 25 - import { 26 - ClickhouseWarehouseAdapter, 27 - NoOpWarehouseAdapter, 28 - type IWarehouseAdapter, 29 - } from '../../plugins/warehouse/index.js'; 30 - import { 31 - ClickhouseAnalyticsAdapter as ClickhouseAnalyticsPlugin, 32 - NoOpAnalyticsAdapter, 33 - type IAnalyticsAdapter, 34 - type AnalyticsEventInput, 35 - } from '../../plugins/analytics/index.js'; 36 - import { assertUnreachable } from '../../utils/misc.js'; 37 - import type SafeTracer from '../../utils/SafeTracer.js'; 38 38 39 39 /** 40 40 * Concrete data warehouse providers 41 41 * Extend this type to add new warehouse implementations 42 42 */ 43 - export type DataWarehouseProvider = 44 - | 'clickhouse' 45 - | 'postgresql' 46 - | 'noop'; 43 + export type DataWarehouseProvider = 'clickhouse' | 'postgresql' | 'noop'; 47 44 48 - export type AnalyticsProvider = 49 - | 'clickhouse' 50 - | 'postgresql' 51 - | 'noop'; 45 + export type AnalyticsProvider = 'clickhouse' | 'postgresql' | 'noop'; 52 46 53 47 // Re-export the interface provider type for external use 54 48 export type { IDataWarehouseProvider }; ··· 94 88 }; 95 89 96 90 return tracer.addActiveSpan( 97 - { resource: `${this.provider}.client`, operation: `${this.provider}.query` }, 91 + { 92 + resource: `${this.provider}.client`, 93 + operation: `${this.provider}.query`, 94 + }, 98 95 runQuery, 99 96 ); 100 97 } ··· 125 122 } 126 123 } 127 124 128 - class AnalyticsAdapterBridge 129 - implements IDataWarehouseAnalytics 130 - { 125 + class AnalyticsAdapterBridge implements IDataWarehouseAnalytics { 131 126 constructor( 132 127 private readonly provider: DataWarehouseProvider, 133 128 private readonly adapter: IAnalyticsAdapter, ··· 195 190 static createDataWarehouse(config: DataWarehouseConfig): IDataWarehouse { 196 191 switch (config.provider) { 197 192 case 'noop': 198 - return new WarehouseAdapterBridge( 199 - 'noop', 200 - new NoOpWarehouseAdapter(), 201 - ); 193 + return new WarehouseAdapterBridge('noop', new NoOpWarehouseAdapter()); 202 194 case 'clickhouse': 203 195 return new WarehouseAdapterBridge( 204 196 'clickhouse', ··· 207 199 }), 208 200 ); 209 201 case 'postgresql': 210 - return new WarehouseAdapterBridge('postgresql', new NoOpWarehouseAdapter()); 202 + return new WarehouseAdapterBridge( 203 + 'postgresql', 204 + new NoOpWarehouseAdapter(), 205 + ); 211 206 default: 212 207 return assertUnreachable( 213 208 config, 214 - `Unknown data warehouse provider: ${(config as DataWarehouseConfig).provider}`, 209 + `Unknown data warehouse provider: ${ 210 + (config as DataWarehouseConfig).provider 211 + }`, 215 212 ); 216 213 } 217 214 } ··· 232 229 default: 233 230 return assertUnreachable( 234 231 config, 235 - `Unknown data warehouse provider: ${(config as DataWarehouseConfig).provider}`, 232 + `Unknown data warehouse provider: ${ 233 + (config as DataWarehouseConfig).provider 234 + }`, 236 235 ); 237 236 } 238 237 } ··· 281 280 /** 282 281 * Create configuration from environment variables 283 282 */ 284 - // eslint-disable-next-line complexity 283 + 285 284 static createConfigFromEnv(): DataWarehouseConfig { 286 285 const provider = (process.env.WAREHOUSE_ADAPTER ?? 287 286 process.env.DATA_WAREHOUSE_PROVIDER ?? ··· 307 306 username: process.env.CLICKHOUSE_USERNAME ?? 'default', 308 307 password: process.env.CLICKHOUSE_PASSWORD ?? '', 309 308 database: process.env.CLICKHOUSE_DATABASE ?? 'default', 310 - protocol: (process.env.CLICKHOUSE_PROTOCOL ?? 'http') as 'http' | 'https', 309 + protocol: (process.env.CLICKHOUSE_PROTOCOL ?? 'http') as 310 + | 'http' 311 + | 'https', 311 312 }, 312 313 pool: { 313 314 max: process.env.CLICKHOUSE_POOL_SIZE ··· 337 338 } 338 339 } 339 340 } 340 -
+1 -1
server/test/extendExpect.ts
··· 52 52 keyof typeof propertyMatchers; 53 53 54 54 if (isJsonPath(k)) { 55 - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete 55 + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete, functional/immutable-data -- derived snapshot matchers 56 56 delete generatedPropertyMatchers[key]; 57 57 JSONPath({ path: k, json: received, resultType: 'path' }).forEach( 58 58 (pathStr: string) => {
+1 -1
server/test/mockHelpers/jestMocks.ts
··· 40 40 // instanceof checks still work. 41 41 const mock = Object.create(obj); 42 42 for (const k of keys) { 43 - // eslint-disable-next-line better-mutation/no-mutation 43 + // eslint-disable-next-line functional/immutable-data 44 44 mock[k] = jest.fn((obj[k] as AnyFn).bind(obj)); 45 45 } 46 46
+13 -16
server/test/setupMockedServer.ts
··· 7 7 8 8 import getBottle, { type Dependencies } from '../iocContainer/index.js'; 9 9 import makeServer from '../server.js'; 10 - import SafeTracer from '../utils/SafeTracer.js'; 11 10 import { type IDataWarehouse } from '../storage/dataWarehouse/IDataWarehouse.js'; 12 11 import type { IDataWarehouseAnalytics } from '../storage/dataWarehouse/IDataWarehouseAnalytics.js'; 12 + import SafeTracer from '../utils/SafeTracer.js'; 13 13 14 14 /** 15 15 * Occassionally, we make a request that's supposed to error, so this function 16 16 * lets us temporarily suppress console messages, to keep our output a bit nicer. 17 17 */ 18 18 export function disableConsoleLogging() { 19 - /* eslint-disable better-mutation/no-mutation */ 19 + /* eslint-disable functional/immutable-data */ 20 20 const noop = () => {}; 21 21 const { log, error } = console; 22 22 console.log = noop; ··· 25 25 console.log = log; 26 26 console.error = error; 27 27 }; 28 - /* eslint-enable better-mutation/no-mutation */ 28 + /* eslint-enable functional/immutable-data */ 29 29 } 30 30 31 31 export async function makeMockedServer() { ··· 40 40 41 41 // The mutation rule below is a false positive, as we're just doing 42 42 // initial setup on this mock object before exposing it. 43 - /* eslint-disable better-mutation/no-mutation */ 44 - const tracer = new SafeTracer(new otel.ProxyTracerProvider().getTracer('noop')); 43 + 44 + const tracer = new SafeTracer( 45 + new otel.ProxyTracerProvider().getTracer('noop'), 46 + ); 45 47 46 48 const queryMock = jest.fn( 47 - async ( 48 - _query: string, 49 - _tracer: SafeTracer, 50 - _binds?: readonly unknown[], 51 - ) => [] as unknown[], 49 + async (_query: string, _tracer: SafeTracer, _binds?: readonly unknown[]) => 50 + [] as unknown[], 52 51 ) as jest.MockedFunction<IDataWarehouse['query']>; 53 52 54 53 const transactionImpl: IDataWarehouse['transaction'] = async (fn) => ··· 58 57 59 58 const startMock = jest.fn(() => {}) as IDataWarehouse['start']; 60 59 const closeMock = jest.fn(async () => {}) as IDataWarehouse['close']; 61 - const getProviderMock = jest.fn(() => 'clickhouse') as IDataWarehouse['getProvider']; 60 + const getProviderMock = jest.fn( 61 + () => 'clickhouse', 62 + ) as IDataWarehouse['getProvider']; 62 63 63 64 const dataWarehouseMock: IDataWarehouse = { 64 65 query: queryMock, ··· 76 77 flushPendingWrites: jest.fn(async () => {}), 77 78 close: jest.fn(async () => {}), 78 79 } as unknown as jest.Mocked<IDataWarehouseAnalytics>; 79 - /* eslint-enable better-mutation/no-mutation */ 80 80 81 81 bottle.value('DataWarehouse', dataWarehouseMock); 82 - bottle.value( 83 - 'DataWarehouseAnalytics', 84 - analyticsMock, 85 - ); 82 + bottle.value('DataWarehouseAnalytics', analyticsMock); 86 83 bottle.value('Tracer', tracer); 87 84 return bottle.container as unknown as Omit< 88 85 Dependencies,
-1
server/test/stubs/getRuleAnomalyDetectionStatistics.ts
··· 1 1 import { readFileSync } from 'fs'; 2 2 import { dirname, join } from 'path'; 3 - // eslint-disable-next-line import/no-extraneous-dependencies 4 3 import yaml from 'js-yaml'; 5 4 6 5 import { type GetRuleAnomalyDetectionStatistics } from '../../services/ruleAnomalyDetectionService/index.js';
+4 -4
server/test/utils.ts
··· 156 156 skip: JestItCall; 157 157 todo: JestItCall; 158 158 }; 159 - // eslint-disable-next-line better-mutation/no-mutation 159 + // eslint-disable-next-line functional/immutable-data 160 160 fn.only = _makeTestWithFixture(makeSetupTeardown, it.only); 161 - // eslint-disable-next-line better-mutation/no-mutation 161 + // eslint-disable-next-line functional/immutable-data 162 162 fn.skip = _makeTestWithFixture(makeSetupTeardown, it.skip); 163 - // eslint-disable-next-line better-mutation/no-mutation 163 + // eslint-disable-next-line functional/immutable-data 164 164 fn.todo = _makeTestWithFixture(makeSetupTeardown, it.todo); 165 165 return fn; 166 166 } ··· 229 229 ): Awaited<U> | Promise<Awaited<U>> | void | Promise<void> { 230 230 try { 231 231 const res = getValue(); 232 - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 232 + 233 233 if (res && typeof res === 'object' && 'then' in res) { 234 234 return res.then(then, catcher) as Promise<Awaited<U>> | Promise<void>; 235 235 } else {
-1
server/utils/encoding.ts
··· 51 51 * @param it The value to stringify. 52 52 */ 53 53 export function jsonStringify<T>(it: T) { 54 - // eslint-disable-next-line no-restricted-syntax 55 54 return stringify(it) as JsonOf<T>; 56 55 } 57 56
+6 -7
server/utils/iterables.test.ts
··· 38 38 describe('chunkAsyncIterableByKey', () => { 39 39 it('should chunk numbers based on even and odd', async () => { 40 40 async function* sampleStream() { 41 - // eslint-disable-next-line better-mutation/no-mutation 42 41 for (let i = 1; i <= 5; i++) { 43 42 yield i; 44 43 yield i; 45 44 } 46 45 } 47 46 48 - const result: number[][] = []; 47 + let result: number[][] = []; 49 48 for await (const chunk of chunkAsyncIterableByKey( 50 49 sampleStream(), 51 50 (item) => item % 2 === 0, 52 51 )) { 53 - result.push(chunk); 52 + result = [...result, chunk]; 54 53 } 55 54 56 55 expect(result).toEqual([ ··· 65 64 it('should handle an empty stream', async () => { 66 65 async function* emptyStream() {} 67 66 68 - const result: number[][] = []; 67 + let result: number[][] = []; 69 68 for await (const chunk of chunkAsyncIterableByKey( 70 69 emptyStream(), 71 70 (it) => it, 72 71 )) { 73 - result.push(chunk); 72 + result = [...result, chunk]; 74 73 } 75 74 76 75 expect(result).toEqual([]); ··· 85 84 86 85 const chunkKey = (item: number | undefined) => item; 87 86 88 - const result: (number | undefined)[][] = []; 87 + let result: (number | undefined)[][] = []; 89 88 for await (const chunk of chunkAsyncIterableByKey( 90 89 sampleStream(), 91 90 chunkKey, 92 91 )) { 93 - result.push(chunk); 92 + result = [...result, chunk]; 94 93 } 95 94 96 95 expect(result).toEqual([[1], [undefined], [1]]);
-1
server/utils/language.ts
··· 1 - /* eslint-disable max-lines */ 2 1 import { makeEnumLike } from '@roostorg/types'; 3 2 4 3 // https://gist.github.com/jrnk/8eb57b065ea0b098d571
-1
server/utils/misc.ts
··· 202 202 } 203 203 204 204 export function removeUndefinedKeys<T extends object>(object: T) { 205 - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 206 205 return _.pickBy(object, (v) => v !== undefined); 207 206 } 208 207
-3
server/utils/url.test.ts
··· 6 6 // This absolutely is unsafe mutation of a global that'll be visible 7 7 // across test suites. However, this env var should only be relied upon 8 8 // by this module, so it should be ok. 9 - // eslint-disable-next-line better-mutation/no-mutation 10 9 process.env.ALLOW_USER_INPUT_LOCALHOST_URIS = 'false'; 11 10 }); 12 11 ··· 24 23 // This absolutely is unsafe mutation of a global that'll be visible 25 24 // across test suites. However, this env var should only be relied upon 26 25 // by this module, so it should be ok. 27 - // eslint-disable-next-line better-mutation/no-mutation 28 26 process.env.ALLOW_USER_INPUT_LOCALHOST_URIS = 'false'; 29 27 30 28 expect(() => validateUrl('https://localhost:3000')).toThrow(); ··· 37 35 // This absolutely is unsafe mutation of a global that'll be visible 38 36 // across test suites. However, this env var should only be relied upon 39 37 // by this module, so it should be ok. 40 - // eslint-disable-next-line better-mutation/no-mutation 41 38 process.env.ALLOW_USER_INPUT_LOCALHOST_URIS = 'true'; 42 39 }); 43 40
+58 -53
server/workers_jobs/RetryFailedNcmecDecisionsJob.ts
··· 1 - /* eslint-disable no-console */ 2 1 import { makeDateString } from '@roostorg/types'; 3 2 import _ from 'lodash'; 4 3 import { v1 as uuidv1 } from 'uuid'; ··· 163 162 threads: submitNcmecReportDecisionComponent.reportedMessages, 164 163 // Use the stored incidentType if available, otherwise default to the most common one 165 164 // The type says it's required, but old decisions in the DB won't have it 166 - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing 165 + 167 166 incidentType: 168 167 submitNcmecReportDecisionComponent.incidentType || 169 168 'Child Pornography (possession, manufacture, and distribution)', ··· 216 215 correlationId, 217 216 targetItem: { 218 217 itemId, 219 - itemType: { id: itemType.id, kind: itemType.kind, name: itemType.name }, 218 + itemType: { 219 + id: itemType.id, 220 + kind: itemType.kind, 221 + name: itemType.name, 222 + }, 220 223 }, 221 224 actorId: row.reviewer_id, 222 225 actorEmail: user?.email, ··· 241 244 return { 242 245 type: 'Job' as const, 243 246 async run() { 244 - // One month before now 245 - const startDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); 246 - // End date is 1 hour before now, to give currently running decisions time to finish. 247 - const endDate = new Date(Date.now() - 60 * 60 * 1000); 248 - const ncmecDecisions = await manualReviewToolService.getNcmecDecisions({ 249 - startDate, 250 - endDate, 251 - }); 252 - const usersWithReports = await ncmecService.getUsersWithNcmecDecision({ 253 - startDate, 254 - }); 255 - if (ncmecDecisions.length === 0) { 256 - return; 257 - } 258 - const previousErrors = await ncmecService.getNcmecErrorsForJobIds( 259 - ncmecDecisions.map((it) => it.job_payload.id), 260 - ); 261 - // Only retry decisions that don't have an applicable NCMEC decision and 262 - // decisions that don't already have a permanent error or a retry count of 263 - // 10 or more. 264 - const decisionsToRetry = ncmecDecisions.filter((it) => { 265 - return ( 266 - !usersWithReports.some( 267 - (usersWithReports) => 268 - usersWithReports.userId === it.job_payload.payload.item.itemId && 269 - usersWithReports.userItemTypeId === 270 - it.job_payload.payload.item.itemTypeIdentifier.id && 271 - usersWithReports.orgId === it.org_id, 272 - ) && 273 - !previousErrors.some( 274 - (error) => 275 - (error.job_id === it.job_payload.id && error.retry_count >= 10) || 276 - (error.job_id === it.job_payload.id && 277 - error.status === 'PERMANENT_ERROR'), 278 - ) 247 + // One month before now 248 + const startDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); 249 + // End date is 1 hour before now, to give currently running decisions time to finish. 250 + const endDate = new Date(Date.now() - 60 * 60 * 1000); 251 + const ncmecDecisions = await manualReviewToolService.getNcmecDecisions({ 252 + startDate, 253 + endDate, 254 + }); 255 + const usersWithReports = await ncmecService.getUsersWithNcmecDecision({ 256 + startDate, 257 + }); 258 + if (ncmecDecisions.length === 0) { 259 + return; 260 + } 261 + const previousErrors = await ncmecService.getNcmecErrorsForJobIds( 262 + ncmecDecisions.map((it) => it.job_payload.id), 279 263 ); 280 - }); 281 - const usersByOrg: { 282 - [key: string]: { 283 - id: string; 284 - email: string; 285 - firstName: string; 286 - lastName: string; 287 - role: string; 288 - }[]; 289 - } = {}; 290 - // Run this sequentially to avoid overloading external systems 291 - for (const row of decisionsToRetry) { 292 - await processDecisionRetry(row, usersByOrg); 293 - } 294 - }, 264 + // Only retry decisions that don't have an applicable NCMEC decision and 265 + // decisions that don't already have a permanent error or a retry count of 266 + // 10 or more. 267 + const decisionsToRetry = ncmecDecisions.filter((it) => { 268 + return ( 269 + !usersWithReports.some( 270 + (usersWithReports) => 271 + usersWithReports.userId === 272 + it.job_payload.payload.item.itemId && 273 + usersWithReports.userItemTypeId === 274 + it.job_payload.payload.item.itemTypeIdentifier.id && 275 + usersWithReports.orgId === it.org_id, 276 + ) && 277 + !previousErrors.some( 278 + (error) => 279 + (error.job_id === it.job_payload.id && 280 + error.retry_count >= 10) || 281 + (error.job_id === it.job_payload.id && 282 + error.status === 'PERMANENT_ERROR'), 283 + ) 284 + ); 285 + }); 286 + const usersByOrg: { 287 + [key: string]: { 288 + id: string; 289 + email: string; 290 + firstName: string; 291 + lastName: string; 292 + role: string; 293 + }[]; 294 + } = {}; 295 + // Run this sequentially to avoid overloading external systems 296 + for (const row of decisionsToRetry) { 297 + await processDecisionRetry(row, usersByOrg); 298 + } 299 + }, 295 300 async shutdown() { 296 301 await closeSharedResourcesForShutdown(); 297 302 },