Mirrored from GitHub github.com/roostorg/coop
0

Configure Feed

Select the types of activity you want to include in your feed.

chore: apply prettier globally and enforce it in CI (#834)

* chore: apply prettier formatting across the codebase

Apply `prettier --write` to all `{ts,tsx,js,jsx,mjs,cjs,json,md,yaml,yml}`
files in a single pass so the codebase matches the formatting enforced by
the pre-commit hook. This is a mechanical, whitespace-only change.

Closes #799 (formatting pass portion).

* ci: enforce prettier formatting in CI

Add a check_formatting job to apply_pr_checks.yaml that runs
`prettier --check` over the same file set the pre-commit hook formats,
via actions/setup-node (no Docker, so docker-compose.yaml is untouched).

Broaden the root `npm run format` script to the same
`{ts,tsx,js,jsx,mjs,cjs,json,md,yaml,yml}` glob so `npm run format`,
lint-staged, and CI all agree on scope.

Document the new job and its local reproduction command in AGENTS.md.

Closes #799 (CI enforcement portion).

* build: add prettier and prettier:fix scripts; use them in CI

Centralize the whole-repo Prettier glob into two root package.json scripts
so CI and local dev share one source of truth:

- `npm run prettier` -> prettier --check (used by the check_formatting CI job)
- `npm run prettier:fix` -> prettier --write
- `npm run format` -> alias of prettier:fix (kept for backwards compat)

CI now runs `npm run prettier` instead of duplicating the glob inline.
The pre-commit hook (lint-staged) intentionally keeps the scoped
`prettier --write` so it only touches staged files; using the whole-repo
glob there would reformat the entire codebase on every commit.

Update AGENTS.md, docs/development/local.md, and copilot-instructions.md
to reference the canonical script names.

+1699 -1343
+1 -1
.github/copilot-instructions.md
··· 19 19 - missing JSDoc on internal helpers 20 20 - subjective style preferences not codified in a project rule 21 21 22 - If a finding would be caught by `npm run lint` or `npm run format`, it's redundant. 22 + If a finding would be caught by `npm run lint` or `npm run prettier` (check) / `npm run prettier:fix` (alias `npm run format`), it's redundant. 23 23 24 24 ## Security (cross-cutting) 25 25
+21
.github/workflows/apply_pr_checks.yaml
··· 40 40 migrator: 41 41 - 'migrator/**' 42 42 43 + check_formatting: 44 + runs-on: ubuntu-latest 45 + timeout-minutes: 5 46 + steps: 47 + - name: Checkout code 48 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 49 + with: 50 + persist-credentials: false 51 + 52 + - name: Setup Node.js 53 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 54 + with: 55 + node-version-file: '.nvmrc' 56 + cache: 'npm' 57 + 58 + - name: Install dependencies 59 + run: npm ci 60 + 61 + - name: Check formatting 62 + run: npm run prettier 63 + 43 64 check_generated_graphql: 44 65 timeout-minutes: 4 45 66 needs: [changes]
+5 -3
AGENTS.md
··· 102 102 103 103 ```bash 104 104 npm run lint # lint all packages 105 - npm run format # format all packages 105 + npm run prettier:fix # format all packages (alias: npm run format) 106 106 (cd server && npm run lint) 107 107 (cd client && npm run lint) 108 108 ``` ··· 111 111 112 112 ## CI 113 113 114 - CI runs entirely via GitHub Actions (`.github/workflows/apply_pr_checks.yaml`). All PR checks are defined as `docker compose` services so you can reproduce any CI job locally. Run them in your shell (paste-as-is — each command's exit code matches the corresponding CI step's exit code): 114 + CI runs entirely via GitHub Actions (`.github/workflows/apply_pr_checks.yaml`). Most PR checks are defined as `docker compose` services so you can reproduce any CI job locally; the formatting check runs directly via `actions/setup-node`. Run them in your shell (paste-as-is — each command's exit code matches the corresponding CI step's exit code): 115 115 116 116 ```bash 117 117 docker compose run --rm codegen-check ··· 120 120 docker compose run --rm client npm run lint 121 121 docker compose run --rm client npm run build 122 122 docker compose run --rm test 123 + npm ci && npm run prettier 123 124 ``` 124 125 125 126 Individual checks: 126 127 127 128 | CI job | Local command | 128 129 | ---------------------------------------- | ----------------------------------------------- | 130 + | `check_formatting` | `npm ci && npm run prettier` | 129 131 | `check_generated_graphql` | `docker compose run --rm codegen-check` | 130 132 | `check_api_server` (lint) | `docker compose run --rm backend npm run lint` | 131 133 | `check_api_server` (build) | `docker compose run --rm backend npm run build` | ··· 159 161 160 162 ## Code style 161 163 162 - - **TypeScript:** ESLint + Prettier (configs in `.eslintrc.cjs` and `.prettierrc` per package). Run `npm run lint` and `npm run format` from root. 164 + - **TypeScript:** ESLint + Prettier (Prettier config at root `.prettierrc`; ESLint configs per package in `server/` and `client/`). Run `npm run lint` and `npm run prettier:fix` from root. 163 165 - **Naming:** Use camelCase for variables/functions; PascalCase for components/classes; SCREAMING_SNAKE_CASE for constants. 164 166 - **GraphQL:** Type-safe resolvers and queries via codegen; never hand-edit `generated.ts`. 165 167 - **Imports:** Absolute imports configured via `tsconfig.json` paths; prefer `@/` prefix over relative paths where configured.
+3 -5
client/.eslintrc.cjs
··· 2 2 { 3 3 selector: 4 4 ':matches(TSTypeAliasDeclaration[id.name=Props], TSInterfaceDeclaration[id.name=Props])', 5 - message: 6 - 'React types for props should be inlined', 5 + message: 'React types for props should be inlined', 7 6 }, 8 7 { 9 8 selector: ··· 33 32 'tailwind.config.js', 34 33 '*.stories.tsx', 35 34 'vite.config.ts', 36 - 'vite-env.d.ts' 35 + 'vite-env.d.ts', 37 36 ], 38 37 plugins: ['@typescript-eslint', 'custom-rules'], 39 38 rules: { ··· 130 129 { 131 130 object: 'window', 132 131 property: 'open', 133 - message: 134 - 'Use an <a> tag to open links, rather than window.open.', 132 + message: 'Use an <a> tag to open links, rather than window.open.', 135 133 }, 136 134 { 137 135 object: '_',
+12 -9
client/eslint/index.js
··· 1 - const fs = require("fs"); 2 - const path = require("path"); 1 + const fs = require('fs'); 2 + const path = require('path'); 3 3 4 - const ruleFiles = fs 5 - .readdirSync(__dirname) 6 - .filter((file) => { 7 - const fullPath = path.join(__dirname, file); 8 - return !fs.statSync(fullPath).isDirectory() && file.endsWith(".js") && file !== "index.js" && !file.endsWith("test.js"); 9 - }); 4 + const ruleFiles = fs.readdirSync(__dirname).filter((file) => { 5 + const fullPath = path.join(__dirname, file); 6 + return ( 7 + !fs.statSync(fullPath).isDirectory() && 8 + file.endsWith('.js') && 9 + file !== 'index.js' && 10 + !file.endsWith('test.js') 11 + ); 12 + }); 10 13 11 14 const rules = Object.fromEntries( 12 - ruleFiles.map((file) => [path.basename(file, ".js"), require("./" + file)]) 15 + ruleFiles.map((file) => [path.basename(file, '.js'), require('./' + file)]), 13 16 ); 14 17 15 18 module.exports = { rules };
+1 -1
client/eslint/no-casting-in-getFieldValueForRole.js
··· 22 22 getFieldValueForRole< 23 23 GQLSchemaFieldRoles, 24 24 keyof GQLSchemaFieldRoles 25 - >(reportedItem, 'displayName')` 25 + >(reportedItem, 'displayName')`, 26 26 }); 27 27 } 28 28
+18 -18
client/src/components/ActionParameterInputs.tsx
··· 3 3 import { useMemo } from 'react'; 4 4 5 5 import { 6 - type GQLActionParameter, 7 6 GQLActionParameterType, 7 + type GQLActionParameter, 8 8 } from '../graphql/generated'; 9 9 10 10 const { Option } = Select; ··· 260 260 (Array.isArray(value) && value.length === 0); 261 261 // A `defaultValue` on the spec satisfies "required" since the server will 262 262 // backfill on publish — keeps the UX consistent with server behavior. 263 - if (isEmpty && (param.defaultValue === undefined || param.defaultValue === null)) { 263 + if ( 264 + isEmpty && 265 + (param.defaultValue === undefined || param.defaultValue === null) 266 + ) { 264 267 missing.push(param.displayName); 265 268 } 266 269 } ··· 273 276 * replaced. Tiny but used in three call sites. 274 277 */ 275 278 export function useUpdateActionValues( 276 - setMap: ( 277 - next: Readonly<Record<string, ActionParameterValues>>, 278 - ) => void, 279 + setMap: (next: Readonly<Record<string, ActionParameterValues>>) => void, 279 280 map: Readonly<Record<string, ActionParameterValues>>, 280 281 ) { 281 282 return useMemo( 282 - () => 283 - (actionId: string, values: ActionParameterValues) => { 284 - // Drop the entry entirely when the values map is empty, so the GQL 285 - // input doesn't carry meaningless `{}` entries that confuse log 286 - // readers. 287 - if (Object.keys(values).length === 0) { 288 - if (!(actionId in map)) return; 289 - const { [actionId]: _omitted, ...rest } = map; 290 - setMap(rest); 291 - return; 292 - } 293 - setMap({ ...map, [actionId]: values }); 294 - }, 283 + () => (actionId: string, values: ActionParameterValues) => { 284 + // Drop the entry entirely when the values map is empty, so the GQL 285 + // input doesn't carry meaningless `{}` entries that confuse log 286 + // readers. 287 + if (Object.keys(values).length === 0) { 288 + if (!(actionId in map)) return; 289 + const { [actionId]: _omitted, ...rest } = map; 290 + setMap(rest); 291 + return; 292 + } 293 + setMap({ ...map, [actionId]: values }); 294 + }, 295 295 [map, setMap], 296 296 ); 297 297 }
+7 -3
client/src/components/ActionParametersModal.tsx
··· 1 + import { type GQLActionParameter } from '@/graphql/generated'; 1 2 import { Tooltip } from 'antd'; 2 3 import { useEffect, useState } from 'react'; 3 4 4 5 import CoopModal from '@/webpages/dashboard/components/CoopModal'; 5 6 import { type CoopModalFooterButtonProps } from '@/webpages/dashboard/components/CoopModalFooter'; 6 - import { type GQLActionParameter } from '@/graphql/generated'; 7 7 8 8 import ActionParameterInputs, { 9 + findMissingRequiredParameters, 9 10 type ActionParameterValues, 10 - findMissingRequiredParameters, 11 11 } from './ActionParameterInputs'; 12 12 13 13 type Props = { ··· 88 88 {!canSave && ( 89 89 <Tooltip title={`Missing: ${missing.join(', ')}`}> 90 90 <div className="mt-3 text-xs text-coop-alert-red"> 91 - Fill in {missing.length === 1 ? 'the required field' : 'all required fields'} to continue. 91 + Fill in{' '} 92 + {missing.length === 1 93 + ? 'the required field' 94 + : 'all required fields'}{' '} 95 + to continue. 92 96 </div> 93 97 </Tooltip> 94 98 )}
+2 -2
client/src/components/common/StepProgressIndicator.tsx
··· 73 73 index < currentStepIndex 74 74 ? 'text-coop-blue' 75 75 : index === currentStepIndex 76 - ? 'text-gray-600' 77 - : 'text-gray-400' 76 + ? 'text-gray-600' 77 + : 'text-gray-400' 78 78 }`} 79 79 > 80 80 {step.name}
+2 -1
client/src/coop-ui/Badge.tsx
··· 29 29 ); 30 30 31 31 export interface BadgeProps 32 - extends React.HTMLAttributes<HTMLDivElement>, 32 + extends 33 + React.HTMLAttributes<HTMLDivElement>, 33 34 VariantProps<typeof badgeVariants> {} 34 35 35 36 function Badge({ className, variant, size, ...props }: BadgeProps) {
+51 -26
client/src/coop-ui/Button.tsx
··· 36 36 { 37 37 variant: 'default', 38 38 color: 'gray', 39 - class: 'bg-gray-800 text-white hover:bg-gray-900 focus:bg-gray-900 dark:bg-white dark:text-neutral-800', 39 + class: 40 + 'bg-gray-800 text-white hover:bg-gray-900 focus:bg-gray-900 dark:bg-white dark:text-neutral-800', 40 41 }, 41 42 { 42 43 variant: 'default', 43 44 color: 'indigo', 44 - class: 'bg-indigo-500 text-white hover:bg-indigo-700 focus:bg-indigo-700', 45 + class: 46 + 'bg-indigo-500 text-white hover:bg-indigo-700 focus:bg-indigo-700', 45 47 }, 46 48 { 47 49 variant: 'default', ··· 51 53 { 52 54 variant: 'default', 53 55 color: 'yellow', 54 - class: 'bg-yellow-500 text-white hover:bg-yellow-700 focus:bg-yellow-700', 56 + class: 57 + 'bg-yellow-500 text-white hover:bg-yellow-700 focus:bg-yellow-700', 55 58 }, 56 59 { 57 60 variant: 'default', ··· 63 66 { 64 67 variant: 'outline', 65 68 color: 'gray', 66 - class: 'border-gray-600 text-gray-600 hover:bg-gray-50 hover:border-gray-800 hover:text-gray-800 focus:border-gray-800 focus:text-gray-800 dark:border-gray-500 dark:text-gray-500 dark:hover:border-gray-700 dark:hover:text-gray-700 dark:focus:border-gray-700 dark:focus:text-gray-700', 69 + class: 70 + 'border-gray-600 text-gray-600 hover:bg-gray-50 hover:border-gray-800 hover:text-gray-800 focus:border-gray-800 focus:text-gray-800 dark:border-gray-500 dark:text-gray-500 dark:hover:border-gray-700 dark:hover:text-gray-700 dark:focus:border-gray-700 dark:focus:text-gray-700', 67 71 }, 68 72 { 69 73 variant: 'outline', 70 74 color: 'indigo', 71 - class: 'border-indigo-600 text-indigo-600 hover:bg-indigo-50 hover:border-indigo-800 hover:text-indigo-800 focus:border-indigo-800 focus:text-indigo-800 dark:border-indigo-500 dark:text-indigo-500 dark:hover:border-indigo-700 dark:hover:text-indigo-700 dark:focus:border-indigo-700 dark:focus:text-indigo-700', 75 + class: 76 + 'border-indigo-600 text-indigo-600 hover:bg-indigo-50 hover:border-indigo-800 hover:text-indigo-800 focus:border-indigo-800 focus:text-indigo-800 dark:border-indigo-500 dark:text-indigo-500 dark:hover:border-indigo-700 dark:hover:text-indigo-700 dark:focus:border-indigo-700 dark:focus:text-indigo-700', 72 77 }, 73 78 { 74 79 variant: 'outline', 75 80 color: 'red', 76 - class: 'border-red-600 text-red-600 hover:bg-red-50 hover:border-red-800 hover:text-red-800 focus:border-red-800 focus:text-red-800 dark:border-red-500 dark:text-red-500 dark:hover:border-red-700 dark:hover:text-red-700 dark:focus:border-red-700 dark:focus:text-red-700', 81 + class: 82 + 'border-red-600 text-red-600 hover:bg-red-50 hover:border-red-800 hover:text-red-800 focus:border-red-800 focus:text-red-800 dark:border-red-500 dark:text-red-500 dark:hover:border-red-700 dark:hover:text-red-700 dark:focus:border-red-700 dark:focus:text-red-700', 77 83 }, 78 84 { 79 85 variant: 'outline', 80 86 color: 'yellow', 81 - class: 'border-yellow-600 text-yellow-600 hover:bg-yellow-50 hover:border-yellow-800 hover:text-yellow-800 focus:border-yellow-800 focus:text-yellow-800 dark:border-yellow-500 dark:text-yellow-500 dark:hover:border-yellow-700 dark:hover:text-yellow-700 dark:focus:border-yellow-700 dark:focus:text-yellow-700', 87 + class: 88 + 'border-yellow-600 text-yellow-600 hover:bg-yellow-50 hover:border-yellow-800 hover:text-yellow-800 focus:border-yellow-800 focus:text-yellow-800 dark:border-yellow-500 dark:text-yellow-500 dark:hover:border-yellow-700 dark:hover:text-yellow-700 dark:focus:border-yellow-700 dark:focus:text-yellow-700', 82 89 }, 83 90 { 84 91 variant: 'outline', 85 92 color: 'teal', 86 - class: 'border-teal-600 text-teal-600 hover:bg-teal-50 hover:border-teal-800 hover:text-teal-800 focus:border-teal-800 focus:text-teal-800 dark:border-teal-500 dark:text-teal-500 dark:hover:border-teal-700 dark:hover:text-teal-700 dark:focus:border-teal-700 dark:focus:text-teal-700', 93 + class: 94 + 'border-teal-600 text-teal-600 hover:bg-teal-50 hover:border-teal-800 hover:text-teal-800 focus:border-teal-800 focus:text-teal-800 dark:border-teal-500 dark:text-teal-500 dark:hover:border-teal-700 dark:hover:text-teal-700 dark:focus:border-teal-700 dark:focus:text-teal-700', 87 95 }, 88 96 89 97 // Ghost variant compounds 90 98 { 91 99 variant: 'ghost', 92 100 color: 'gray', 93 - class: 'text-gray-600 hover:bg-gray-100 hover:text-gray-800 focus:bg-gray-100 focus:text-gray-800 dark:text-gray-500 dark:hover:bg-gray-800/30 dark:hover:text-gray-400 dark:focus:bg-gray-800/30 dark:focus:text-gray-400', 101 + class: 102 + 'text-gray-600 hover:bg-gray-100 hover:text-gray-800 focus:bg-gray-100 focus:text-gray-800 dark:text-gray-500 dark:hover:bg-gray-800/30 dark:hover:text-gray-400 dark:focus:bg-gray-800/30 dark:focus:text-gray-400', 94 103 }, 95 104 { 96 105 variant: 'ghost', 97 106 color: 'indigo', 98 - class: 'text-indigo-600 hover:bg-indigo-100 hover:text-indigo-800 focus:bg-indigo-100 focus:text-indigo-800 dark:text-indigo-500 dark:hover:bg-indigo-800/30 dark:hover:text-indigo-400 dark:focus:bg-indigo-800/30 dark:focus:text-indigo-400', 107 + class: 108 + 'text-indigo-600 hover:bg-indigo-100 hover:text-indigo-800 focus:bg-indigo-100 focus:text-indigo-800 dark:text-indigo-500 dark:hover:bg-indigo-800/30 dark:hover:text-indigo-400 dark:focus:bg-indigo-800/30 dark:focus:text-indigo-400', 99 109 }, 100 110 { 101 111 variant: 'ghost', 102 112 color: 'red', 103 - class: 'text-red-600 hover:bg-red-100 hover:text-red-800 focus:bg-red-100 focus:text-red-800 dark:text-red-500 dark:hover:bg-red-800/30 dark:hover:text-red-400 dark:focus:bg-red-800/30 dark:focus:text-red-400', 113 + class: 114 + 'text-red-600 hover:bg-red-100 hover:text-red-800 focus:bg-red-100 focus:text-red-800 dark:text-red-500 dark:hover:bg-red-800/30 dark:hover:text-red-400 dark:focus:bg-red-800/30 dark:focus:text-red-400', 104 115 }, 105 116 { 106 117 variant: 'ghost', 107 118 color: 'yellow', 108 - class: 'text-yellow-600 hover:bg-yellow-100 hover:text-yellow-800 focus:bg-yellow-100 focus:text-yellow-800 dark:text-yellow-500 dark:hover:bg-yellow-800/30 dark:hover:text-yellow-400 dark:focus:bg-yellow-800/30 dark:focus:text-yellow-400', 119 + class: 120 + 'text-yellow-600 hover:bg-yellow-100 hover:text-yellow-800 focus:bg-yellow-100 focus:text-yellow-800 dark:text-yellow-500 dark:hover:bg-yellow-800/30 dark:hover:text-yellow-400 dark:focus:bg-yellow-800/30 dark:focus:text-yellow-400', 109 121 }, 110 122 { 111 123 variant: 'ghost', 112 124 color: 'teal', 113 - class: 'text-teal-600 hover:bg-teal-100 hover:text-teal-800 focus:bg-teal-100 focus:text-teal-800 dark:text-teal-500 dark:hover:bg-teal-800/30 dark:hover:text-teal-400 dark:focus:bg-teal-800/30 dark:focus:text-teal-400', 125 + class: 126 + 'text-teal-600 hover:bg-teal-100 hover:text-teal-800 focus:bg-teal-100 focus:text-teal-800 dark:text-teal-500 dark:hover:bg-teal-800/30 dark:hover:text-teal-400 dark:focus:bg-teal-800/30 dark:focus:text-teal-400', 114 127 }, 115 128 116 129 // Soft variant compounds 117 130 { 118 131 variant: 'soft', 119 132 color: 'gray', 120 - class: 'bg-gray-100 text-gray-900 hover:bg-gray-200 focus:bg-gray-200 dark:text-gray-400 dark:hover:bg-gray-900 dark:focus:bg-gray-900', 133 + class: 134 + 'bg-gray-100 text-gray-900 hover:bg-gray-200 focus:bg-gray-200 dark:text-gray-400 dark:hover:bg-gray-900 dark:focus:bg-gray-900', 121 135 }, 122 136 { 123 137 variant: 'soft', 124 138 color: 'indigo', 125 - class: 'bg-indigo-100 text-indigo-900 hover:bg-indigo-200 focus:bg-indigo-200 dark:text-indigo-400 dark:hover:bg-indigo-900 dark:focus:bg-indigo-900', 139 + class: 140 + 'bg-indigo-100 text-indigo-900 hover:bg-indigo-200 focus:bg-indigo-200 dark:text-indigo-400 dark:hover:bg-indigo-900 dark:focus:bg-indigo-900', 126 141 }, 127 142 { 128 143 variant: 'soft', 129 144 color: 'red', 130 - class: 'bg-red-100 text-red-900 hover:bg-red-200 focus:bg-red-200 dark:text-red-400 dark:hover:bg-red-900 dark:focus:bg-red-900', 145 + class: 146 + 'bg-red-100 text-red-900 hover:bg-red-200 focus:bg-red-200 dark:text-red-400 dark:hover:bg-red-900 dark:focus:bg-red-900', 131 147 }, 132 148 { 133 149 variant: 'soft', 134 150 color: 'yellow', 135 - class: 'bg-yellow-100 text-yellow-900 hover:bg-yellow-200 focus:bg-yellow-200 dark:text-yellow-400 dark:hover:bg-yellow-900 dark:focus:bg-yellow-900', 151 + class: 152 + 'bg-yellow-100 text-yellow-900 hover:bg-yellow-200 focus:bg-yellow-200 dark:text-yellow-400 dark:hover:bg-yellow-900 dark:focus:bg-yellow-900', 136 153 }, 137 154 { 138 155 variant: 'soft', 139 156 color: 'teal', 140 - class: 'bg-teal-100 text-teal-900 hover:bg-teal-200 focus:bg-teal-200 dark:text-teal-400 dark:hover:bg-teal-900 dark:focus:bg-teal-900', 157 + class: 158 + 'bg-teal-100 text-teal-900 hover:bg-teal-200 focus:bg-teal-200 dark:text-teal-400 dark:hover:bg-teal-900 dark:focus:bg-teal-900', 141 159 }, 142 160 143 161 // White variant compounds ··· 171 189 { 172 190 variant: 'link', 173 191 color: 'gray', 174 - class: 'text-gray-600 hover:text-gray-800 focus:text-gray-800 dark:text-gray-500 dark:hover:text-gray-400 dark:focus:text-gray-400', 192 + class: 193 + 'text-gray-600 hover:text-gray-800 focus:text-gray-800 dark:text-gray-500 dark:hover:text-gray-400 dark:focus:text-gray-400', 175 194 }, 176 195 { 177 196 variant: 'link', 178 197 color: 'indigo', 179 - class: 'text-indigo-600 hover:text-indigo-800 focus:text-indigo-800 dark:text-indigo-500 dark:hover:text-indigo-400 dark:focus:text-indigo-400', 198 + class: 199 + 'text-indigo-600 hover:text-indigo-800 focus:text-indigo-800 dark:text-indigo-500 dark:hover:text-indigo-400 dark:focus:text-indigo-400', 180 200 }, 181 201 { 182 202 variant: 'link', 183 203 color: 'red', 184 - class: 'text-red-600 hover:text-red-800 focus:text-red-800 dark:text-red-500 dark:hover:text-red-400 dark:focus:text-red-400', 204 + class: 205 + 'text-red-600 hover:text-red-800 focus:text-red-800 dark:text-red-500 dark:hover:text-red-400 dark:focus:text-red-400', 185 206 }, 186 207 { 187 208 variant: 'link', 188 209 color: 'yellow', 189 - class: 'text-yellow-600 hover:text-yellow-800 focus:text-yellow-800 dark:text-yellow-500 dark:hover:text-yellow-400 dark:focus:text-yellow-400', 210 + class: 211 + 'text-yellow-600 hover:text-yellow-800 focus:text-yellow-800 dark:text-yellow-500 dark:hover:text-yellow-400 dark:focus:text-yellow-400', 190 212 }, 191 213 { 192 214 variant: 'link', 193 215 color: 'teal', 194 - class: 'text-teal-600 hover:text-teal-800 focus:text-teal-800 dark:text-teal-500 dark:hover:text-teal-400 dark:focus:text-teal-400', 216 + class: 217 + 'text-teal-600 hover:text-teal-800 focus:text-teal-800 dark:text-teal-500 dark:hover:text-teal-400 dark:focus:text-teal-400', 195 218 }, 196 219 ], 197 220 defaultVariants: { ··· 206 229 export type ButtonColor = VariantProps<typeof buttonVariants>['color']; 207 230 export type ButtonSize = VariantProps<typeof buttonVariants>['size']; 208 231 209 - export interface ButtonProps 210 - extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'color'> { 232 + export interface ButtonProps extends Omit< 233 + React.ButtonHTMLAttributes<HTMLButtonElement>, 234 + 'color' 235 + > { 211 236 variant?: ButtonVariant; 212 237 color?: ButtonColor; 213 238 size?: ButtonSize; ··· 236 261 const Comp = asChild ? Slot : 'button'; 237 262 238 263 const finalColor = 239 - variant === 'white' && !color ? 'gray' : color ?? 'indigo'; 264 + variant === 'white' && !color ? 'gray' : (color ?? 'indigo'); 240 265 241 266 const content = ( 242 267 <>
+1 -1
client/src/coop-ui/Calendar.stories.tsx
··· 1 1 import { Calendar, CalendarProps } from '@/coop-ui/Calendar'; 2 - import { action } from 'storybook/actions'; 3 2 import { Meta, StoryFn } from '@storybook/react'; 4 3 import * as React from 'react'; 4 + import { action } from 'storybook/actions'; 5 5 6 6 export default { 7 7 title: 'Components/Calendar',
+4 -5
client/src/coop-ui/Checkbox.tsx
··· 6 6 // Note: indeterminate state can be added back once there is a need for it 7 7 // it's been removed due to the need to cast the value to boolean 8 8 // when using this component 9 - interface CheckboxProps 10 - extends Omit< 11 - React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>, 12 - 'onCheckedChange' 13 - > { 9 + interface CheckboxProps extends Omit< 10 + React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>, 11 + 'onCheckedChange' 12 + > { 14 13 onCheckedChange?: (checked: boolean) => void; 15 14 } 16 15
+5 -6
client/src/coop-ui/Input.tsx
··· 1 1 import { cn } from '@/lib/utils'; 2 2 import * as React from 'react'; 3 3 4 - export interface InputProps 5 - extends React.InputHTMLAttributes<HTMLInputElement> { 4 + export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> { 6 5 startSlot?: React.ReactNode; 7 6 endSlot?: React.ReactNode; 8 7 } ··· 17 16 startSlot && endSlot 18 17 ? 'rounded-none' 19 18 : startSlot 20 - ? 'rounded-r-lg rounded-l-none' 21 - : endSlot 22 - ? 'rounded-l-lg rounded-r-none' 23 - : 'rounded-lg', 19 + ? 'rounded-r-lg rounded-l-none' 20 + : endSlot 21 + ? 'rounded-l-lg rounded-r-none' 22 + : 'rounded-lg', 24 23 className, 25 24 ); 26 25
+9 -3
client/src/coop-ui/Select.stories.tsx
··· 166 166 render: () => ( 167 167 <div className="flex flex-col space-y-4"> 168 168 <div className="flex items-center space-x-4"> 169 - <Text size="XS" className="w-20">Small:</Text> 169 + <Text size="XS" className="w-20"> 170 + Small: 171 + </Text> 170 172 <Select> 171 173 <SelectTrigger className="w-[180px]" size="small"> 172 174 <SelectValue placeholder="Small select" /> ··· 182 184 </div> 183 185 184 186 <div className="flex items-center space-x-4"> 185 - <Text size="XS" className="w-20">Medium (default):</Text> 187 + <Text size="XS" className="w-20"> 188 + Medium (default): 189 + </Text> 186 190 <Select> 187 191 <SelectTrigger className="w-[180px]" size="medium"> 188 192 <SelectValue placeholder="Medium select" /> ··· 198 202 </div> 199 203 200 204 <div className="flex items-center space-x-4"> 201 - <Text size="XS" className="w-20">Large:</Text> 205 + <Text size="XS" className="w-20"> 206 + Large: 207 + </Text> 202 208 <Select> 203 209 <SelectTrigger className="w-[180px]" size="large"> 204 210 <SelectValue placeholder="Large select" />
+3 -2
client/src/coop-ui/Switch.tsx
··· 3 3 import { Check, X } from 'lucide-react'; 4 4 import * as React from 'react'; 5 5 6 - interface SwitchProps 7 - extends React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> { 6 + interface SwitchProps extends React.ComponentPropsWithoutRef< 7 + typeof SwitchPrimitives.Root 8 + > { 8 9 size?: 'small'; //| 'medium' | 'large'; 9 10 } 10 11
+5 -6
client/src/coop-ui/Textarea.tsx
··· 1 1 import { cn } from '@/lib/utils'; 2 2 import * as React from 'react'; 3 3 4 - export interface TextareaProps 5 - extends React.TextareaHTMLAttributes<HTMLTextAreaElement> { 4 + export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> { 6 5 startSlot?: React.ReactNode; 7 6 endSlot?: React.ReactNode; 8 7 } ··· 17 16 startSlot && endSlot 18 17 ? 'rounded-none' 19 18 : startSlot 20 - ? 'rounded-r-lg rounded-l-none' 21 - : endSlot 22 - ? 'rounded-l-lg rounded-r-none' 23 - : 'rounded-lg', 19 + ? 'rounded-r-lg rounded-l-none' 20 + : endSlot 21 + ? 'rounded-l-lg rounded-r-none' 22 + : 'rounded-lg', 24 23 className, 25 24 ); 26 25
+2 -4
client/src/coop-ui/Typography.tsx
··· 45 45 }); 46 46 47 47 interface TextProps 48 - extends React.HTMLAttributes<HTMLElement>, 49 - VariantProps<typeof textVariants> { 48 + extends React.HTMLAttributes<HTMLElement>, VariantProps<typeof textVariants> { 50 49 as?: 'span' | 'div' | 'label' | 'p'; 51 50 size?: TextSize; 52 51 weight?: TextWeight; ··· 66 65 ); 67 66 68 67 interface HeadingProps 69 - extends React.HTMLAttributes<HTMLElement>, 70 - VariantProps<typeof textVariants> { 68 + extends React.HTMLAttributes<HTMLElement>, VariantProps<typeof textVariants> { 71 69 as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; 72 70 size?: TextSize; 73 71 weight?: TextWeight;
+6 -6
client/src/graphql/inputHelpers.ts
··· 72 72 Array.isArray(it) 73 73 ? it.map(stripTypename) 74 74 : isPlainObject(it) 75 - ? mapValues(omit(it, '__typename'), stripTypename) 76 - : it 75 + ? mapValues(omit(it, '__typename'), stripTypename) 76 + : it 77 77 ) as WithoutTypename<T>; 78 78 } 79 79 80 80 export type WithoutTypename<T> = T extends (infer U)[] 81 81 ? WithoutTypename<U>[] 82 82 : T extends (...args: any[]) => any 83 - ? T 84 - : T extends object 85 - ? Omit<{ [K in keyof T]: WithoutTypename<T[K]> }, '__typename'> 86 - : T; 83 + ? T 84 + : T extends object 85 + ? Omit<{ [K in keyof T]: WithoutTypename<T[K]> }, '__typename'> 86 + : T;
+9 -3
client/src/graphql/operations/hashBanks.ts
··· 132 132 `; 133 133 134 134 export const UPDATE_EXCHANGE_CREDENTIALS_MUTATION = gql` 135 - mutation UpdateExchangeCredentials($apiName: String!, $credentialsJson: String!) { 136 - updateExchangeCredentials(apiName: $apiName, credentialsJson: $credentialsJson) 135 + mutation UpdateExchangeCredentials( 136 + $apiName: String! 137 + $credentialsJson: String! 138 + ) { 139 + updateExchangeCredentials( 140 + apiName: $apiName 141 + credentialsJson: $credentialsJson 142 + ) 137 143 } 138 - `; 144 + `;
+8 -3
client/src/rechartsScaleWrapper.js
··· 19 19 */ 20 20 function fallbackTicks(domain, tickCount) { 21 21 const count = 22 - Number.isInteger(tickCount) && tickCount > 0 ? tickCount : DEFAULT_TICK_COUNT; 22 + Number.isInteger(tickCount) && tickCount > 0 23 + ? tickCount 24 + : DEFAULT_TICK_COUNT; 23 25 const min = typeof domain[0] === 'number' ? domain[0] : 0; 24 - const max = typeof domain[1] === 'number' && domain[1] > min ? domain[1] : min + 1; 26 + const max = 27 + typeof domain[1] === 'number' && domain[1] > min ? domain[1] : min + 1; 25 28 if (count === 1) return [min]; 26 29 const step = (max - min) / (count - 1); 27 30 const ticks = new Array(count); ··· 37 40 // for older versions that don't set `name`. 38 41 return ( 39 42 (err && err.name === 'DecimalError') || 40 - (err && typeof err.message === 'string' && err.message.includes('Division by zero')) 43 + (err && 44 + typeof err.message === 'string' && 45 + err.message.includes('Division by zero')) 41 46 ); 42 47 } 43 48
+2 -2
client/src/utils/typescript-types.ts
··· 35 35 export type If<Cond extends boolean, IfTrue, IfFalse> = Cond extends true 36 36 ? IfTrue 37 37 : Cond extends false 38 - ? IfFalse 39 - : IfTrue | IfFalse; 38 + ? IfFalse 39 + : IfTrue | IfFalse;
-1
client/src/webpages/auth/SignUp.tsx
··· 279 279 </div> 280 280 ); 281 281 } 282 -
+33 -12
client/src/webpages/dashboard/actions/ActionParametersEditor.tsx
··· 1 - import { ChevronsUpDown, Plus, Trash2 } from 'lucide-react'; 2 - import { useId, useMemo } from 'react'; 3 - 4 1 import { Button } from '@/coop-ui/Button'; 5 2 import { Checkbox } from '@/coop-ui/Checkbox'; 6 3 import { Input } from '@/coop-ui/Input'; ··· 15 12 } from '@/coop-ui/Select'; 16 13 import { Switch } from '@/coop-ui/Switch'; 17 14 import { cn } from '@/lib/utils'; 15 + import { ChevronsUpDown, Plus, Trash2 } from 'lucide-react'; 16 + import { useId, useMemo } from 'react'; 18 17 19 18 import { 19 + GQLActionParameterType, 20 20 type GQLActionParameterInput, 21 21 type GQLActionParameterOptionInput, 22 - GQLActionParameterType, 23 22 } from '../../../graphql/generated'; 24 23 25 24 // Mirror of the server-side pattern in `actionParametersValidation.ts`. Allows ··· 134 133 return ( 135 134 <div className="rounded-xl border border-gray-200 p-4"> 136 135 <div className="grid grid-cols-1 gap-x-3 gap-y-3 md:grid-cols-12"> 137 - <Field label="Name (key)" htmlFor={`${id}-name`} className="md:col-span-4"> 136 + <Field 137 + label="Name (key)" 138 + htmlFor={`${id}-name`} 139 + className="md:col-span-4" 140 + > 138 141 <Input 139 142 id={`${id}-name`} 140 143 value={param.name} ··· 306 309 if (isNumber) { 307 310 return ( 308 311 <> 309 - <Field label="Min (optional)" htmlFor={`${id}-min`} className="md:col-span-3"> 312 + <Field 313 + label="Min (optional)" 314 + htmlFor={`${id}-min`} 315 + className="md:col-span-3" 316 + > 310 317 <NumberInput 311 318 id={`${id}-min`} 312 319 value={param.min} ··· 314 321 onChange={(min) => onChange({ min })} 315 322 /> 316 323 </Field> 317 - <Field label="Max (optional)" htmlFor={`${id}-max`} className="md:col-span-3"> 324 + <Field 325 + label="Max (optional)" 326 + htmlFor={`${id}-max`} 327 + className="md:col-span-3" 328 + > 318 329 <NumberInput 319 330 id={`${id}-max`} 320 331 value={param.max} ··· 341 352 }) { 342 353 switch (param.type) { 343 354 case GQLActionParameterType.String: { 344 - const value = typeof param.defaultValue === 'string' ? param.defaultValue : ''; 355 + const value = 356 + typeof param.defaultValue === 'string' ? param.defaultValue : ''; 345 357 return ( 346 358 <Input 347 359 id={id} ··· 654 666 size="icon" 655 667 disabled={disabled} 656 668 onClick={() => 657 - onChange(options.filter((_, optionIndex) => optionIndex !== index)) 669 + onChange( 670 + options.filter((_, optionIndex) => optionIndex !== index), 671 + ) 658 672 } 659 673 aria-label="Remove option" 660 674 > ··· 756 770 // widgets in `DefaultValueInput`, so only range/membership can drift. 757 771 const dv = draft.defaultValue; 758 772 if (dv !== undefined) { 759 - if (draft.type === GQLActionParameterType.Number && typeof dv === 'number') { 773 + if ( 774 + draft.type === GQLActionParameterType.Number && 775 + typeof dv === 'number' 776 + ) { 760 777 if (draft.min !== undefined && dv < draft.min) { 761 778 return `${at}: default below min.`; 762 779 } ··· 788 805 readonly description?: string | null; 789 806 readonly type: GQLActionParameterType; 790 807 readonly required: boolean; 791 - readonly options?: ReadonlyArray<{ readonly value: string; readonly label: string }> | null; 808 + readonly options?: ReadonlyArray<{ 809 + readonly value: string; 810 + readonly label: string; 811 + }> | null; 792 812 readonly min?: number | null; 793 813 readonly max?: number | null; 794 814 readonly maxLength?: number | null; ··· 819 839 T extends Parameters<typeof fromGraphQLParameters>[0], 820 840 >(parameters: T | undefined): ActionParameterDraft[] | undefined { 821 841 return useMemo( 822 - () => (parameters === undefined ? undefined : fromGraphQLParameters(parameters)), 842 + () => 843 + parameters === undefined ? undefined : fromGraphQLParameters(parameters), 823 844 [parameters], 824 845 ); 825 846 }
-1
client/src/webpages/dashboard/banks/MatchingBanksDashboard.tsx
··· 56 56 ? 'location' 57 57 : 'hash' 58 58 }`} 59 - 60 59 disabled={!canEditBanks} 61 60 disabledTooltipTitle="To create Matching Banks, you need Admin permissions." 62 61 disabledTooltipPlacement="bottomRight"
+82 -64
client/src/webpages/dashboard/banks/hash/HashBankForm.tsx
··· 1 + import { Button, Form, Input, Select, Slider, Switch, Tag } from 'antd'; 1 2 import React, { useCallback, useEffect, useMemo, useState } from 'react'; 2 - import { Button, Form, Input, Select, Slider, Switch, Tag } from 'antd'; 3 + import { Helmet } from 'react-helmet-async'; 3 4 import { useNavigate, useParams } from 'react-router-dom'; 4 - import { Helmet } from 'react-helmet-async'; 5 + 6 + import FullScreenLoading from '../../../../components/common/FullScreenLoading'; 7 + import CoopButton from '../../components/CoopButton'; 8 + import CoopModal from '../../components/CoopModal'; 9 + import FormHeader from '../../components/FormHeader'; 10 + import FormSectionHeader from '../../components/FormSectionHeader'; 11 + import NameDescriptionInput from '../../components/NameDescriptionInput'; 12 + 5 13 import { 14 + GQLHashBankByIdDocument, 15 + namedOperations, 6 16 useGQLCreateHashBankMutation, 7 - useGQLUpdateHashBankMutation, 17 + useGQLExchangeApiSchemaLazyQuery, 18 + useGQLExchangeApisQuery, 19 + useGQLHashBankByIdQuery, 8 20 useGQLUpdateExchangeCredentialsMutation, 9 - useGQLHashBankByIdQuery, 10 - useGQLExchangeApisQuery, 11 - useGQLExchangeApiSchemaLazyQuery, 12 - namedOperations, 13 - GQLHashBankByIdDocument, 21 + useGQLUpdateHashBankMutation, 14 22 type GQLExchangeApiSchemaQuery, 15 23 } from '../../../../graphql/generated'; 16 - import CoopModal from '../../components/CoopModal'; 17 - import FormHeader from '../../components/FormHeader'; 18 - import FormSectionHeader from '../../components/FormSectionHeader'; 19 - import NameDescriptionInput from '../../components/NameDescriptionInput'; 20 - import CoopButton from '../../components/CoopButton'; 21 - import FullScreenLoading from '../../../../components/common/FullScreenLoading'; 22 24 23 25 type SchemaField = GQLExchangeApiSchemaQuery['exchangeApiSchema'] extends 24 26 | infer S 25 27 | null 26 28 | undefined 27 29 ? S extends { config_schema: { fields: ReadonlyArray<infer F> } } 28 - ? F 29 - : never 30 + ? F 31 + : never 30 32 : never; 31 33 32 34 const EXCHANGE_DISPLAY_NAMES: Record<string, string> = { ··· 93 95 (fieldName: string, field: SchemaField, raw: string) => { 94 96 onChange({ ...values, [fieldName]: coerceFieldValue(field, raw) }); 95 97 }, 96 - [values, onChange] 98 + [values, onChange], 97 99 ); 98 100 99 101 if (fields.length === 0) return null; ··· 151 153 ) : ( 152 154 <Input 153 155 value={ 154 - values[field.name] != null 155 - ? String(values[field.name]) 156 - : '' 156 + values[field.name] != null ? String(values[field.name]) : '' 157 157 } 158 158 placeholder={ 159 159 field.type === 'number' ··· 190 190 const [bankDescription, setBankDescription] = useState(''); 191 191 const [enabledRatio, setEnabledRatio] = useState(1.0); 192 192 193 - const [selectedExchangeApi, setSelectedExchangeApi] = useState<string | null>(null); 194 - const [exchangeConfigValues, setExchangeConfigValues] = useState<Record<string, unknown>>({}); 195 - const [exchangeCredValues, setExchangeCredValues] = useState<Record<string, unknown>>({}); 193 + const [selectedExchangeApi, setSelectedExchangeApi] = useState<string | null>( 194 + null, 195 + ); 196 + const [exchangeConfigValues, setExchangeConfigValues] = useState< 197 + Record<string, unknown> 198 + >({}); 199 + const [exchangeCredValues, setExchangeCredValues] = useState< 200 + Record<string, unknown> 201 + >({}); 196 202 197 203 const isCreating = id == null; 198 204 ··· 202 208 const exchangeApisQuery = useGQLExchangeApisQuery({ skip: !isCreating }); 203 209 const exchangeApis = useMemo( 204 210 () => exchangeApisQuery.data?.exchangeApis ?? [], 205 - [exchangeApisQuery.data?.exchangeApis] 211 + [exchangeApisQuery.data?.exchangeApis], 206 212 ); 207 213 208 214 const [fetchSchema, schemaQuery] = useGQLExchangeApiSchemaLazyQuery(); ··· 211 217 212 218 const selectedApiInfo = useMemo( 213 219 () => exchangeApis.find((a) => a.name === selectedExchangeApi), 214 - [exchangeApis, selectedExchangeApi] 220 + [exchangeApis, selectedExchangeApi], 215 221 ); 216 222 217 223 useEffect(() => { ··· 222 228 } 223 229 }, [selectedExchangeApi, fetchSchema]); 224 230 225 - const [editCredValues, setEditCredValues] = useState<Record<string, unknown>>({}); 231 + const [editCredValues, setEditCredValues] = useState<Record<string, unknown>>( 232 + {}, 233 + ); 226 234 const [showCredForm, setShowCredForm] = useState(false); 227 235 228 236 useEffect(() => { ··· 381 389 const exchangeInput = 382 390 selectedExchangeApi && schema 383 391 ? { 384 - api_name: selectedExchangeApi, 385 - config_json: JSON.stringify(exchangeConfigValues), 386 - credentials_json: 387 - schema.credentials_schema && 392 + api_name: selectedExchangeApi, 393 + config_json: JSON.stringify(exchangeConfigValues), 394 + credentials_json: 395 + schema.credentials_schema && 388 396 selectedApiInfo && 389 397 !selectedApiInfo.has_auth 390 - ? JSON.stringify(exchangeCredValues) 391 - : undefined, 392 - } 398 + ? JSON.stringify(exchangeCredValues) 399 + : undefined, 400 + } 393 401 : undefined; 394 402 395 403 createHashBank({ ··· 440 448 hideModal(); 441 449 442 450 if ( 443 - (createMutationParams.data?.createHashBank && 'data' in createMutationParams.data.createHashBank) || 444 - (updateMutationParams.data?.updateHashBank && 'data' in updateMutationParams.data.updateHashBank) 451 + (createMutationParams.data?.createHashBank && 452 + 'data' in createMutationParams.data.createHashBank) || 453 + (updateMutationParams.data?.updateHashBank && 454 + 'data' in updateMutationParams.data.updateHashBank) 445 455 ) { 446 456 navigate(-1); 447 457 } ··· 453 463 (f) => 454 464 f.required && 455 465 (exchangeConfigValues[f.name] == null || 456 - exchangeConfigValues[f.name] === '') 466 + exchangeConfigValues[f.name] === ''), 457 467 ); 458 468 459 469 const hasRequiredCredsMissing = ··· 465 475 (f) => 466 476 f.required && 467 477 (exchangeCredValues[f.name] == null || 468 - exchangeCredValues[f.name] === '') 478 + exchangeCredValues[f.name] === ''), 469 479 ); 470 480 471 - const isExchangeIncomplete = Boolean(hasRequiredConfigMissing) || Boolean(hasRequiredCredsMissing); 481 + const isExchangeIncomplete = 482 + Boolean(hasRequiredConfigMissing) || Boolean(hasRequiredCredsMissing); 472 483 473 484 const modal = ( 474 485 <CoopModal ··· 488 499 ); 489 500 490 501 const exchangeApiDisplayName = bank?.exchange 491 - ? EXCHANGE_DISPLAY_NAMES[bank.exchange.api] ?? bank.exchange.api 502 + ? (EXCHANGE_DISPLAY_NAMES[bank.exchange.api] ?? bank.exchange.api) 492 503 : null; 493 504 494 505 return ( ··· 526 537 {bank.exchange.enabled ? 'Enabled' : 'Disabled'} 527 538 </Tag> 528 539 <Tag color={bank.exchange.has_auth ? 'green' : 'orange'}> 529 - {bank.exchange.has_auth ? 'Credentials Set' : 'Credentials Missing'} 540 + {bank.exchange.has_auth 541 + ? 'Credentials Set' 542 + : 'Credentials Missing'} 530 543 </Tag> 531 544 {bank.exchange.last_fetch_succeeded === false && ( 532 545 <Tag color="red">Fetch Failed</Tag> ··· 540 553 </div> 541 554 {bank.exchange.last_fetch_time && ( 542 555 <span className="text-xs text-zinc-500"> 543 - Last fetch: {new Date(bank.exchange.last_fetch_time).toLocaleString()} 556 + Last fetch:{' '} 557 + {new Date(bank.exchange.last_fetch_time).toLocaleString()} 544 558 {bank.exchange.fetched_items != null && ( 545 559 <> &middot; {bank.exchange.fetched_items} items</> 546 560 )} ··· 548 562 )} 549 563 {bank.exchange.last_fetch_succeeded === false && ( 550 564 <span className="text-xs text-red-600"> 551 - The last fetch from this exchange failed. Check that credentials are correct and the exchange service is reachable. 565 + The last fetch from this exchange failed. Check that 566 + credentials are correct and the exchange service is 567 + reachable. 552 568 </span> 553 569 )} 554 570 </div> ··· 581 597 <CoopButton 582 598 title="Save Credentials" 583 599 loading={updateCredsMutationParams.loading} 584 - disabled={ 585 - schema.credentials_schema.fields 586 - .filter((f) => f.required) 587 - .some( 588 - (f) => 589 - editCredValues[f.name] == null || 590 - editCredValues[f.name] === '' 591 - ) 592 - } 600 + disabled={schema.credentials_schema.fields 601 + .filter((f) => f.required) 602 + .some( 603 + (f) => 604 + editCredValues[f.name] == null || 605 + editCredValues[f.name] === '', 606 + )} 593 607 disabledTooltipTitle="Please fill in all required credential fields" 594 608 onClick={onUpdateCredentials} 595 609 /> ··· 625 639 marks={{ 626 640 0: 'Disabled', 627 641 0.5: '50%', 628 - 1: 'Enabled' 642 + 1: 'Enabled', 629 643 }} 630 644 tooltip={{ 631 - formatter: (value) => `${Math.round((value ?? 0) * 100)}%` 645 + formatter: (value) => `${Math.round((value ?? 0) * 100)}%`, 632 646 }} 633 - style={{ 634 - '--ant-slider-track-background-color': getSliderColor(enabledRatio), 635 - maxWidth: '100%', 636 - } as React.CSSProperties} 647 + style={ 648 + { 649 + '--ant-slider-track-background-color': 650 + getSliderColor(enabledRatio), 651 + maxWidth: '100%', 652 + } as React.CSSProperties 653 + } 637 654 onChange={(value) => { 638 655 setEnabledRatio(value); 639 656 }} ··· 661 678 loading={exchangeApisQuery.loading} 662 679 className="max-w-md" 663 680 options={exchangeApis.map((api) => ({ 664 - label: EXCHANGE_DISPLAY_NAMES[api.name] ?? api.name.replace(/_/g, ' '), 681 + label: 682 + EXCHANGE_DISPLAY_NAMES[api.name] ?? 683 + api.name.replace(/_/g, ' '), 665 684 value: api.name, 666 685 }))} 667 686 /> ··· 702 721 </div> 703 722 )} 704 723 705 - {schema.credentials_schema && 706 - selectedApiInfo?.has_auth && ( 707 - <div className="mt-4 p-3 text-sm rounded-md bg-emerald-50 text-emerald-700"> 708 - Credentials for this exchange API are already configured. 709 - </div> 710 - )} 724 + {schema.credentials_schema && selectedApiInfo?.has_auth && ( 725 + <div className="mt-4 p-3 text-sm rounded-md bg-emerald-50 text-emerald-700"> 726 + Credentials for this exchange API are already configured. 727 + </div> 728 + )} 711 729 </> 712 730 )} 713 731
+41 -31
client/src/webpages/dashboard/banks/hash/HashBanksDashboard.tsx
··· 1 - import React, { useMemo, useState, useCallback } from 'react'; 2 1 import { Tag } from 'antd'; 3 - import { MouseEvent } from 'react'; 4 - import { useNavigate } from 'react-router-dom'; 2 + import React, { MouseEvent, useCallback, useMemo, useState } from 'react'; 5 3 import { Helmet } from 'react-helmet-async'; 6 - import { 7 - useGQLHashBanksQuery, 8 - useGQLDeleteHashBankMutation, 9 - namedOperations 10 - } from '../../../../graphql/generated'; 4 + import { useNavigate } from 'react-router-dom'; 5 + 6 + import FullScreenLoading from '../../../../components/common/FullScreenLoading'; 11 7 import CoopModal from '../../components/CoopModal'; 12 8 import EmptyDashboard from '../../components/EmptyDashboard'; 13 9 import RowMutations, { ··· 19 15 } from '../../components/table/filters'; 20 16 import { stringSort } from '../../components/table/sort'; 21 17 import CustomTable from '../../components/table/Table'; 22 - import FullScreenLoading from '../../../../components/common/FullScreenLoading'; 18 + 19 + import { 20 + namedOperations, 21 + useGQLDeleteHashBankMutation, 22 + useGQLHashBanksQuery, 23 + } from '../../../../graphql/generated'; 23 24 24 25 const getStatusColor = (enabled_ratio: number) => { 25 26 if (enabled_ratio === 0) return 'red'; ··· 45 46 46 47 const navigate = useNavigate(); 47 48 48 - const editBank = useCallback((id: string, event: MouseEvent) => { 49 - // This ensures that the row's onClick isn't called because 50 - // the row is the parent component 51 - event.stopPropagation(); 52 - navigate(`/dashboard/rules/banks/form/hash/${id}`); 53 - }, [navigate]); 49 + const editBank = useCallback( 50 + (id: string, event: MouseEvent) => { 51 + // This ensures that the row's onClick isn't called because 52 + // the row is the parent component 53 + event.stopPropagation(); 54 + navigate(`/dashboard/rules/banks/form/hash/${id}`); 55 + }, 56 + [navigate], 57 + ); 54 58 55 - const onDeleteBank = useCallback((id: string) => { 56 - deleteHashBank({ 57 - variables: { id }, 58 - refetchQueries: [namedOperations.Query.HashBanks], 59 - }); 60 - }, [deleteHashBank]); 59 + const onDeleteBank = useCallback( 60 + (id: string) => { 61 + deleteHashBank({ 62 + variables: { id }, 63 + refetchQueries: [namedOperations.Query.HashBanks], 64 + }); 65 + }, 66 + [deleteHashBank], 67 + ); 61 68 62 69 const showModal = useCallback((id: string, event: MouseEvent) => { 63 70 // This ensures that the row's onClick isn't called because ··· 69 76 }); 70 77 }, []); 71 78 72 - const mutations = useCallback((id: string) => { 73 - return ( 74 - <RowMutations 75 - onEdit={(event: MouseEvent) => editBank(id, event)} 76 - onDelete={(event: MouseEvent) => showModal(id, event)} 77 - canDelete={true} 78 - /> 79 - ); 80 - }, [editBank, showModal]); 79 + const mutations = useCallback( 80 + (id: string) => { 81 + return ( 82 + <RowMutations 83 + onEdit={(event: MouseEvent) => editBank(id, event)} 84 + onDelete={(event: MouseEvent) => showModal(id, event)} 85 + canDelete={true} 86 + /> 87 + ); 88 + }, 89 + [editBank, showModal], 90 + ); 81 91 82 92 const columns = useMemo( 83 93 () => [ ··· 212 222 {deleteModal} 213 223 </div> 214 224 ); 215 - } 225 + }
+1 -1
client/src/webpages/dashboard/bulk_actioning/BulkActioningDashboard.tsx
··· 7 7 import { type JsonObject } from 'type-fest'; 8 8 9 9 import ActionParameterInputs, { 10 - type ActionParameterValues, 11 10 findMissingRequiredParameters, 11 + type ActionParameterValues, 12 12 } from '../../../components/ActionParameterInputs'; 13 13 import FullScreenLoading from '../../../components/common/FullScreenLoading'; 14 14 import { selectFilterByLabelOption } from '../components/antDesignUtils';
+1 -1
client/src/webpages/dashboard/components/PolicyDropdown.tsx
··· 65 65 treeLine={true} 66 66 maxTagCount={maxTagCount} 67 67 placeholder={ 68 - placeholder ?? multiple ? 'Select Policies' : 'Select policy' 68 + (placeholder ?? multiple) ? 'Select Policies' : 'Select policy' 69 69 } 70 70 dropdownMatchSelectWidth={false} 71 71 value={selectedPolicyIds}
+1 -1
client/src/webpages/dashboard/components/RoundedTag.tsx
··· 56 56 <div 57 57 className={`inline-flex items-center gap-x-1.5 py-1.5 px-3 rounded-full text-sm font-medium whitespace-nowrap ${colors}`} 58 58 > 59 - {title} 59 + {title} 60 60 </div> 61 61 ); 62 62 }
+2 -2
client/src/webpages/dashboard/components/TextToken.tsx
··· 8 8 const { title, onDelete, disabled } = props; 9 9 return ( 10 10 <div className="flex text-start m-0.5 py-0.5 px-1.5 bg-gray-200 rounded items-center gap-1.5 text-base"> 11 - <span className="flex items-center text-center">{title}</span> 12 - {Boolean(!disabled) ? <CloseButton onClose={onDelete} /> : null} 11 + <span className="flex items-center text-center">{title}</span> 12 + {Boolean(!disabled) ? <CloseButton onClose={onDelete} /> : null} 13 13 </div> 14 14 ); 15 15 }
+4 -1
client/src/webpages/dashboard/components/table/sort.tsx
··· 208 208 if (!b) return -1; 209 209 210 210 // Sort by timestamp (oldest first = smaller timestamp first) 211 - return new Date(a as string | Date).getTime() - new Date(b as string | Date).getTime(); 211 + return ( 212 + new Date(a as string | Date).getTime() - 213 + new Date(b as string | Date).getTime() 214 + ); 212 215 }; 213 216 }
+2 -4
client/src/webpages/dashboard/integrations/IntegrationCard.tsx
··· 1 1 import { ReactNode, useState } from 'react'; 2 2 import { Link, useNavigate } from 'react-router-dom'; 3 3 4 - import type { GQLIntegrationMetadata } from '../../../graphql/generated'; 5 4 import CoopModal from '../components/CoopModal'; 6 5 6 + import type { GQLIntegrationMetadata } from '../../../graphql/generated'; 7 7 import { INTEGRATION_LOGO_FALLBACKS } from './integrationLogos'; 8 8 9 9 export default function IntegrationCard(props: { ··· 14 14 const { name, title, docsUrl } = integration; 15 15 // Integrations page uses only the plain logo (logoUrl from logoPath). Do not fall back to logoWithBackgroundUrl. 16 16 const rawLogo = 17 - integration.logoUrl ?? 18 - INTEGRATION_LOGO_FALLBACKS[name]?.logo ?? 19 - ''; 17 + integration.logoUrl ?? INTEGRATION_LOGO_FALLBACKS[name]?.logo ?? ''; 20 18 // Resolve relative API paths to absolute URL so img loads correctly (e.g. /api/v1/integration-logos/ID). 21 19 const logo = 22 20 typeof rawLogo === 'string' && rawLogo.startsWith('/')
+7 -7
client/src/webpages/dashboard/integrations/IntegrationConfigApiCredentialsSection.tsx
··· 151 151 truePercentage: 'True percentage (0–100)', 152 152 }; 153 153 154 - const renderPluginCredential = ( 155 - pluginCredential: { __typename: 'PluginIntegrationApiCredential'; credential: Record<string, unknown> }, 156 - ) => { 154 + const renderPluginCredential = (pluginCredential: { 155 + __typename: 'PluginIntegrationApiCredential'; 156 + credential: Record<string, unknown>; 157 + }) => { 157 158 const credential = pluginCredential.credential ?? {}; 158 159 const entries = Object.entries(credential).filter( 159 160 ([key]) => key !== 'name', ··· 166 167 <div className="flex flex-col gap-4"> 167 168 {fieldsToShow.map(([key, value]) => ( 168 169 <div key={key} className={`flex flex-col ${inputWidthClass}`}> 169 - <div className="mb-1"> 170 - {PLUGIN_FIELD_LABELS[key] ?? key} 171 - </div> 170 + <div className="mb-1">{PLUGIN_FIELD_LABELS[key] ?? key}</div> 172 171 <Input 173 172 value={String(value ?? '')} 174 173 onChange={(event) => { 175 174 const next = { ...credential, [key]: event.target.value }; 176 175 setApiCredential({ 177 176 __typename: 'PluginIntegrationApiCredential', 178 - credential: next as import('../../../graphql/generated').Scalars['JSONObject'], 177 + credential: 178 + next as import('../../../graphql/generated').Scalars['JSONObject'], 179 179 }); 180 180 }} 181 181 />
+29 -35
client/src/webpages/dashboard/integrations/IntegrationConfigForm.tsx
··· 49 49 } 50 50 } 51 51 52 - mutation SetPluginIntegrationConfig($input: SetPluginIntegrationConfigInput!) { 52 + mutation SetPluginIntegrationConfig( 53 + $input: SetPluginIntegrationConfigInput! 54 + ) { 53 55 setPluginIntegrationConfig(input: $input) { 54 56 ... on SetIntegrationConfigSuccessResponse { 55 57 config { ··· 136 138 * This function returns an empty API credential config (type is 137 139 * IntegrationConfigApiCredential), so the UI can display the proper empty inputs. 138 140 */ 139 - export function getNewEmptyApiKey( 140 - name: string, 141 - ): GQLIntegrationApiCredential { 141 + export function getNewEmptyApiKey(name: string): GQLIntegrationApiCredential { 142 142 switch (name) { 143 143 case 'GOOGLE_CONTENT_SAFETY_API': { 144 144 return { ··· 258 258 response?.__typename === 'IntegrationConfigSuccessResult' 259 259 ? response.config 260 260 : undefined; 261 - const formattedName = 262 - apiConfig?.title ?? integrationName.replace(/_/g, ' '); 261 + const formattedName = apiConfig?.title ?? integrationName.replace(/_/g, ' '); 263 262 const logo = apiConfig 264 263 ? (apiConfig.logoUrl ?? 265 264 INTEGRATION_LOGO_FALLBACKS[apiConfig.name]?.logo ?? ··· 290 289 'googleContentSafetyApi' in mappedApiCredential && 291 290 !( 292 291 mappedApiCredential[ 293 - 'googleContentSafetyApi' 292 + 'googleContentSafetyApi' 294 293 ] as GQLGoogleContentSafetyApiIntegrationApiCredential 295 294 ).apiKey 296 295 ) { ··· 307 306 308 307 if ( 309 308 'zentropi' in mappedApiCredential && 310 - !( 311 - mappedApiCredential[ 312 - 'zentropi' 313 - ] as GQLZentropiIntegrationApiCredential 314 - ).apiKey 309 + !(mappedApiCredential['zentropi'] as GQLZentropiIntegrationApiCredential) 310 + .apiKey 315 311 ) { 316 312 return 'Please input the Zentropi API key'; 317 313 } ··· 334 330 if (isPluginIntegration) { 335 331 const cred = 336 332 apiCredential.__typename === 'PluginIntegrationApiCredential' 337 - ? apiCredential.credential ?? {} 333 + ? (apiCredential.credential ?? {}) 338 334 : {}; 339 335 await setPluginConfig({ 340 336 variables: { ··· 361 357 const [modalTitle, modalBody, modalButtonText] = 362 358 mutationError == null 363 359 ? [ 364 - `${formattedName} Config Saved`, 365 - `Your ${formattedName} Config was successfully saved!`, 366 - 'Done', 367 - ] 360 + `${formattedName} Config Saved`, 361 + `Your ${formattedName} Config was successfully saved!`, 362 + 'Done', 363 + ] 368 364 : [ 369 - `Error Saving ${formattedName} Config`, 370 - `We encountered an error trying to save your ${formattedName} Config. Please try again.`, 371 - 'OK', 372 - ]; 365 + `Error Saving ${formattedName} Config`, 366 + `We encountered an error trying to save your ${formattedName} Config. Please try again.`, 367 + 'OK', 368 + ]; 373 369 374 370 const onHideModal = () => { 375 371 hideModal(); ··· 403 399 case 'GOOGLE_CONTENT_SAFETY_API': 404 400 return ( 405 401 <> 406 - The Content Safety API is an AI classifier which issues a Child 407 - Safety prioritization recommendation on content sent to it. Content Safety API users 408 - must conduct their own manual review in order to determine whether to take 409 - action on the content, and comply with applicable local reporting 410 - laws. Apply for API keys{' '} 402 + The Content Safety API is an AI classifier which issues a Child 403 + Safety prioritization recommendation on content sent to it. Content 404 + Safety API users must conduct their own manual review in order to 405 + determine whether to take action on the content, and comply with 406 + applicable local reporting laws. Apply for API keys{' '} 411 407 <a 412 408 href="https://protectingchildren.google/tools-for-partners/" 413 409 target="_blank" ··· 415 411 className="text-blue-600 hover:underline" 416 412 > 417 413 here 418 - </a> 419 - {' '} 414 + </a>{' '} 420 415 and mention in your application that you are using the Coop 421 416 moderation tool. Upon reviewing your application, Google will be 422 - back in touch shortly to take the application forward if you qualify. 417 + back in touch shortly to take the application forward if you 418 + qualify. 423 419 </> 424 420 ); 425 421 case 'OPEN_AI': ··· 441 437 <div className="flex flex-col justify-between w-4/5 mb-4"> 442 438 <div className="flex items-center gap-3 mb-1"> 443 439 <div className="w-12 h-12 rounded-full bg-slate-200 flex items-center justify-center shrink-0 overflow-hidden"> 444 - <img 445 - src={logo} 446 - alt="" 447 - className="w-full h-full object-contain" 448 - /> 440 + <img src={logo} alt="" className="w-full h-full object-contain" /> 449 441 </div> 450 442 <div className="text-2xl font-bold">{`${formattedName} Integration`}</div> 451 443 </div> ··· 473 465 <span>Learn more about how to read model cards</span> 474 466 </a> 475 467 )} 476 - <div className="font-semibold text-zinc-800 mb-2">Configuration</div> 468 + <div className="font-semibold text-zinc-800 mb-2"> 469 + Configuration 470 + </div> 477 471 <div className="text-sm text-zinc-600 mb-3"> 478 472 Configure your integration settings below. 479 473 </div>
+8 -5
client/src/webpages/dashboard/integrations/IntegrationsDashboard.tsx
··· 38 38 useGQLAvailableIntegrationsQuery({ 39 39 fetchPolicy: 'network-only', 40 40 }); 41 - const { loading: loadingMy, error, data: myData } = 42 - useGQLMyIntegrationsQuery(); 41 + const { 42 + loading: loadingMy, 43 + error, 44 + data: myData, 45 + } = useGQLMyIntegrationsQuery(); 43 46 44 47 const loading = loadingCatalog || loadingMy; 45 48 ··· 58 61 const myIntegrations = allIntegrations.filter((it) => 59 62 myIntegrationNames.includes(it.name), 60 63 ); 61 - const otherIntegrations = allIntegrations.filter( 62 - (it) => !myIntegrationNames.includes(it.name), 63 - ).sort((a, b) => a.name.localeCompare(b.name)); 64 + const otherIntegrations = allIntegrations 65 + .filter((it) => !myIntegrationNames.includes(it.name)) 66 + .sort((a, b) => a.name.localeCompare(b.name)); 64 67 65 68 return ( 66 69 <div className="flex flex-col">
+1 -1
client/src/webpages/dashboard/integrations/ModelCardView.tsx
··· 1 - import { useState } from 'react'; 2 1 import { ChevronDown, ChevronRight } from 'lucide-react'; 2 + import { useState } from 'react'; 3 3 4 4 import type { 5 5 GQLModelCard,
+4 -2
client/src/webpages/dashboard/investigation/ItemInvestigationSummary.tsx
··· 13 13 type GQLContentSchemaFieldRoles, 14 14 type GQLThreadItemType, 15 15 } from '../../../graphql/generated'; 16 + import { findFirstIframeUrl } from '../../../utils/contentUrlUtils'; 16 17 import { 17 18 getFieldValueForRole, 18 19 getFieldValueOrValues, ··· 22 23 import ItemActionHistory from '../items/ItemActionHistory'; 23 24 import IframeContentDisplayComponent from '../mrt/manual_review_job/IframeContentDisplayComponent'; 24 25 import FieldsComponent from '../mrt/manual_review_job/v2/ManualReviewJobFieldsComponent'; 25 - import { findFirstIframeUrl } from '../../../utils/contentUrlUtils'; 26 26 27 27 export default function ItemInvestigationSummary(props: { 28 28 item: { ··· 254 254 'type' in firstIframeUrl && 255 255 firstIframeUrl.type === 'URL' ? ( 256 256 <div className="w-full"> 257 - <IframeContentDisplayComponent contentUrl={firstIframeUrl?.value} /> 257 + <IframeContentDisplayComponent 258 + contentUrl={firstIframeUrl?.value} 259 + /> 258 260 </div> 259 261 ) : null} 260 262 <div className="my-6">
+2 -2
client/src/webpages/dashboard/mrt/ManualReviewDecisionsTable.tsx
··· 187 187 return a.Header === 'Name' 188 188 ? -1 189 189 : b.Header === 'Name' 190 - ? 1 191 - : a.Header.localeCompare(b.Header); 190 + ? 1 191 + : a.Header.localeCompare(b.Header); 192 192 }); 193 193 194 194 const filledInData = columns
+31 -26
client/src/webpages/dashboard/mrt/ManualReviewRecentDecisionsFilter.tsx
··· 1 + import { DateRangePicker } from '@/coop-ui/DateRangePicker'; 1 2 import ChevronDown from '@/icons/lni/Direction/chevron-down.svg?react'; 2 3 import ChevronUp from '@/icons/lni/Direction/chevron-up.svg?react'; 3 - import { DateRangePicker } from '@/coop-ui/DateRangePicker'; 4 4 import { Select } from 'antd'; 5 5 import without from 'lodash/without'; 6 6 import { useRef, useState } from 'react'; ··· 43 43 44 44 export type DecisionOrAction = 45 45 | { 46 - type: 'CUSTOM_ACTION'; 47 - actionId: string; 48 - } 46 + type: 'CUSTOM_ACTION'; 47 + actionId: string; 48 + } 49 49 | { 50 - type: 'REJECT_APPEAL' | 'ACCEPT_APPEAL'; 51 - appealId: string; 52 - actionIds: string[]; 53 - } 50 + type: 'REJECT_APPEAL' | 'ACCEPT_APPEAL'; 51 + appealId: string; 52 + actionIds: string[]; 53 + } 54 54 | { 55 - type: Exclude< 56 - GQLManualReviewDecisionType, 57 - 'CUSTOM_ACTION' | 'RELATED_ACTION' | 'REJECT_APPEAL' | 'ACCEPT_APPEAL' 58 - >; 59 - }; 55 + type: Exclude< 56 + GQLManualReviewDecisionType, 57 + 'CUSTOM_ACTION' | 'RELATED_ACTION' | 'REJECT_APPEAL' | 'ACCEPT_APPEAL' 58 + >; 59 + }; 60 60 61 61 const decisionFilterByColumns = [ 62 62 'decisions', ··· 167 167 it === 'RELATED_ACTION' || it === 'CUSTOM_ACTION' 168 168 ? undefined 169 169 : { 170 - id: jsonStringify({ type: it }), 171 - name: getReadableNameFromDecisionType(it), 172 - }, 170 + id: jsonStringify({ type: it }), 171 + name: getReadableNameFromDecisionType(it), 172 + }, 173 173 ), 174 174 ].flat(), 175 175 ); ··· 254 254 something to do with dynamically choosing whether to render each icon because when 255 255 we render both and just hide one of them, componentRef.current.contains() works. */} 256 256 <ChevronUp 257 - className={`ml-2 w-3 fill-slate-400 flex items-center ${filterByMenuVisible ? '' : 'hidden' 258 - }`} 257 + className={`ml-2 w-3 fill-slate-400 flex items-center ${ 258 + filterByMenuVisible ? '' : 'hidden' 259 + }`} 259 260 /> 260 261 <ChevronDown 261 - className={`ml-2 w-3 fill-slate-400 flex items-center ${filterByMenuVisible ? 'hidden' : '' 262 - }`} 262 + className={`ml-2 w-3 fill-slate-400 flex items-center ${ 263 + filterByMenuVisible ? 'hidden' : '' 264 + }`} 263 265 /> 264 266 </div> 265 267 {filterByMenuVisible && ( ··· 295 297 ); 296 298 return ( 297 299 <div 298 - className={`flex flex-col ${isExpanded ? 'bg-gray-100' : '' 299 - }`} 300 + className={`flex flex-col ${ 301 + isExpanded ? 'bg-gray-100' : '' 302 + }`} 300 303 key={column} 301 304 > 302 305 <div ··· 315 318 something to do with dynamically choosing whether to render each icon because when 316 319 we render both and just hide one of them, componentRef.current.contains() works. */} 317 320 <ChevronUp 318 - className={`font-bold w-3 fill-slate-400 ${isExpanded ? '' : 'hidden' 319 - }`} 321 + className={`font-bold w-3 fill-slate-400 ${ 322 + isExpanded ? '' : 'hidden' 323 + }`} 320 324 /> 321 325 <ChevronDown 322 - className={`font-bold w-3 fill-slate-400 ${isExpanded ? 'hidden' : '' 323 - }`} 326 + className={`font-bold w-3 fill-slate-400 ${ 327 + isExpanded ? 'hidden' : '' 328 + }`} 324 329 /> 325 330 </div> 326 331 {isExpanded && (
+2 -2
client/src/webpages/dashboard/mrt/manual_review_job/ManualReviewJobContentBlurableVideo.tsx
··· 72 72 ? blurStrength 73 73 ? BLUR_LEVELS[blurStrength] 74 74 : !playing 75 - ? 'blur-sm' 76 - : 'blur-0' 75 + ? 'blur-sm' 76 + : 'blur-0' 77 77 : 'blur-0' 78 78 }`} 79 79 >
+3 -1
client/src/webpages/dashboard/mrt/manual_review_job/v2/ManualReviewJobCommentSection.tsx
··· 99 99 isBeingDeleted ? 'text-gray-300' : 'text-gray-500' 100 100 }`} 101 101 > 102 - {formatDistanceToNow(new Date(comment.createdAt as string), { addSuffix: true })} 102 + {formatDistanceToNow(new Date(comment.createdAt as string), { 103 + addSuffix: true, 104 + })} 103 105 </div> 104 106 </div> 105 107 <div
+6 -6
client/src/webpages/dashboard/mrt/manual_review_job/v2/ncmec/NCMECMediaViewer.tsx
··· 218 218 state!.category === 'A1' 219 219 ? 'border-red-400' 220 220 : state!.category === 'A2' 221 - ? 'border-orange-400' 222 - : state!.category === 'B1' 223 - ? 'border-amber-400' 224 - : state!.category === 'B2' 225 - ? 'border-blue-400' 226 - : 'border-slate-500' 221 + ? 'border-orange-400' 222 + : state!.category === 'B1' 223 + ? 'border-amber-400' 224 + : state!.category === 'B2' 225 + ? 'border-blue-400' 226 + : 'border-slate-500' 227 227 }` 228 228 : 'border-transparent' 229 229 }`}
+6 -2
client/src/webpages/dashboard/mrt/queue_routing/ManualReviewQueueRoutingRuleForm.tsx
··· 4 4 5 5 import { selectFilterByLabelOption } from '../../components/antDesignUtils'; 6 6 7 - import { GQLConditionConjunction, GQLSignal } from '../../../../graphql/generated'; 7 + import { 8 + GQLConditionConjunction, 9 + GQLSignal, 10 + } from '../../../../graphql/generated'; 8 11 import { 9 12 hasNestedConditionSets, 10 13 removeConditionSet, ··· 154 157 signals: readonly GQLSignal[]; 155 158 parentConditionSet?: RuleFormConditionSet; 156 159 }) => { 157 - const { conditionSet, conditionSetIndex, signals, parentConditionSet } = opts; 160 + const { conditionSet, conditionSetIndex, signals, parentConditionSet } = 161 + opts; 158 162 159 163 if (hasNestedConditionSets(conditionSet)) { 160 164 const conditions = conditionSet.conditions;
+8 -3
client/src/webpages/dashboard/mrt/queue_routing/ManualReviewQueueRuleFormCondition.tsx
··· 1 1 import { DeleteOutlined, InfoCircleOutlined } from '@ant-design/icons'; 2 2 import { Button, Select, Tooltip } from 'antd'; 3 3 4 - import { GQLConditionConjunction, GQLSignal } from '../../../../graphql/generated'; 4 + import { 5 + GQLConditionConjunction, 6 + GQLSignal, 7 + } from '../../../../graphql/generated'; 5 8 import { 6 9 hasNestedConditionSets, 7 10 removeCondition, ··· 9 12 import { 10 13 ConditionInput, 11 14 ConditionLocation, 15 + isConditionSet, 12 16 RuleFormConditionSet, 13 17 RuleFormLeafCondition, 14 - isConditionSet, 15 18 } from '../../rules/types'; 16 19 import ManualReviewQueueRuleConditionComparator from './condition/comparator/ManualReviewQueueRuleConditionComparator'; 17 20 import ManualReviewQueueRuleConditionInput from './condition/input/ManualReviewQueueRuleConditionInput'; ··· 226 229 ), 227 230 ) 228 231 } 229 - allConditions={parentConditionSet.conditions.filter((c): c is RuleFormLeafCondition => !isConditionSet(c))} 232 + allConditions={parentConditionSet.conditions.filter( 233 + (c): c is RuleFormLeafCondition => !isConditionSet(c), 234 + )} 230 235 /> 231 236 <ManualReviewQueueRuleConditionComparator 232 237 condition={condition}
+16 -16
client/src/webpages/dashboard/mrt/queue_routing/condition/comparator/ManualReviewQueueRuleConditionComparator.tsx
··· 35 35 const comparatorTypes = condition.signal?.outputType 36 36 ? outputTypeToComparators(condition.signal.outputType) 37 37 : inputScalarType === GQLScalarType.Number 38 - ? [ 39 - GQLValueComparator.Equals, 40 - GQLValueComparator.NotEqualTo, 41 - GQLValueComparator.GreaterThan, 42 - GQLValueComparator.LessThan, 43 - GQLValueComparator.GreaterThanOrEquals, 44 - GQLValueComparator.LessThanOrEquals, 45 - GQLValueComparator.IsUnavailable, 46 - GQLValueComparator.IsNotProvided, 47 - ] 48 - : [ 49 - GQLValueComparator.Equals, 50 - GQLValueComparator.NotEqualTo, 51 - GQLValueComparator.IsUnavailable, 52 - GQLValueComparator.IsNotProvided, 53 - ]; 38 + ? [ 39 + GQLValueComparator.Equals, 40 + GQLValueComparator.NotEqualTo, 41 + GQLValueComparator.GreaterThan, 42 + GQLValueComparator.LessThan, 43 + GQLValueComparator.GreaterThanOrEquals, 44 + GQLValueComparator.LessThanOrEquals, 45 + GQLValueComparator.IsUnavailable, 46 + GQLValueComparator.IsNotProvided, 47 + ] 48 + : [ 49 + GQLValueComparator.Equals, 50 + GQLValueComparator.NotEqualTo, 51 + GQLValueComparator.IsUnavailable, 52 + GQLValueComparator.IsNotProvided, 53 + ]; 54 54 55 55 // If there is only one valid comparator to choose from, we should set 56 56 // the condition.comparator value to that value by default
+1 -2
client/src/webpages/dashboard/mrt/queue_routing/condition/input/ManualReviewQueueRuleConditionInput.tsx
··· 42 42 'Use this to check inspect the user who created this content, ' + 43 43 'rather than inspecting the content itself.', 44 44 [CoopInput.POLICY_ID]: 'The policy that was used to enqueue this job.', 45 - [CoopInput.SOURCE]: 46 - 'The creation source from which this job was enqueued.', 45 + [CoopInput.SOURCE]: 'The creation source from which this job was enqueued.', 47 46 }; 48 47 49 48 export default function ManualReviewQueueRuleConditionInput(props: {
+7 -2
client/src/webpages/dashboard/mrt/queue_routing/condition/matching_values/ManualReviewQueueRuleConditionMatchingBankInput.tsx
··· 42 42 const isRegexSignal = 43 43 condition.signal?.type && receivesRegexInput(condition.signal.type); 44 44 45 - const { textBankIds, locationBankIds, imageBankIds } = condition.matchingValues ?? {}; 45 + const { textBankIds, locationBankIds, imageBankIds } = 46 + condition.matchingValues ?? {}; 46 47 const bankIds = textBankIds ?? locationBankIds ?? imageBankIds ?? []; 47 48 48 49 const { loading, error, data } = useGQLMatchingBankIdsQuery(); 49 50 const { textBanks, locationBanks, hashBanks } = data?.myOrg?.banks ?? {}; 50 - const allBanks = [textBanks ?? [], locationBanks ?? [], hashBanks ?? []].flat(); 51 + const allBanks = [ 52 + textBanks ?? [], 53 + locationBanks ?? [], 54 + hashBanks ?? [], 55 + ].flat(); 51 56 52 57 if (loading) { 53 58 return <ComponentLoading />;
+1 -1
client/src/webpages/dashboard/mrt/queue_routing/condition/signal/ManualReviewQueueRuleConditionSignal.tsx
··· 1 + import { GQLSignal } from '@/graphql/generated'; 1 2 import { DownOutlined } from '@ant-design/icons'; 2 3 import { Button } from 'antd'; 3 4 import { useState } from 'react'; 4 5 5 - import { GQLSignal } from '@/graphql/generated'; 6 6 import RuleFormSignalModal from '../../../../rules/rule_form/signal_modal/RuleFormSignalModal'; 7 7 import { 8 8 ConditionLocation,
+8 -8
client/src/webpages/dashboard/mrt/queue_routing/condition/threshold/ManualReviewQueueRuleConditionThreshold.tsx
··· 212 212 {renderBooleanThreshold 213 213 ? 'Value' 214 214 : renderPolicyThreshold 215 - ? 'Policy' 216 - : renderStringThreshold 217 - ? 'Creation Source' 218 - : 'Threshold'} 215 + ? 'Policy' 216 + : renderStringThreshold 217 + ? 'Creation Source' 218 + : 'Threshold'} 219 219 </div> 220 220 {!editing ? ( 221 221 <ManualReviewQueueRoutingStaticTextField ··· 242 242 {renderBooleanThreshold 243 243 ? 'Value' 244 244 : renderPolicyThreshold 245 - ? 'Policy' 246 - : renderStringThreshold 247 - ? 'Creation Source' 248 - : 'Threshold'} 245 + ? 'Policy' 246 + : renderStringThreshold 247 + ? 'Creation Source' 248 + : 'Threshold'} 249 249 </div> 250 250 </div> 251 251 </div>
+5 -8
client/src/webpages/dashboard/mrt/visualization/ManualReviewDashboardInsightsCard.tsx
··· 1 - import { ChevronDown, ChevronUp } from 'lucide-react'; 2 1 import { 3 2 differenceInDays, 4 3 differenceInHours, ··· 7 6 differenceInWeeks, 8 7 differenceInYears, 9 8 } from 'date-fns'; 10 - import { ArrowRight } from 'lucide-react'; 9 + import { ArrowRight, ChevronDown, ChevronUp } from 'lucide-react'; 11 10 import React from 'react'; 12 11 import { Link } from 'react-router-dom'; 13 12 ··· 23 22 loading: boolean; 24 23 } 25 24 26 - interface ManualReviewDashboardInsightsCardWithChangeProps 27 - extends ManualReviewDashboardInsightsCardBaseProps { 25 + interface ManualReviewDashboardInsightsCardWithChangeProps extends ManualReviewDashboardInsightsCardBaseProps { 28 26 change: number | undefined; 29 27 } 30 28 31 - interface ManualReviewDashboardInsightsCardWithLinkProps 32 - extends ManualReviewDashboardInsightsCardBaseProps { 29 + interface ManualReviewDashboardInsightsCardWithLinkProps extends ManualReviewDashboardInsightsCardBaseProps { 33 30 link: string; 34 31 linkTitle: string; 35 32 } ··· 130 127 props.change === 0 131 128 ? 'text-slate-600 bg-slate-100' 132 129 : props.change < 0 133 - ? 'text-red-600 bg-red-100' 134 - : 'text-green-600 bg-green-100' 130 + ? 'text-red-600 bg-red-100' 131 + : 'text-green-600 bg-green-100' 135 132 } p-1 rounded text-sm font-semibold flex items-center`} 136 133 > 137 134 {props.change === 0 ? null : props.change < 0 ? (
+8 -3
client/src/webpages/dashboard/mrt/visualization/ManualReviewDashboardInsightsChart.tsx
··· 11 11 } from '@ant-design/icons'; 12 12 import { gql } from '@apollo/client'; 13 13 import { Tooltip as AntTooltip } from 'antd'; 14 + import { format } from 'date-fns'; 14 15 import flatten from 'lodash/flatten'; 15 16 import keys from 'lodash/keys'; 16 17 import map from 'lodash/map'; ··· 21 22 import sumBy from 'lodash/sumBy'; 22 23 import union from 'lodash/union'; 23 24 import without from 'lodash/without'; 24 - import { format } from 'date-fns'; 25 25 import React, { 26 26 ReactNode, 27 27 useCallback, ··· 698 698 699 699 const formattedData = countsByDay?.map((it) => { 700 700 const obj: { [key: string]: any } = { 701 - ds: format(new Date(parseInt(it.time)), timeDivision === 'HOUR' ? 'yyyy-MM-dd HH:mm' : 'yyyy-MM-dd'), 701 + ds: format( 702 + new Date(parseInt(it.time)), 703 + timeDivision === 'HOUR' ? 'yyyy-MM-dd HH:mm' : 'yyyy-MM-dd', 704 + ), 702 705 }; 703 706 obj[getLineNameFromCount(it)] = it.count; 704 707 return obj; ··· 720 723 ]; 721 724 722 725 type ChartRow = Record<string, string | number>; 723 - const groupedData = formattedDataWithAllDates.reduce<Record<string, ChartRow>>((result, item) => { 726 + const groupedData = formattedDataWithAllDates.reduce< 727 + Record<string, ChartRow> 728 + >((result, item) => { 724 729 const ds = item.ds; 725 730 726 731 if (!(ds in result)) {
+3 -1
client/src/webpages/dashboard/mrt/visualization/TimeToActionChart.tsx
··· 219 219 220 220 const emptyChart = ( 221 221 <div className="flex flex-col items-center justify-center h-full gap-3 p-6 bg-indigo-100 rounded"> 222 - <div className="text-sm text-slate-400">No data available for the selected time period.</div> 222 + <div className="text-sm text-slate-400"> 223 + No data available for the selected time period. 224 + </div> 223 225 <CoopButton 224 226 title="Reset Filters" 225 227 onClick={() => getEmptyFilterState(timeWindow)}
+12 -4
client/src/webpages/dashboard/overview/OverviewChart.tsx
··· 8 8 import { titleCaseEnumString } from '@/utils/string'; 9 9 import { getDateRange } from '@/utils/time'; 10 10 import { gql } from '@apollo/client'; 11 + import { format } from 'date-fns'; 11 12 import flatten from 'lodash/flatten'; 12 13 import keys from 'lodash/keys'; 13 14 import map from 'lodash/map'; ··· 16 17 import sum from 'lodash/sum'; 17 18 import union from 'lodash/union'; 18 19 import without from 'lodash/without'; 19 - import { format } from 'date-fns'; 20 20 import { 21 21 useCallback, 22 22 useEffect, ··· 325 325 326 326 const formattedData = countsPerMetricPerTimeUnit?.map((it) => { 327 327 const obj: { [key: string]: string | number } = { 328 - ds: format(new Date(parseInt(it.time)), timeDivision === 'HOUR' ? 'yyyy-MM-dd HH:mm' : 'yyyy-MM-dd'), 328 + ds: format( 329 + new Date(parseInt(it.time)), 330 + timeDivision === 'HOUR' ? 'yyyy-MM-dd HH:mm' : 'yyyy-MM-dd', 331 + ), 329 332 }; 330 333 obj[getLineNameFromCount(it)] = it.count; 331 334 return obj; ··· 347 350 ]; 348 351 349 352 type ChartRow = Record<string, string | number>; 350 - const groupedData = formattedDataWithAllDates.reduce<Record<string, ChartRow>>((result, item) => { 353 + const groupedData = formattedDataWithAllDates.reduce< 354 + Record<string, ChartRow> 355 + >((result, item) => { 351 356 const ds = item.ds; 352 357 353 358 if (!(ds in result)) { ··· 461 466 </div> 462 467 </div> 463 468 <div className="z-10 flex flex-col w-full h-full min-h-[400px] pb-4"> 464 - {!loading && (finalChartData.length === 0 || uniqueLines.length === 0 || !hasNonZeroData) ? ( 469 + {!loading && 470 + (finalChartData.length === 0 || 471 + uniqueLines.length === 0 || 472 + !hasNonZeroData) ? ( 465 473 emptyChart 466 474 ) : ( 467 475 <ResponsiveContainer width="100%" height={400}>
+3 -1
client/src/webpages/dashboard/overview/OverviewTable.tsx
··· 97 97 98 98 const emptyChart = ( 99 99 <div className="flex flex-col items-center justify-center gap-3 p-6 rounded bg-slate-100"> 100 - <div className="text-sm text-slate-400">No data available for the selected time period.</div> 100 + <div className="text-sm text-slate-400"> 101 + No data available for the selected time period. 102 + </div> 101 103 </div> 102 104 ); 103 105
+2 -2
client/src/webpages/dashboard/rules/dashboard/visualization/RulesDashboardInsights.tsx
··· 2 2 3 3 import { DateRangePicker } from '@/coop-ui/DateRangePicker'; 4 4 import { InvestmentFilled, PieChartAltFilled } from '@/icons'; 5 - import { TriangleAlert } from 'lucide-react'; 6 5 import { truncateAndFormatLargeNumber } from '@/utils/number'; 7 6 import { 8 7 BarChartOutlined, ··· 11 10 PieChartOutlined, 12 11 } from '@ant-design/icons'; 13 12 import { gql } from '@apollo/client'; 13 + import { format } from 'date-fns'; 14 14 import capitalize from 'lodash/capitalize'; 15 15 import flatten from 'lodash/flatten'; 16 16 import groupBy from 'lodash/groupBy'; ··· 26 26 import sumBy from 'lodash/sumBy'; 27 27 import union from 'lodash/union'; 28 28 import without from 'lodash/without'; 29 - import { format } from 'date-fns'; 29 + import { TriangleAlert } from 'lucide-react'; 30 30 import React, { ReactNode, useCallback, useMemo, useState } from 'react'; 31 31 import { 32 32 Area,
+12 -4
client/src/webpages/dashboard/rules/dashboard/visualization/rulesDashboardInsightsChart.tsx
··· 10 10 import Download from '@/icons/lni/Web and Technology/download.svg?react'; 11 11 import { truncateAndFormatLargeNumber } from '@/utils/number'; 12 12 import type { TimeDivisionOptions } from '@/webpages/dashboard/overview/Overview'; 13 + import { format } from 'date-fns'; 13 14 import flatten from 'lodash/flatten'; 14 15 import keys from 'lodash/keys'; 15 16 import map from 'lodash/map'; ··· 18 19 import sumBy from 'lodash/sumBy'; 19 20 import union from 'lodash/union'; 20 21 import without from 'lodash/without'; 21 - import { format } from 'date-fns'; 22 22 import { useCallback, useEffect, useMemo, useState } from 'react'; 23 23 import { CSVLink } from 'react-csv'; 24 24 import { ··· 197 197 198 198 const formattedData = countsByDay?.map((it) => { 199 199 const obj: { [key: string]: any } = { 200 - ds: format(new Date(parseInt(it.time)), timeDivision === 'HOUR' ? 'yyyy-MM-dd HH:mm' : 'yyyy-MM-dd'), 200 + ds: format( 201 + new Date(parseInt(it.time)), 202 + timeDivision === 'HOUR' ? 'yyyy-MM-dd HH:mm' : 'yyyy-MM-dd', 203 + ), 201 204 }; 202 205 obj[getLineNameFromCount(it)] = it.count; 203 206 return obj; ··· 219 222 ]; 220 223 221 224 type ChartRow = Record<string, string | number>; 222 - const groupedData = formattedDataWithAllDates.reduce<Record<string, ChartRow>>((result, item) => { 225 + const groupedData = formattedDataWithAllDates.reduce< 226 + Record<string, ChartRow> 227 + >((result, item) => { 223 228 const ds = item.ds; 224 229 225 230 if (!(ds in result)) { ··· 539 544 </div> 540 545 </div> 541 546 <div className="z-10 flex flex-col w-full h-full min-h-[400px] pb-4"> 542 - {!loading && (finalChartData.length === 0 || uniqueLines.length === 0 || !hasNonZeroData) ? ( 547 + {!loading && 548 + (finalChartData.length === 0 || 549 + uniqueLines.length === 0 || 550 + !hasNonZeroData) ? ( 543 551 emptyChart 544 552 ) : ( 545 553 <ResponsiveContainer width="100%" height={400}>
+3 -5
client/src/webpages/dashboard/rules/info/insights/RuleInsightsActionsChart.tsx
··· 1 1 import { DateRangePicker } from '@/coop-ui/DateRangePicker'; 2 2 import { InvestmentFilled, PieChartAltFilled } from '@/icons'; 3 - import { TriangleAlert } from 'lucide-react'; 4 3 import { truncateAndFormatLargeNumber } from '@/utils/number'; 5 4 import { BarChartOutlined, LineChartOutlined } from '@ant-design/icons'; 6 5 import { gql } from '@apollo/client'; 6 + import { format } from 'date-fns'; 7 7 import last from 'lodash/last'; 8 8 import orderBy from 'lodash/orderBy'; 9 9 import sortBy from 'lodash/sortBy'; 10 10 import sumBy from 'lodash/sumBy'; 11 - import { format } from 'date-fns'; 11 + import { TriangleAlert } from 'lucide-react'; 12 12 import { ReactNode, useCallback, useMemo, useState } from 'react'; 13 13 import { 14 14 Bar, ··· 171 171 ); 172 172 return ( 173 173 <div className="flex flex-col bg-white rounded-lg shadow text-start"> 174 - <div className="p-3 text-white rounded-t-lg bg-primary"> 175 - {label} 176 - </div> 174 + <div className="p-3 text-white rounded-t-lg bg-primary">{label}</div> 177 175 {data.length > 1 && ( 178 176 <div className="flex flex-col"> 179 177 <div className="flex items-center px-3 py-2">
+8 -4
client/src/webpages/dashboard/rules/info/insights/sample_details/RuleInsightsSampleDetailMatchingValues.tsx
··· 49 49 const { loading, error, data } = useGQLMatchingBankNamesQuery({ 50 50 skip: 51 51 !type || 52 - ![MatchingValueType.TEXT_BANK, MatchingValueType.LOCATION_BANK, MatchingValueType.IMAGE_BANK].includes( 53 - type, 54 - ), 52 + ![ 53 + MatchingValueType.TEXT_BANK, 54 + MatchingValueType.LOCATION_BANK, 55 + MatchingValueType.IMAGE_BANK, 56 + ].includes(type), 55 57 }); 56 58 const { textBanks, locationBanks, hashBanks } = data?.myOrg?.banks ?? {}; 57 59 ··· 132 134 case MatchingValueType.IMAGE_BANK: 133 135 return staticValue({ 134 136 text: matchingValues 135 - .imageBankIds!.map((id) => hashBanks?.find((it) => it.id === id)?.name) 137 + .imageBankIds!.map( 138 + (id) => hashBanks?.find((it) => it.id === id)?.name, 139 + ) 136 140 .join(', '), 137 141 outcome: condition.result?.outcome, 138 142 matchedValue: condition.result?.matchedValue,
+1 -1
client/src/webpages/dashboard/rules/rule_form/ReportingRuleForm.tsx
··· 23 23 GQLAction, 24 24 GQLConditionConjunction, 25 25 GQLReportingRuleStatus, 26 + GQLSignal, 26 27 GQLUserPermission, 27 28 useGQLCreateReportingRuleMutation, 28 29 useGQLDeleteReportingRuleMutation, 29 30 useGQLReportingRuleFormOrgDataQuery, 30 31 useGQLReportingRuleQuery, 31 32 useGQLUpdateReportingRuleMutation, 32 - GQLSignal, 33 33 } from '../../../../graphql/generated'; 34 34 import { userHasPermissions } from '../../../../routing/permissions'; 35 35 import useRouteQueryParams from '../../../../routing/useRouteQueryParams';
+16 -16
client/src/webpages/dashboard/rules/rule_form/RuleForm.tsx
··· 1 1 import { Label } from '@/coop-ui/Label'; 2 2 import { Switch } from '@/coop-ui/Switch'; 3 + import { 4 + GQLAction, 5 + GQLConditionConjunction, 6 + GQLRuleStatus, 7 + GQLSignal, 8 + GQLUserPermission, 9 + namedOperations, 10 + useGQLContentRuleFormConfigQuery, 11 + useGQLCreateContentRuleMutation, 12 + useGQLCreateUserRuleMutation, 13 + useGQLDeleteRuleMutation, 14 + useGQLMatchingBankIdsQuery, 15 + useGQLRuleQuery, 16 + useGQLUpdateContentRuleMutation, 17 + useGQLUpdateUserRuleMutation, 18 + } from '@/graphql/generated'; 3 19 import CopyAlt from '@/icons/lni/Web and Technology/copy-alt.svg?react'; 4 20 import TrashCan from '@/icons/lni/Web and Technology/trash-can.svg?react'; 5 21 import { DownOutlined, PlusOutlined, UpOutlined } from '@ant-design/icons'; ··· 22 38 import PolicyDropdown from '../../components/PolicyDropdown'; 23 39 import SubmitButton from '../../components/SubmitButton'; 24 40 25 - import { 26 - GQLAction, 27 - GQLConditionConjunction, 28 - GQLRuleStatus, 29 - GQLUserPermission, 30 - namedOperations, 31 - useGQLContentRuleFormConfigQuery, 32 - useGQLCreateContentRuleMutation, 33 - useGQLCreateUserRuleMutation, 34 - useGQLDeleteRuleMutation, 35 - useGQLMatchingBankIdsQuery, 36 - useGQLRuleQuery, 37 - useGQLUpdateContentRuleMutation, 38 - useGQLUpdateUserRuleMutation, 39 - GQLSignal, 40 - } from '@/graphql/generated'; 41 41 import { userHasPermissions } from '../../../../routing/permissions'; 42 42 import useRouteQueryParams from '../../../../routing/useRouteQueryParams'; 43 43 import { DAY, HOUR, MONTH, WEEK } from '../../../../utils/time';
+5 -3
client/src/webpages/dashboard/rules/rule_form/RuleFormCondition.tsx
··· 4 4 import { 5 5 GQLConditionConjunction, 6 6 GQLScalarType, 7 - GQLValueComparator, 8 7 GQLSignal, 8 + GQLValueComparator, 9 9 } from '../../../../graphql/generated'; 10 10 import { CoopInput } from '../../types/enums'; 11 11 import { 12 12 ConditionInput, 13 13 ConditionLocation, 14 + isConditionSet, 14 15 RuleFormConditionSet, 15 16 RuleFormLeafCondition, 16 - isConditionSet, 17 17 } from '../types'; 18 18 import RuleFormConditionComparator from './condition/comparator/RuleFormConditionComparator'; 19 19 import { getDerivedFieldOutputType } from './condition/input/derivedField'; ··· 246 246 location={location} 247 247 inputScalarType={inputScalarType} 248 248 onUpdateMatchingValues={onUpdateMatchingValues} 249 - allConditions={parentConditionSet.conditions.filter((c): c is RuleFormLeafCondition => !isConditionSet(c))} 249 + allConditions={parentConditionSet.conditions.filter( 250 + (c): c is RuleFormLeafCondition => !isConditionSet(c), 251 + )} 250 252 /> 251 253 <RuleFormConditionComparator 252 254 condition={condition}
+2 -4
client/src/webpages/dashboard/rules/rule_form/RuleFormReducers.tsx
··· 568 568 // Note: this filter isn't technically needed, but in the future we might 569 569 // allow non-custom signals to run on content types, so we keep it here 570 570 // so future devs don't need to remember to add it. 571 - return eligibleSignals.filter( 572 - (it) => 573 - it.type === GQLSignalType.Custom, 574 - ).length; 571 + return eligibleSignals.filter((it) => it.type === GQLSignalType.Custom) 572 + .length; 575 573 }) 576 574 .map((itemType) => ({ 577 575 type: 'FULL_ITEM' as const,
+5 -5
client/src/webpages/dashboard/rules/rule_form/RuleFormUtils.ts
··· 15 15 GQLDerivedFieldSpec, 16 16 GQLLeafConditionFieldsFragment, 17 17 GQLScalarType, 18 + GQLSignal, 18 19 GQLSignalType, 19 20 GQLValueComparator, 20 21 type GQLConditionInput, 21 - GQLSignal, 22 22 } from '../../../../graphql/generated'; 23 23 import { taggedUnionToOneOfInput } from '../../../../graphql/inputHelpers'; 24 24 import { locationAreaToLocationAreaInput } from '../../../../models/locationBank'; ··· 480 480 conditionSetIndex: number, 481 481 ) { 482 482 let newConditionSet = cloneDeep(conditionSet); 483 - 483 + 484 484 if (hasNestedConditionSets(newConditionSet)) { 485 485 const nestedConditionSets = newConditionSet.conditions; 486 - 486 + 487 487 // Only allow deletion if there are multiple condition sets 488 488 if (nestedConditionSets.length > 1) { 489 489 nestedConditionSets.splice(conditionSetIndex, 1); 490 - 490 + 491 491 // If, after removing this condition set, we now only have one ConditionSet 492 492 // left, then we make it a top-level ConditionSet (rather than a ConditionSet 493 493 // that just contains one ConditionSet within it). ··· 501 501 } 502 502 } 503 503 } 504 - 504 + 505 505 return newConditionSet; 506 506 } 507 507
+16 -16
client/src/webpages/dashboard/rules/rule_form/condition/comparator/RuleFormConditionComparator.tsx
··· 30 30 const comparatorTypes = condition.signal?.outputType 31 31 ? outputTypeToComparators(condition.signal.outputType) 32 32 : inputScalarType === GQLScalarType.Number 33 - ? [ 34 - GQLValueComparator.Equals, 35 - GQLValueComparator.NotEqualTo, 36 - GQLValueComparator.GreaterThan, 37 - GQLValueComparator.LessThan, 38 - GQLValueComparator.GreaterThanOrEquals, 39 - GQLValueComparator.LessThanOrEquals, 40 - GQLValueComparator.IsUnavailable, 41 - GQLValueComparator.IsNotProvided, 42 - ] 43 - : [ 44 - GQLValueComparator.Equals, 45 - GQLValueComparator.NotEqualTo, 46 - GQLValueComparator.IsUnavailable, 47 - GQLValueComparator.IsNotProvided, 48 - ]; 33 + ? [ 34 + GQLValueComparator.Equals, 35 + GQLValueComparator.NotEqualTo, 36 + GQLValueComparator.GreaterThan, 37 + GQLValueComparator.LessThan, 38 + GQLValueComparator.GreaterThanOrEquals, 39 + GQLValueComparator.LessThanOrEquals, 40 + GQLValueComparator.IsUnavailable, 41 + GQLValueComparator.IsNotProvided, 42 + ] 43 + : [ 44 + GQLValueComparator.Equals, 45 + GQLValueComparator.NotEqualTo, 46 + GQLValueComparator.IsUnavailable, 47 + GQLValueComparator.IsNotProvided, 48 + ]; 49 49 50 50 // If there is only one valid comparator to choose from, we should set 51 51 // the condition.comparator value to that value by default
+1 -1
client/src/webpages/dashboard/rules/rule_form/condition/input/RuleFormConditionInput.tsx
··· 1 + import { GQLSignal } from '@/graphql/generated'; 1 2 import { Form, Select } from 'antd'; 2 3 3 4 import { selectFilterByLabelOption } from '@/webpages/dashboard/components/antDesignUtils'; 4 5 5 - import { GQLSignal } from '@/graphql/generated'; 6 6 import { safePick } from '../../../../../../utils/misc'; 7 7 import { 8 8 jsonParse,
+2 -1
client/src/webpages/dashboard/rules/rule_form/condition/matching_values/RuleFormConditionMatchingBankInput.tsx
··· 41 41 const isRegexSignal = 42 42 condition.signal?.type && receivesRegexInput(condition.signal.type); 43 43 44 - const { textBankIds, locationBankIds, imageBankIds } = condition.matchingValues ?? {}; 44 + const { textBankIds, locationBankIds, imageBankIds } = 45 + condition.matchingValues ?? {}; 45 46 const bankIds = textBankIds ?? locationBankIds ?? imageBankIds ?? []; 46 47 47 48 const { loading, error, data } = useGQLMatchingBankIdsQuery();
+11 -5
client/src/webpages/dashboard/rules/rule_form/condition/matching_values/RuleFormConditionMediaMatchingValues.tsx
··· 2 2 3 3 import ComponentLoading from '../../../../../../components/common/ComponentLoading'; 4 4 import { selectFilterByLabelOption } from '@/webpages/dashboard/components/antDesignUtils'; 5 + 5 6 import { useGQLHashBanksQuery } from '../../../../../../graphql/generated'; 6 7 import { ConditionLocation, RuleFormLeafCondition } from '../../../types'; 7 8 ··· 15 16 ) => void; 16 17 allConditions?: RuleFormLeafCondition[]; 17 18 }) { 18 - const { condition, location, onUpdateMatchingValues, allConditions = [] } = props; 19 + const { 20 + condition, 21 + location, 22 + onUpdateMatchingValues, 23 + allConditions = [], 24 + } = props; 19 25 const { conditionSetIndex, conditionIndex } = location; 20 26 21 27 const { loading, error, data } = useGQLHashBanksQuery(); ··· 25 31 const selectedBankIds = new Set( 26 32 allConditions 27 33 .filter((c) => c !== condition) // Exclude current condition by reference 28 - .flatMap((c) => c.matchingValues?.imageBankIds ?? []) 34 + .flatMap((c) => c.matchingValues?.imageBankIds ?? []), 29 35 ); 30 36 31 37 if (loading) { ··· 59 65 dropdownMatchSelectWidth={false} 60 66 > 61 67 {hashBanks.map((bank) => ( 62 - <Option 63 - key={bank.id} 64 - value={bank.id} 68 + <Option 69 + key={bank.id} 70 + value={bank.id} 65 71 label={bank.name} 66 72 disabled={selectedBankIds.has(bank.id)} 67 73 >
+8 -3
client/src/webpages/dashboard/rules/rule_form/condition/signal/RuleFormConditionSignal.tsx
··· 1 + import { GQLSignal } from '@/graphql/generated'; 1 2 import { DownOutlined } from '@ant-design/icons'; 2 3 import { Button } from 'antd'; 3 4 import { useState } from 'react'; 4 5 5 - import { GQLSignal } from '@/graphql/generated'; 6 6 import { ConditionLocation, RuleFormLeafCondition } from '../../../types'; 7 7 import RuleFormSignalModal from '../../signal_modal/RuleFormSignalModal'; 8 8 import RuleFormConditionSignalSubcategory from './RuleFormConditionSignalSubcategory'; ··· 14 14 onUpdateSignalSubcategory: (subcategory: string) => void; 15 15 isAutomatedRule?: boolean; 16 16 }) { 17 - const { condition, location, onUpdateSignal, onUpdateSignalSubcategory, isAutomatedRule } = 18 - props; 17 + const { 18 + condition, 19 + location, 20 + onUpdateSignal, 21 + onUpdateSignalSubcategory, 22 + isAutomatedRule, 23 + } = props; 19 24 const eligibleSignals = condition.eligibleSignals; 20 25 const [modalInfo, setModalInfo] = useState<{ 21 26 visible: boolean;
+2 -2
client/src/webpages/dashboard/rules/rule_form/condition/threshold/RuleFormConditionThreshold.tsx
··· 157 157 {renderBooleanThreshold 158 158 ? booleanThreshold 159 159 : renderSelectThreshold 160 - ? selectThreshold([...signalOutputOptions]) 161 - : defaultThreshold} 160 + ? selectThreshold([...signalOutputOptions]) 161 + : defaultThreshold} 162 162 <div className="invisible pb-1 text-xs font-bold"> 163 163 {renderBooleanThreshold ? 'Value' : 'Threshold'} 164 164 </div>
+9 -3
client/src/webpages/dashboard/rules/rule_form/signal_modal/RuleFormSignalModal.tsx
··· 1 + import { GQLSignal, GQLSignalSubcategory } from '@/graphql/generated'; 1 2 import { useEffect, useState } from 'react'; 2 3 3 4 import CoopModal from '../../../components/CoopModal'; 4 5 5 - import { GQLSignal, GQLSignalSubcategory } from '@/graphql/generated'; 6 6 import { rebuildSubcategoryTreeFromGraphQLResponse } from '../../../../../utils/signalUtils'; 7 7 import RuleFormSignalModalSignalDetailView from './RuleFormSignalModalSignalDetailView'; 8 8 import RuleFormSignalModalSignalGallery from './RuleFormSignalModalSignalGallery'; ··· 16 16 selectedSignal?: GQLSignal; 17 17 isAutomatedRule?: boolean; 18 18 }) { 19 - const { visible, allSignals, onSelectSignal, onClose, selectedSignal, isAutomatedRule } = 20 - props; 19 + const { 20 + visible, 21 + allSignals, 22 + onSelectSignal, 23 + onClose, 24 + selectedSignal, 25 + isAutomatedRule, 26 + } = props; 21 27 22 28 const onModalClose = () => { 23 29 setDetailViewSignal(null);
+1 -5
client/src/webpages/dashboard/rules/rule_form/signal_modal/RuleFormSignalModalMenuItem.tsx
··· 1 + import { GQLDisabledInfo, GQLSignal, GQLSignalType } from '@/graphql/generated'; 1 2 import { RightOutlined } from '@ant-design/icons'; 2 3 import { Tooltip } from 'antd'; 3 4 import startCase from 'lodash/startCase'; 4 5 5 - import { 6 - GQLDisabledInfo, 7 - GQLSignalType, 8 - GQLSignal, 9 - } from '@/graphql/generated'; 10 6 import LogoWhiteWithBackground from '../../../../../images/LogoWhiteWithBackground.png'; 11 7 import { INTEGRATION_CONFIGS } from '../../../integrations/integrationConfigs'; 12 8
+5 -5
client/src/webpages/dashboard/rules/rule_form/signal_modal/RuleFormSignalModalSignalDetailView.tsx
··· 1 + import { 2 + GQLSignal, 3 + GQLSignalPricingStructureType, 4 + GQLSignalSubcategory, 5 + } from '@/graphql/generated'; 1 6 import { SignalSubcategory } from '@roostorg/coop-types'; 2 7 import capitalize from 'lodash/capitalize'; 3 8 4 9 import CoopButton from '@/webpages/dashboard/components/CoopButton'; 5 10 6 - import { 7 - GQLSignalPricingStructureType, 8 - GQLSignalSubcategory, 9 - GQLSignal, 10 - } from '@/graphql/generated'; 11 11 import LogoWhiteWithBackground from '../../../../../images/LogoWhiteWithBackground.png'; 12 12 import { INTEGRATION_CONFIGS } from '../../../integrations/integrationConfigs'; 13 13 import { signalDisplayName } from './RuleFormSignalModalMenuItem';
+23 -17
client/src/webpages/dashboard/rules/rule_form/signal_modal/RuleFormSignalModalSignalGallery.tsx
··· 2 2 import { SearchOutlined } from '@ant-design/icons'; 3 3 import { Input } from 'antd'; 4 4 import { useMemo, useState } from 'react'; 5 + 5 6 import { INTEGRATION_CONFIGS } from '../../../integrations/integrationConfigs'; 6 7 import RuleFormSignalModalMenuItem from './RuleFormSignalModalMenuItem'; 7 8 import RuleFormSignalModalNoSearchResults from './RuleFormSignalModalNoSearchResults'; ··· 12 13 onSignalInfoSelected: (signal: GQLSignal) => void; 13 14 isAutomatedRule?: boolean; 14 15 }) { 15 - const { allSignals, onSelectSignal, onSignalInfoSelected, isAutomatedRule } = props; 16 + const { allSignals, onSelectSignal, onSignalInfoSelected, isAutomatedRule } = 17 + props; 16 18 const { data: isDemoOrgData } = useGQLIsDemoOrgQuery(); 17 19 const isDemoOrg = isDemoOrgData?.myOrg?.isDemoOrg ?? false; 18 20 ··· 22 24 () => 23 25 allSignals 24 26 // Show built-in Coop signals (no integration), known integrations (in INTEGRATION_CONFIGS), or plugin integrations (any other string) 25 - .filter((signal) => 26 - signal.integration === null || 27 - INTEGRATION_CONFIGS.some( 28 - (config) => signal.integration === config.name, 29 - ) || 30 - (typeof signal.integration === 'string' && 31 - signal.integration.length > 0), 27 + .filter( 28 + (signal) => 29 + signal.integration === null || 30 + INTEGRATION_CONFIGS.some( 31 + (config) => signal.integration === config.name, 32 + ) || 33 + (typeof signal.integration === 'string' && 34 + signal.integration.length > 0), 32 35 ) 33 36 // Then filter out the text similarity score signals 34 37 .filter((it) => it.type !== 'TEXT_SIMILARITY_SCORE') ··· 61 64 {[...filteredSignals] 62 65 .map((signal) => { 63 66 // Override disabledInfo if signal is restricted for automated rules 64 - const effectiveDisabledInfo = 67 + const effectiveDisabledInfo = 65 68 isAutomatedRule && !signal.allowedInAutomatedRules 66 69 ? { 67 70 __typename: 'DisabledInfo' as const, 68 71 disabled: true, 69 - disabledMessage: 'This signal can only be used in routing rules, not in automated rules with actions.', 72 + disabledMessage: 73 + 'This signal can only be used in routing rules, not in automated rules with actions.', 70 74 } 71 75 : signal.disabledInfo; 72 - 76 + 73 77 return { 74 78 signal, 75 79 effectiveDisabledInfo, 76 80 }; 77 81 }) 78 82 .sort((a, b) => 79 - a.effectiveDisabledInfo.disabled && !b.effectiveDisabledInfo.disabled 83 + a.effectiveDisabledInfo.disabled && 84 + !b.effectiveDisabledInfo.disabled 80 85 ? 1 81 - : !a.effectiveDisabledInfo.disabled && b.effectiveDisabledInfo.disabled 82 - ? -1 83 - : `${a.signal.integration}_${a.signal.name}`.localeCompare( 84 - `${b.signal.integration}_${b.signal.name}`, 85 - ), 86 + : !a.effectiveDisabledInfo.disabled && 87 + b.effectiveDisabledInfo.disabled 88 + ? -1 89 + : `${a.signal.integration}_${a.signal.name}`.localeCompare( 90 + `${b.signal.integration}_${b.signal.name}`, 91 + ), 86 92 ) 87 93 .map(({ signal, effectiveDisabledInfo }) => { 88 94 const staticConfig = INTEGRATION_CONFIGS.find(
+1 -1
client/src/webpages/dashboard/rules/rule_form/signal_modal/RuleFormSignalModalSubcategoryGallery.tsx
··· 1 + import { GQLSignal, GQLSignalSubcategory } from '@/graphql/generated'; 1 2 import { SearchOutlined } from '@ant-design/icons'; 2 3 import { Input, Select } from 'antd'; 3 4 import omit from 'lodash/omit'; 4 5 import { useState } from 'react'; 5 6 6 - import { GQLSignal, GQLSignalSubcategory } from '@/graphql/generated'; 7 7 import { rebuildSubcategoryTreeFromGraphQLResponse } from '../../../../../utils/signalUtils'; 8 8 import RuleFormSignalModalNoSearchResults from './RuleFormSignalModalNoSearchResults'; 9 9 import { RuleFormSignalModalSubcategory } from './RuleFormSignalModalSubcategory';
+1 -1
client/src/webpages/dashboard/rules/types.ts
··· 8 8 GQLLocationAreaInput, 9 9 GQLMatchingValues, 10 10 GQLScalarType, 11 - GQLValueComparator, 12 11 GQLSignal, 12 + GQLValueComparator, 13 13 } from '../../../graphql/generated'; 14 14 import { CoopInput } from '../types/enums'; 15 15 import {
+6 -2
client/src/webpages/settings/AccountSettings.tsx
··· 179 179 }); 180 180 }, 181 181 onCompleted: (data) => { 182 - if (data.changePassword.__typename === 'ChangePasswordSuccessResponse') { 182 + if ( 183 + data.changePassword.__typename === 'ChangePasswordSuccessResponse' 184 + ) { 183 185 toast.success('Password Changed', { 184 186 description: 'Your password has been successfully updated.', 185 187 }); ··· 496 498 </Button> 497 499 <Button 498 500 onClick={handleChangePassword} 499 - disabled={isChangePasswordButtonDisabled || isChangePasswordLoading} 501 + disabled={ 502 + isChangePasswordButtonDisabled || isChangePasswordLoading 503 + } 500 504 loading={isChangePasswordLoading} 501 505 > 502 506 Change Password
+64 -34
client/src/webpages/settings/ApiAuthenticationSettings.tsx
··· 5 5 import { Textarea } from '@/coop-ui/Textarea'; 6 6 import { Tooltip, TooltipContent, TooltipTrigger } from '@/coop-ui/Tooltip'; 7 7 import { Heading, Text } from '@/coop-ui/Typography'; 8 - import { 9 - useGQLApiAuthQuery, 10 - useGQLRotateApiKeyMutation, 11 - useGQLRotateWebhookSigningKeyMutation, 12 - } from '../../graphql/generated'; 13 8 import { Clipboard, Eye, EyeClosed, RotateCcw } from 'lucide-react'; 14 9 import { useState } from 'react'; 15 10 import { Helmet } from 'react-helmet-async'; ··· 17 12 18 13 import FullScreenLoading from '../../components/common/FullScreenLoading'; 19 14 20 - import { GQLUserPermission } from '../../graphql/generated'; 15 + import { 16 + GQLUserPermission, 17 + useGQLApiAuthQuery, 18 + useGQLRotateApiKeyMutation, 19 + useGQLRotateWebhookSigningKeyMutation, 20 + } from '../../graphql/generated'; 21 21 import { userHasPermissions } from '../../routing/permissions'; 22 - 23 22 24 23 const ApiAuthenticationSettings = () => { 25 24 const { data, loading, error, refetch } = useGQLApiAuthQuery(); ··· 62 61 variant="outline" 63 62 size="sm" 64 63 className="mt-3" 65 - onClick={async () => { await refetch(); }} 64 + onClick={async () => { 65 + await refetch(); 66 + }} 66 67 > 67 68 Retry 68 69 </Button> ··· 83 84 return ( 84 85 <div className="p-4 bg-amber-50 border border-amber-200 rounded-lg"> 85 86 <Text size="SM">Unable to load organization. Please try again.</Text> 86 - <Button variant="outline" size="sm" className="mt-3" onClick={async () => { await refetch(); }}> 87 + <Button 88 + variant="outline" 89 + size="sm" 90 + className="mt-3" 91 + onClick={async () => { 92 + await refetch(); 93 + }} 94 + > 87 95 Retry 88 96 </Button> 89 97 </div> ··· 92 100 93 101 const apiKey = data?.apiKey; 94 102 const { publicSigningKey } = org; 95 - 103 + 96 104 // Show the new API key if we have one, otherwise show a message about the existing key 97 - const displayApiKey = newApiKey ?? (apiKey === 'API key exists (hidden for security)' ? 'API key exists (hidden for security)' : 'No API key available'); 105 + const displayApiKey = 106 + newApiKey ?? 107 + (apiKey === 'API key exists (hidden for security)' 108 + ? 'API key exists (hidden for security)' 109 + : 'No API key available'); 98 110 const isNewKey = Boolean(newApiKey); 99 - const isKeyHidden = !newApiKey && apiKey === 'API key exists (hidden for security)'; 111 + const isKeyHidden = 112 + !newApiKey && apiKey === 'API key exists (hidden for security)'; 100 113 101 114 const copyText = (text: string) => { 102 115 navigator.clipboard.writeText(text); ··· 105 118 const handleRotateApiKey = async () => { 106 119 setIsRotating(true); 107 120 setRotationError(null); 108 - 121 + 109 122 try { 110 123 const result = await rotateApiKey({ 111 124 variables: { ··· 116 129 }, 117 130 }); 118 131 119 - if (result.data?.rotateApiKey.__typename === 'RotateApiKeySuccessResponse') { 132 + if ( 133 + result.data?.rotateApiKey.__typename === 'RotateApiKeySuccessResponse' 134 + ) { 120 135 setNewApiKey(result.data.rotateApiKey.apiKey); 121 136 await refetch(); 122 137 } else if (result.data?.rotateApiKey.__typename === 'RotateApiKeyError') { 123 - setRotationError(result.data.rotateApiKey.detail ?? 'Failed to rotate API key'); 138 + setRotationError( 139 + result.data.rotateApiKey.detail ?? 'Failed to rotate API key', 140 + ); 124 141 } 125 142 } catch (err) { 126 143 setRotationError('An error occurred while rotating the API key'); ··· 214 231 {isRotating ? 'Rotating...' : 'Rotate Key'} 215 232 </Button> 216 233 </div> 217 - 234 + 218 235 {newApiKey && ( 219 236 <div className="mb-4 p-4 bg-green-50 border border-green-200 rounded-lg"> 220 237 <Text size="SM" className="text-green-800 mb-2"> 221 - New API key generated successfully! Please copy and store it securely. 238 + New API key generated successfully! Please copy and store it 239 + securely. 222 240 </Text> 223 241 <Input 224 242 type="text" ··· 238 256 /> 239 257 </div> 240 258 )} 241 - 259 + 242 260 {rotationError && ( 243 261 <div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg"> 244 262 <Text size="SM" className="text-red-800"> ··· 246 264 </Text> 247 265 </div> 248 266 )} 249 - 267 + 250 268 <Input 251 269 id="apiKey" 252 270 type={apiKeyVisible && isNewKey ? 'text' : 'password'} 253 271 className={apiKeyVisible && isNewKey ? 'tracking-widest' : undefined} 254 272 value={ 255 - isNewKey && apiKeyVisible 256 - ? displayApiKey 257 - : isKeyHidden 273 + isNewKey && apiKeyVisible 274 + ? displayApiKey 275 + : isKeyHidden 258 276 ? '••••••••••••••••••••••••••••••••••••••••' 259 277 : displayApiKey 260 278 } ··· 283 301 </Button> 284 302 </TooltipTrigger> 285 303 <TooltipContent side="top"> 286 - {isNewKey ? 'Copy to clipboard' : 'Key is hidden for security'} 304 + {isNewKey 305 + ? 'Copy to clipboard' 306 + : 'Key is hidden for security'} 287 307 </TooltipContent> 288 308 </Tooltip> 289 309 </div> 290 310 } 291 311 /> 292 - 312 + 293 313 {isNewKey && ( 294 314 <div className="mt-2 p-3 bg-yellow-50 border border-yellow-200 rounded-md"> 295 315 <div className="flex"> 296 316 <div className="flex-shrink-0"> 297 - <svg className="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor"> 298 - <path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" /> 317 + <svg 318 + className="h-5 w-5 text-yellow-400" 319 + viewBox="0 0 20 20" 320 + fill="currentColor" 321 + > 322 + <path 323 + fillRule="evenodd" 324 + d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" 325 + clipRule="evenodd" 326 + /> 299 327 </svg> 300 328 </div> 301 329 <div className="ml-3"> ··· 304 332 </h3> 305 333 <div className="mt-2 text-sm text-yellow-700"> 306 334 <p> 307 - This is the only time you'll see your API key in plain text. 308 - We only store a hash value for security. Please copy and save this key securely. 335 + This is the only time you'll see your API key in plain text. 336 + We only store a hash value for security. Please copy and 337 + save this key securely. 309 338 </p> 310 339 </div> 311 340 </div> ··· 401 430 } 402 431 /> 403 432 </div> 404 - 433 + 405 434 {/* API Key rotation confirmation dialog */} 406 435 {showRotationDialog && ( 407 436 <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> ··· 410 439 Rotate API Key 411 440 </Heading> 412 441 <Text size="SM" className="mb-6"> 413 - Are you sure you want to rotate your API key? This will generate a new key and 414 - deactivate the current one. Make sure to update any applications using the current key. 442 + Are you sure you want to rotate your API key? This will generate a 443 + new key and deactivate the current one. Make sure to update any 444 + applications using the current key. 415 445 </Text> 416 446 <div className="flex gap-3 justify-end"> 417 447 <Button ··· 442 472 Generate new webhook verification key 443 473 </Heading> 444 474 <Text size="SM" className="mb-6"> 445 - This will generate a new webhook signature verification key. The current key will stop 446 - working for verifying new webhook requests. Update your systems with the new key after 447 - generating it. 475 + This will generate a new webhook signature verification key. The 476 + current key will stop working for verifying new webhook requests. 477 + Update your systems with the new key after generating it. 448 478 </Text> 449 479 <div className="flex gap-3 justify-end"> 450 480 <Button
+3 -11
client/tsconfig.json
··· 8 8 "esModuleInterop": true, 9 9 "skipLibCheck": true, 10 10 "forceConsistentCasingInFileNames": true, 11 - "lib": [ 12 - "dom", 13 - "dom.iterable", 14 - "esnext" 15 - ], 11 + "lib": ["dom", "dom.iterable", "esnext"], 16 12 "allowJs": true, 17 13 "allowSyntheticDefaultImports": true, 18 14 "noFallthroughCasesInSwitch": true, ··· 22 18 "noEmit": true, 23 19 "downlevelIteration": true, 24 20 "paths": { 25 - "@/*": [ 26 - "./src/*" 27 - ] 21 + "@/*": ["./src/*"] 28 22 } 29 23 }, 30 - "include": [ 31 - "src" 32 - ] 24 + "include": ["src"] 33 25 }
+3 -8
client/vite.config.ts
··· 1 1 import path from 'path'; 2 + import react from '@vitejs/plugin-react'; 2 3 import { defineConfig } from 'vite'; 3 - import react from '@vitejs/plugin-react'; 4 + import commonjs from 'vite-plugin-commonjs'; 4 5 import svgr from 'vite-plugin-svgr'; 5 6 import tsconfigPaths from 'vite-tsconfig-paths'; 6 - import commonjs from 'vite-plugin-commonjs'; 7 7 8 8 export default defineConfig({ 9 - plugins: [ 10 - react(), 11 - svgr(), 12 - tsconfigPaths(), 13 - commonjs(), 14 - ], 9 + plugins: [react(), svgr(), tsconfigPaths(), commonjs()], 15 10 resolve: { 16 11 alias: { 17 12 '@': path.resolve(__dirname, './src'),
+2 -2
db/README.md
··· 18 18 ## Creating New Migrations 19 19 20 20 ```shell 21 - # Generate a new migration file 21 + # Generate a new migration file 22 22 node --loader ts-node/esm index.ts generate:migration --db api-server-pg --name "add_users_table" 23 23 ``` 24 24 25 - See the root `migrator/README.md` for concepts on migrations vs seeds. 25 + See the root `migrator/README.md` for concepts on migrations vs seeds.
-2
db/src/configs/clickhouse.ts
··· 1 1 import { readFileSync } from 'fs'; 2 2 import { dirname, join as pathJoin } from 'path'; 3 3 import { fileURLToPath } from 'url'; 4 - 5 4 import { createClient, type ClickHouseClient } from '@clickhouse/client'; 6 5 import { wrapMigration, type DatabaseConfig } from '@roostorg/db-migrator'; 7 6 import type { UmzugStorage } from 'umzug'; ··· 240 239 241 240 return statements; 242 241 } 243 -
+20 -13
db/src/scripts/scylla/2025.12.03T00.00.00.initial-schema.cjs
··· 12 12 'SizeTieredCompactionStrategy', 13 13 'LeveledCompactionStrategy', 14 14 'TimeWindowCompactionStrategy', 15 - 'IncrementalCompactionStrategy' 15 + 'IncrementalCompactionStrategy', 16 16 ]); 17 17 18 18 await query( ··· 55 55 ? 'repair' 56 56 : 'immediate' 57 57 }' } 58 - ${allowedCompactionStrategies.has(process.env.SCYLLA_COMPACTION_STRATEGY ?? '') 59 - ? `AND compaction = { 'class': '${process.env.SCYLLA_COMPACTION_STRATEGY}' }` 60 - : '' 58 + ${ 59 + allowedCompactionStrategies.has( 60 + process.env.SCYLLA_COMPACTION_STRATEGY ?? '', 61 + ) 62 + ? `AND compaction = { 'class': '${process.env.SCYLLA_COMPACTION_STRATEGY}' }` 63 + : '' 61 64 };`); 62 65 63 66 await query(`CREATE MATERIALIZED VIEW IF NOT EXISTS item_submission_by_thread_and_time ··· 88 91 ) WITH CLUSTERING ORDER BY (created_at DESC) 89 92 AND compression = {'sstable_compression': 'LZ4Compressor'} 90 93 AND default_time_to_live = 7776000 91 - ${allowedCompactionStrategies.has(process.env.SCYLLA_COMPACTION_STRATEGY ?? '') 92 - ? `AND compaction = { 'class': '${process.env.SCYLLA_COMPACTION_STRATEGY}' }` 93 - : '' 94 + ${ 95 + allowedCompactionStrategies.has( 96 + process.env.SCYLLA_COMPACTION_STRATEGY ?? '', 97 + ) 98 + ? `AND compaction = { 'class': '${process.env.SCYLLA_COMPACTION_STRATEGY}' }` 99 + : '' 94 100 };`); 95 101 await query(`CREATE INDEX ON user_strikes(org_id)`); 96 102 97 - await query( 98 - 'CREATE INDEX on item_submission_by_thread(item_identifier);', 99 - ); 103 + await query('CREATE INDEX on item_submission_by_thread(item_identifier);'); 100 104 101 105 await query(`CREATE TABLE IF NOT EXISTS rule_action_execution_times ( 102 106 org_id text, ··· 106 110 PRIMARY KEY ((org_id, user_identifier), rule_id) 107 111 ) WITH compression = {'sstable_compression': 'LZ4Compressor'} 108 112 AND default_time_to_live = 7776000 109 - ${allowedCompactionStrategies.has(process.env.SCYLLA_COMPACTION_STRATEGY ?? '') 110 - ? `AND compaction = { 'class': '${process.env.SCYLLA_COMPACTION_STRATEGY}' }` 111 - : '' 113 + ${ 114 + allowedCompactionStrategies.has( 115 + process.env.SCYLLA_COMPACTION_STRATEGY ?? '', 116 + ) 117 + ? `AND compaction = { 'class': '${process.env.SCYLLA_COMPACTION_STRATEGY}' }` 118 + : '' 112 119 };`); 113 120 }; 114 121
+13 -13
db/src/scripts/scylla/2025.12.03T00.00.01.change-compression-to-zstd.cjs
··· 10 10 const query = context.execute.bind(context); 11 11 12 12 /** 13 - * Moving from LZ4 compression to Zstd to improve the compression ratio of 14 - * our data, as scylla storage costs are quite high. Zstd almost always 15 - * gives better space savings at the expense of higher 16 - * compression/decompression times Given that our Scylla cluster has a lot 17 - * of idle CPU, the tradeoff here should only favor us and provide cost 18 - * savings while not stressing out the cluster or extending query times too 19 - * much. A good discussion of the tradeoffs for scylla compression 20 - * strategies is here: 21 - * https://www.scylladb.com/2019/10/07/compression-in-scylla-part-two/ 22 - * 23 - * After testing, confirmed that all materialized views need to be manually 24 - * updated with the desired compression settings (which makes sense). 25 - */ 13 + * Moving from LZ4 compression to Zstd to improve the compression ratio of 14 + * our data, as scylla storage costs are quite high. Zstd almost always 15 + * gives better space savings at the expense of higher 16 + * compression/decompression times Given that our Scylla cluster has a lot 17 + * of idle CPU, the tradeoff here should only favor us and provide cost 18 + * savings while not stressing out the cluster or extending query times too 19 + * much. A good discussion of the tradeoffs for scylla compression 20 + * strategies is here: 21 + * https://www.scylladb.com/2019/10/07/compression-in-scylla-part-two/ 22 + * 23 + * After testing, confirmed that all materialized views need to be manually 24 + * updated with the desired compression settings (which makes sense). 25 + */ 26 26 await query(` 27 27 ALTER TABLE item_submission_by_thread WITH compression = {'sstable_compression': 'ZstdCompressor', 'compression_level': 1, 'chunk_length_in_kb': 16 }; 28 28 `);
+2 -2
docs/development/local.md
··· 320 320 ## Code quality 321 321 322 322 ```sh 323 - npm run lint # ESLint 324 - npm run format # Prettier 323 + npm run lint # ESLint 324 + npm run prettier # Prettier (check only; use `npm run prettier:fix` to write, alias `npm run format`) 325 325 npm run check:prepush # Run before pushing 326 326 ```
+1 -1
integrations.config.example.json
··· 21 21 } 22 22 } 23 23 ] 24 - } 24 + }
+5 -5
migrator/README.md
··· 6 6 7 7 ### Migrations vs Seeds 8 8 9 - | Type | Purpose | When it runs | 10 - | :---- | :---- | :---- | 11 - | **Migration** | Schema changes (tables, columns, indexes) | All environments | 12 - | **Seed** | Test fixtures and environment-specific data | Single environment only | 9 + | Type | Purpose | When it runs | 10 + | :------------ | :------------------------------------------ | :---------------------- | 11 + | **Migration** | Schema changes (tables, columns, indexes) | All environments | 12 + | **Seed** | Test fixtures and environment-specific data | Single environment only | 13 13 14 14 ### How Seeds Work 15 15 ··· 17 17 18 18 ## Usage 19 19 20 - See `db/README.md` for CLI commands and setup instructions. 20 + See `db/README.md` for CLI commands and setup instructions.
+3 -1
package.json
··· 16 16 "db:drop": "cd db && npm i && NODE_OPTIONS=\"--loader ts-node/esm --require dotenv/config\" node src/index.ts drop", 17 17 "check:prepush": "cd server && npm run check:prepush && cd ../client && npm run check:prepush", 18 18 "typecheck": "cd server && npm run typecheck && cd ../client && npx tsc --noEmit", 19 - "format": "prettier --write \"./**/*.{ts,tsx,json}\"", 19 + "prettier": "prettier --check \"./**/*.{ts,tsx,js,jsx,mjs,cjs,json,md,yaml,yml}\"", 20 + "prettier:fix": "prettier --write \"./**/*.{ts,tsx,js,jsx,mjs,cjs,json,md,yaml,yml}\"", 21 + "format": "npm run prettier:fix", 20 22 "generate": "graphql-codegen", 21 23 "generate:watch": "graphql-codegen --watch \"server/graphql/**/**.ts\"", 22 24 "prepare": "husky",
+7 -8
server/README.md
··· 5 5 - `npm start` will start the server locally in watch mode. Just make sure that Redis and Postgres are running locally, and your `.env` file has the relevant connection settings to reach them. 6 6 - `npm run runWorkerOrJob [workerName] | [jobName]` will run a specific worker or job, which aren't run at all when using `npm run start`. The current set of workers and jobs, and therefore legal arguments for this script, are in the `workers_jobs` directory, besides `index.ts` and `dbTypes.ts`. 7 7 8 - ## Tracing/Logging 8 + ## Tracing/Logging 9 9 10 10 Coop uses distributed tracing (OpenTelemetry) for observability. Direct logging via `console.*` is disabled by lint rules. Instead, attach log messages to spans for better correlation and debugging. 11 11 ··· 33 33 Sometimes you may want to capture a unit of work in its own span in which case you can use `addActiveSpan`. 34 34 35 35 ```js 36 - return tracer.addActiveSpan( 37 - { resource: 'dataWarehouse.query', operation: 'query' }, 38 - (span) => { 39 - // do work 40 - } 41 - ); 36 + return tracer.addActiveSpan( 37 + { resource: 'dataWarehouse.query', operation: 'query' }, 38 + (span) => { 39 + // do work 40 + }, 41 + ); 42 42 ``` 43 43 44 44 ### Record Exception ··· 62 62 Tracer.logActiveSpanFailedIfAny(e); 63 63 } 64 64 ``` 65 -
+6 -7
server/bin/get-invite-token.ts
··· 2 2 /* eslint-disable no-console */ 3 3 /** 4 4 * Script to get invite token for a user 5 - * 5 + * 6 6 * Usage: 7 7 * npm run get-invite -- --email "user@example.com" 8 8 */ 9 - 10 9 import yargs from 'yargs'; 11 10 import { hideBin } from 'yargs/helpers'; 12 11 ··· 28 27 const container = bottle.container; 29 28 30 29 try { 31 - const result = await container.KyselyPg 32 - .selectFrom('public.invite_user_tokens') 30 + const result = await container.KyselyPg.selectFrom( 31 + 'public.invite_user_tokens', 32 + ) 33 33 .selectAll() 34 34 .where('email', '=', argv.email) 35 35 .orderBy('created_at', 'desc') ··· 65 65 } catch (error: unknown) { 66 66 console.error('\n❌ Error retrieving invite token:\n'); 67 67 console.error(error); 68 - 68 + 69 69 try { 70 70 await container.closeSharedResourcesForShutdown(); 71 71 } catch (shutdownError) { 72 72 console.error('Error during shutdown:', shutdownError); 73 73 } 74 - 74 + 75 75 process.exit(1); 76 76 } 77 77 } ··· 80 80 console.error('Unhandled error:', error); 81 81 process.exit(1); 82 82 }); 83 -
+1 -1
server/condition_evaluator/condition.ts
··· 18 18 19 19 import { getFieldDerivationCost } from '../services/derivedFieldsService/index.js'; 20 20 import { 21 - type Condition, 22 21 ConditionCompletionOutcome, 23 22 ConditionFailureOutcome, 23 + type Condition, 24 24 type ConditionOutcome, 25 25 type ConditionSet, 26 26 } from '../services/moderationConfigService/index.js';
+12 -9
server/eslint.config.mjs
··· 1 1 import { createRequire } from 'node:module'; 2 - import { FlatCompat } from '@eslint/eslintrc'; 3 2 import { fixupConfigRules, fixupPluginRules } from '@eslint/compat'; 3 + import { FlatCompat } from '@eslint/eslintrc'; 4 4 import functional from 'eslint-plugin-functional'; 5 5 6 6 const require = createRequire(import.meta.url); ··· 12 12 13 13 const functionalPlugin = fixupPluginRules(functional); 14 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, 15 + const flatConfigs = fixupConfigRules(compat.config(legacyConfig)).map( 16 + (config) => 17 + config.plugins?.functional 18 + ? { 19 + ...config, 20 + plugins: { ...config.plugins, functional: functionalPlugin }, 21 + } 22 + : config, 22 23 ); 23 24 24 25 export default [ ··· 34 35 ], 35 36 }, 36 37 ...flatConfigs.map((config) => 37 - config.files ? config : { ...config, files: ['**/*.ts', '**/*.tsx', '**/*.js'] }, 38 + config.files 39 + ? config 40 + : { ...config, files: ['**/*.ts', '**/*.tsx', '**/*.js'] }, 38 41 ), 39 42 ];
-1
server/graphql/customScalars/CoopInputOrString.ts
··· 1 1 import { GraphQLScalarType, Kind } from 'graphql'; 2 2 3 3 import { CoopInput } from '../../services/moderationConfigService/index.js'; 4 - 5 4 import { userInputError } from '../utils/errors.js'; 6 5 7 6 export const CoopInputEnumInverted = Object.fromEntries(
-1
server/graphql/customScalars/Cursor.ts
··· 9 9 type JsonOf, 10 10 } from '../../utils/encoding.js'; 11 11 import { type JSON } from '../../utils/json-schema-types.js'; 12 - 13 12 import { userInputError } from '../utils/errors.js'; 14 13 15 14 /**
-1
server/graphql/customScalars/NonEmptyString.ts
··· 4 4 tryParseNonEmptyString, 5 5 type NonEmptyString, 6 6 } from '../../utils/typescript-types.js'; 7 - 8 7 import { userInputError } from '../utils/errors.js'; 9 8 10 9 /**
+1
server/graphql/customScalars/OpaqueScalarMixin.ts
··· 1 1 import { Kind, type GraphQLScalarType } from 'graphql'; 2 2 import jwt from 'jsonwebtoken'; 3 + 3 4 import { userInputError } from '../utils/errors.js'; 4 5 5 6 const parseOpaqueScalarValue =
+3 -1
server/graphql/customScalars/StringOrFloat.ts
··· 30 30 31 31 function parseStringOrFloatValue(value: unknown) { 32 32 if (typeof value !== 'string' && typeof value !== 'number') { 33 - throw userInputError('StringOrFloat must be a string or number when passed to the server.'); 33 + throw userInputError( 34 + 'StringOrFloat must be a string or number when passed to the server.', 35 + ); 34 36 } 35 37 36 38 // NB: Number('') returns 0, so we have to check for the empty string
+1 -3
server/graphql/datasources/ActionApi.test.ts
··· 11 11 getItemTypeEventuallyConsistent: jest.Mock, 12 12 ) => InstanceType<typeof ActionAPI>; 13 13 14 - function makeApi(overrides?: { 15 - action?: Record<string, unknown>; 16 - }) { 14 + function makeApi(overrides?: { action?: Record<string, unknown> }) { 17 15 const action = overrides?.action ?? { 18 16 id: 'action-1', 19 17 orgId: 'org-1',
+5 -5
server/graphql/datasources/IntegrationApi.ts
··· 1 - 2 1 import { inject, type Dependencies } from '../../iocContainer/index.js'; 2 + 3 3 import '../../services/signalAuthService/index.js'; 4 + 5 + import { getIntegrationRegistry } from '../../services/integrationRegistry/index.js'; 4 6 import { Integration } from '../../services/signalsService/index.js'; 5 7 import { 6 8 CoopError, 7 9 ErrorType, 8 10 type ErrorInstanceData, 9 11 } from '../../utils/errors.js'; 10 - import { getIntegrationRegistry } from '../../services/integrationRegistry/index.js'; 12 + import { type GQLSetIntegrationConfigInput } from '../generated.js'; 11 13 import type { 12 14 IntegrationManifestEntry, 13 15 ModelCard, 14 16 } from './integrationManifests.js'; 15 - import { type GQLSetIntegrationConfigInput } from '../generated.js'; 16 17 17 18 export type TIntegrationConfigWithMetadata = Readonly<{ 18 19 name: string; ··· 64 65 class IntegrationAPI { 65 66 constructor( 66 67 private readonly signalAuthService: Dependencies['SignalAuthService'], 67 - ) { 68 - } 68 + ) {} 69 69 70 70 async setConfig( 71 71 params: GQLSetIntegrationConfigInput,
+1 -3
server/graphql/datasources/InvestigationApi.ts
··· 1 - 2 1 import { inject, type Dependencies } from '../../iocContainer/index.js'; 3 2 import { type ItemSubmissionForGQL } from '../types.js'; 4 3 ··· 10 9 class InvestigationAPI { 11 10 constructor( 12 11 private readonly itemHistoryQueries: Dependencies['ItemHistoryQueries'], 13 - ) { 14 - } 12 + ) {} 15 13 16 14 async getItemHistory(opts: { 17 15 itemId: string;
+2 -3
server/graphql/modules/authentication.ts
··· 6 6 const typeDefs = /* GraphQL */ ` 7 7 type Query { 8 8 me: User @publicResolver 9 - getSSORedirectUrl(emailAddress: String!): String 10 - @publicResolver 9 + getSSORedirectUrl(emailAddress: String!): String @publicResolver 11 10 } 12 11 13 12 type Mutation { ··· 26 25 } 27 26 28 27 union LoginResponse = 29 - LoginSuccessResponse 28 + | LoginSuccessResponse 30 29 | LoginUserDoesNotExistError 31 30 | LoginIncorrectPasswordError 32 31 | LoginSsoRequiredError
+59 -24
server/graphql/modules/hashBanks/resolvers.test.ts
··· 1 1 /* eslint-disable @typescript-eslint/no-explicit-any */ 2 - import { resolvers } from './resolvers.js'; 3 2 import type { HashBank } from '../../../services/hmaService/index.js'; 3 + import { resolvers } from './resolvers.js'; 4 4 5 5 const MOCK_BANK: HashBank = { 6 6 id: 1, ··· 31 31 describe('Mutation.createHashBank', () => { 32 32 it('creates a bank without exchange', async () => { 33 33 const ctx = makeContext(); 34 - const input = { name: 'test bank', description: 'desc', enabled_ratio: 1.0 }; 34 + const input = { 35 + name: 'test bank', 36 + description: 'desc', 37 + enabled_ratio: 1.0, 38 + }; 35 39 36 - const result = await (resolvers.Mutation as any).createHashBank({}, { input }, ctx); 40 + const result = await (resolvers.Mutation as any).createHashBank( 41 + {}, 42 + { input }, 43 + ctx, 44 + ); 37 45 38 46 expect(result).toHaveProperty('data'); 39 47 expect(ctx.services.HMAHashBankService.createBank).toHaveBeenCalledWith( 40 - 'org1', 'test bank', 'desc', 1.0, undefined 48 + 'org1', 49 + 'test bank', 50 + 'desc', 51 + 1.0, 52 + undefined, 41 53 ); 42 - expect(ctx.services.HMAHashBankService.setExchangeCredentials).not.toHaveBeenCalled(); 54 + expect( 55 + ctx.services.HMAHashBankService.setExchangeCredentials, 56 + ).not.toHaveBeenCalled(); 43 57 }); 44 58 45 59 it('creates a bank with exchange and credentials', async () => { ··· 55 69 }, 56 70 }; 57 71 58 - const result = await (resolvers.Mutation as any).createHashBank({}, { input }, ctx); 72 + const result = await (resolvers.Mutation as any).createHashBank( 73 + {}, 74 + { input }, 75 + ctx, 76 + ); 59 77 60 78 expect(result).toHaveProperty('data'); 61 79 expect(ctx.services.HMAHashBankService.createBank).toHaveBeenCalledWith( 62 - 'org1', 'test bank', 'desc', 1.0, 63 - { apiName: 'fb_threatexchange', apiJson: { privacy_group: 123 } } 80 + 'org1', 81 + 'test bank', 82 + 'desc', 83 + 1.0, 84 + { apiName: 'fb_threatexchange', apiJson: { privacy_group: 123 } }, 64 85 ); 65 - expect(ctx.services.HMAHashBankService.setExchangeCredentials).toHaveBeenCalledWith( 66 - 'fb_threatexchange', { api_token: 'tok' } 67 - ); 86 + expect( 87 + ctx.services.HMAHashBankService.setExchangeCredentials, 88 + ).toHaveBeenCalledWith('fb_threatexchange', { api_token: 'tok' }); 68 89 }); 69 90 70 91 it('returns success with warning when credentials fail', async () => { 71 92 const ctx = makeContext({ 72 93 createBank: jest.fn().mockResolvedValue(MOCK_BANK), 73 - setExchangeCredentials: jest.fn().mockRejectedValue(new Error('cred error')), 94 + setExchangeCredentials: jest 95 + .fn() 96 + .mockRejectedValue(new Error('cred error')), 74 97 }); 75 98 const input = { 76 99 name: 'test bank', ··· 83 106 }, 84 107 }; 85 108 86 - const result = await (resolvers.Mutation as any).createHashBank({}, { input }, ctx); 109 + const result = await (resolvers.Mutation as any).createHashBank( 110 + {}, 111 + { input }, 112 + ctx, 113 + ); 87 114 88 115 expect(result).toHaveProperty('data'); 89 116 expect(result.warning).toContain('credentials could not be set'); ··· 103 130 104 131 await (resolvers.Mutation as any).createHashBank({}, { input }, ctx); 105 132 106 - expect(ctx.services.HMAHashBankService.setExchangeCredentials).not.toHaveBeenCalled(); 133 + expect( 134 + ctx.services.HMAHashBankService.setExchangeCredentials, 135 + ).not.toHaveBeenCalled(); 107 136 }); 108 137 }); 109 138 ··· 111 140 it('calls setExchangeCredentials and returns true', async () => { 112 141 const ctx = makeContext(); 113 142 114 - const result = await (resolvers.Mutation as any).updateExchangeCredentials( 143 + const result = await ( 144 + resolvers.Mutation as any 145 + ).updateExchangeCredentials( 115 146 {}, 116 147 { apiName: 'ncmec', credentialsJson: '{"user":"u","password":"p"}' }, 117 - ctx 148 + ctx, 118 149 ); 119 150 120 151 expect(result).toBe(true); 121 - expect(ctx.services.HMAHashBankService.setExchangeCredentials).toHaveBeenCalledWith( 122 - 'ncmec', { user: 'u', password: 'p' } 123 - ); 152 + expect( 153 + ctx.services.HMAHashBankService.setExchangeCredentials, 154 + ).toHaveBeenCalledWith('ncmec', { user: 'u', password: 'p' }); 124 155 }); 125 156 }); 126 157 ··· 137 168 }); 138 169 139 170 const result = await (resolvers as any).HashBank.exchange( 140 - { hma_name: 'COOP_ORG1_TEST_BANK' }, {}, ctx 171 + { hma_name: 'COOP_ORG1_TEST_BANK' }, 172 + {}, 173 + ctx, 141 174 ); 142 175 143 176 expect(result).toEqual(exchangeInfo); 144 - expect(ctx.services.HMAHashBankService.getExchangeForBank).toHaveBeenCalledWith( 145 - 'COOP_ORG1_TEST_BANK' 146 - ); 177 + expect( 178 + ctx.services.HMAHashBankService.getExchangeForBank, 179 + ).toHaveBeenCalledWith('COOP_ORG1_TEST_BANK'); 147 180 }); 148 181 149 182 it('returns null when no exchange is configured', async () => { 150 183 const ctx = makeContext(); 151 184 152 185 const result = await (resolvers as any).HashBank.exchange( 153 - { hma_name: 'COOP_ORG1_STANDALONE' }, {}, ctx 186 + { hma_name: 'COOP_ORG1_STANDALONE' }, 187 + {}, 188 + ctx, 154 189 ); 155 190 156 191 expect(result).toBeNull();
+76 -33
server/graphql/modules/hashBanks/resolvers.ts
··· 1 1 import { isCoopErrorOfType } from '../../../utils/errors.js'; 2 + import type { 3 + GQLMutationResolvers, 4 + GQLQueryResolvers, 5 + } from '../../generated.js'; 2 6 import type { Context } from '../../resolvers.js'; 3 - import type { GQLMutationResolvers, GQLQueryResolvers } from '../../generated.js'; 7 + import { unauthenticatedError } from '../../utils/errors.js'; 4 8 import { gqlErrorResult, gqlSuccessResult } from '../../utils/gqlResult.js'; 5 - import { unauthenticatedError } from '../../utils/errors.js'; 6 9 7 10 interface ExchangeConfigInput { 8 11 api_name: string; ··· 27 30 } 28 31 29 32 try { 30 - return await context.services.HMAHashBankService.getBank(user.orgId, name); 33 + return await context.services.HMAHashBankService.getBank( 34 + user.orgId, 35 + name, 36 + ); 31 37 } catch (e) { 32 38 if (isCoopErrorOfType(e, 'NotFoundError')) { 33 39 return null; ··· 43 49 } 44 50 45 51 try { 46 - return await context.services.HMAHashBankService.getBankById(user.orgId, parseInt(id, 10)); 52 + return await context.services.HMAHashBankService.getBankById( 53 + user.orgId, 54 + parseInt(id, 10), 55 + ); 47 56 } catch (e) { 48 57 if (isCoopErrorOfType(e, 'NotFoundError')) { 49 58 return null; ··· 61 70 return context.services.HMAHashBankService.getExchangeApis(); 62 71 }, 63 72 64 - async exchangeApiSchema(_: unknown, { apiName }: { apiName: string }, context: Context) { 73 + async exchangeApiSchema( 74 + _: unknown, 75 + { apiName }: { apiName: string }, 76 + context: Context, 77 + ) { 65 78 const user = context.getUser(); 66 79 if (!user?.orgId) { 67 80 throw unauthenticatedError('User required.'); ··· 74 87 const Mutation: GQLMutationResolvers<Context> = { 75 88 async createHashBank( 76 89 _: unknown, 77 - { input }: { input: { 78 - name: string; 79 - description?: string | null; 80 - enabled_ratio: number; 81 - exchange?: ExchangeConfigInput | null; 82 - }}, 83 - context: Context 90 + { 91 + input, 92 + }: { 93 + input: { 94 + name: string; 95 + description?: string | null; 96 + enabled_ratio: number; 97 + exchange?: ExchangeConfigInput | null; 98 + }; 99 + }, 100 + context: Context, 84 101 ) { 85 102 const user = context.getUser(); 86 103 if (!user?.orgId) { ··· 92 109 ? { 93 110 apiName: input.exchange.api_name, 94 111 // eslint-disable-next-line no-restricted-syntax 95 - apiJson: JSON.parse(input.exchange.config_json) as Record<string, unknown>, 112 + apiJson: JSON.parse(input.exchange.config_json) as Record< 113 + string, 114 + unknown 115 + >, 96 116 } 97 117 : undefined; 98 118 ··· 101 121 input.name, 102 122 input.description ?? '', 103 123 input.enabled_ratio, 104 - exchangeConfig 124 + exchangeConfig, 105 125 ); 106 126 107 127 let warning: string | undefined; 108 128 if (input.exchange?.credentials_json) { 109 129 try { 110 130 // eslint-disable-next-line no-restricted-syntax 111 - const credData = JSON.parse(input.exchange.credentials_json) as Record<string, unknown>; 131 + const credData = JSON.parse( 132 + input.exchange.credentials_json, 133 + ) as Record<string, unknown>; 112 134 await context.services.HMAHashBankService.setExchangeCredentials( 113 135 input.exchange.api_name, 114 - credData 136 + credData, 115 137 ); 116 138 } catch (credError) { 117 139 // eslint-disable-next-line no-console 118 - console.error('Failed to set exchange credentials during bank creation:', credError); 119 - warning = 'Bank and exchange were created, but credentials could not be set. You can update them from the bank settings page.'; 140 + console.error( 141 + 'Failed to set exchange credentials during bank creation:', 142 + credError, 143 + ); 144 + warning = 145 + 'Bank and exchange were created, but credentials could not be set. You can update them from the bank settings page.'; 120 146 } 121 147 } 122 148 123 - return gqlSuccessResult({ data: bank, warning }, 'MutateHashBankSuccessResponse'); 149 + return gqlSuccessResult( 150 + { data: bank, warning }, 151 + 'MutateHashBankSuccessResponse', 152 + ); 124 153 } catch (e) { 125 154 if (isCoopErrorOfType(e, 'MatchingBankNameExistsError')) { 126 155 return gqlErrorResult(e, '/input/name'); ··· 131 160 132 161 async updateHashBank( 133 162 _: unknown, 134 - { input }: { input: { id: string; name?: string | null; description?: string | null; enabled_ratio?: number | null } }, 135 - context: Context 163 + { 164 + input, 165 + }: { 166 + input: { 167 + id: string; 168 + name?: string | null; 169 + description?: string | null; 170 + enabled_ratio?: number | null; 171 + }; 172 + }, 173 + context: Context, 136 174 ) { 137 175 const user = context.getUser(); 138 176 if (!user?.orgId) { ··· 147 185 name: input.name ?? undefined, 148 186 description: input.description ?? undefined, 149 187 enabled_ratio: input.enabled_ratio ?? undefined, 150 - } 188 + }, 151 189 ); 152 190 return gqlSuccessResult({ data: bank }, 'MutateHashBankSuccessResponse'); 153 191 } catch (e) { ··· 158 196 } 159 197 }, 160 198 161 - async deleteHashBank( 162 - _: unknown, 163 - { id }: { id: string }, 164 - context: Context 165 - ) { 199 + async deleteHashBank(_: unknown, { id }: { id: string }, context: Context) { 166 200 const user = context.getUser(); 167 201 if (!user?.orgId) { 168 202 throw unauthenticatedError('User required.'); ··· 175 209 async updateExchangeCredentials( 176 210 _: unknown, 177 211 { apiName, credentialsJson }: { apiName: string; credentialsJson: string }, 178 - context: Context 212 + context: Context, 179 213 ) { 180 214 const user = context.getUser(); 181 215 if (!user?.orgId) { ··· 184 218 185 219 // eslint-disable-next-line no-restricted-syntax 186 220 const credData = JSON.parse(credentialsJson) as Record<string, unknown>; 187 - await context.services.HMAHashBankService.setExchangeCredentials(apiName, credData); 221 + await context.services.HMAHashBankService.setExchangeCredentials( 222 + apiName, 223 + credData, 224 + ); 188 225 return true; 189 - } 226 + }, 190 227 }; 191 228 192 229 const HashBank = { 193 - async exchange(parent: { hma_name: string }, _args: unknown, context: Context) { 194 - return context.services.HMAHashBankService.getExchangeForBank(parent.hma_name); 230 + async exchange( 231 + parent: { hma_name: string }, 232 + _args: unknown, 233 + context: Context, 234 + ) { 235 + return context.services.HMAHashBankService.getExchangeForBank( 236 + parent.hma_name, 237 + ); 195 238 }, 196 239 }; 197 240 ··· 199 242 Query, 200 243 Mutation, 201 244 HashBank, 202 - }; 245 + };
+6 -3
server/graphql/modules/hashBanks/schema.ts
··· 80 80 } 81 81 82 82 union MutateHashBankResponse = 83 - MutateHashBankSuccessResponse 83 + | MutateHashBankSuccessResponse 84 84 | MatchingBankNameExistsError 85 85 86 86 type Query { ··· 95 95 createHashBank(input: CreateHashBankInput!): MutateHashBankResponse! 96 96 updateHashBank(input: UpdateHashBankInput!): MutateHashBankResponse! 97 97 deleteHashBank(id: ID!): Boolean! 98 - updateExchangeCredentials(apiName: String!, credentialsJson: String!): Boolean! 98 + updateExchangeCredentials( 99 + apiName: String! 100 + credentialsJson: String! 101 + ): Boolean! 99 102 } 100 - `; 103 + `;
+2 -2
server/graphql/modules/locationBank.ts
··· 5 5 type GQLQueryResolvers, 6 6 } from '../generated.js'; 7 7 import { type ResolverMap } from '../resolvers.js'; 8 - import { gqlErrorResult, gqlSuccessResult } from '../utils/gqlResult.js'; 9 8 import { unauthenticatedError } from '../utils/errors.js'; 9 + import { gqlErrorResult, gqlSuccessResult } from '../utils/gqlResult.js'; 10 10 11 11 const typeDefs = /* GraphQL */ ` 12 12 type Query { ··· 33 33 } 34 34 35 35 union MutateLocationBankResponse = 36 - MutateLocationBankSuccessResponse 36 + | MutateLocationBankSuccessResponse 37 37 | LocationBankNameExistsError 38 38 39 39 type MutateLocationBankSuccessResponse {
+1 -1
server/graphql/modules/policy.ts
··· 9 9 type GQLQueryResolvers, 10 10 type GQLUpdatePolicyResponseResolvers, 11 11 } from '../generated.js'; 12 - import { gqlErrorResult, gqlSuccessResult } from '../utils/gqlResult.js'; 13 12 import { unauthenticatedError } from '../utils/errors.js'; 13 + import { gqlErrorResult, gqlSuccessResult } from '../utils/gqlResult.js'; 14 14 15 15 const { partition } = _; 16 16
+3 -3
server/graphql/modules/reportingRule.ts
··· 11 11 type GQLQueryResolvers, 12 12 type GQLReportingRuleResolvers, 13 13 } from '../generated.js'; 14 - import { gqlErrorResult, gqlSuccessResult } from '../utils/gqlResult.js'; 15 14 import { unauthenticatedError } from '../utils/errors.js'; 15 + import { gqlErrorResult, gqlSuccessResult } from '../utils/gqlResult.js'; 16 16 17 17 const typeDefs = /* GraphQL */ ` 18 18 enum ReportingRuleStatus { ··· 67 67 } 68 68 69 69 union CreateReportingRuleResponse = 70 - MutateReportingRuleSuccessResponse 70 + | MutateReportingRuleSuccessResponse 71 71 | ReportingRuleNameExistsError 72 72 73 73 union UpdateReportingRuleResponse = 74 - MutateReportingRuleSuccessResponse 74 + | MutateReportingRuleSuccessResponse 75 75 | ReportingRuleNameExistsError 76 76 | NotFoundError 77 77
+6 -6
server/graphql/modules/rule.ts
··· 26 26 type GQLUserRuleResolvers, 27 27 } from '../generated.js'; 28 28 import { type Context, type ResolverMap } from '../resolvers.js'; 29 - import { gqlErrorResult, gqlSuccessResult } from '../utils/gqlResult.js'; 30 29 import { unauthenticatedError } from '../utils/errors.js'; 30 + import { gqlErrorResult, gqlSuccessResult } from '../utils/gqlResult.js'; 31 31 32 32 const typeDefs = /* GraphQL */ ` 33 33 type Query { ··· 204 204 } 205 205 206 206 union DerivedFieldSource = 207 - DerivedFieldFullItemSource 207 + | DerivedFieldFullItemSource 208 208 | DerivedFieldFieldSource 209 209 | DerivedFieldCoopInputSource 210 210 ··· 447 447 } 448 448 449 449 union CreateContentRuleResponse = 450 - MutateContentRuleSuccessResponse 450 + | MutateContentRuleSuccessResponse 451 451 | RuleNameExistsError 452 452 453 453 union UpdateContentRuleResponse = 454 - MutateContentRuleSuccessResponse 454 + | MutateContentRuleSuccessResponse 455 455 | RuleNameExistsError 456 456 | RuleHasRunningBacktestsError 457 457 | NotFoundError 458 458 459 459 union CreateUserRuleResponse = 460 - MutateUserRuleSuccessResponse 460 + | MutateUserRuleSuccessResponse 461 461 | RuleNameExistsError 462 462 463 463 union UpdateUserRuleResponse = 464 - MutateUserRuleSuccessResponse 464 + | MutateUserRuleSuccessResponse 465 465 | RuleNameExistsError 466 466 | RuleHasRunningBacktestsError 467 467 | NotFoundError
-1
server/graphql/utils/authorization.ts
··· 7 7 } from 'graphql'; 8 8 9 9 import type { Context } from '../resolvers.js'; 10 - 11 10 import { unauthenticatedError } from './errors.js'; 12 11 13 12 export function shouldSkipAuth(
+5 -4
server/graphql/utils/paginationHandler.ts
··· 1 1 import { type GraphQLFieldResolver as Resolver } from 'graphql'; 2 2 3 3 import { type JSON } from '../../utils/json-schema-types.js'; 4 - 5 4 import { userInputError } from './errors.js'; 6 5 7 6 /** ··· 93 92 CursorValue extends JSON, 94 93 Node extends object = object, 95 94 Context = unknown, 96 - Args extends 97 - ConnectionArguments<CursorValue> = ConnectionArguments<CursorValue>, 95 + Args extends ConnectionArguments<CursorValue> = 96 + ConnectionArguments<CursorValue>, 98 97 >( 99 98 fetcher: (args: { 100 99 size: number; ··· 137 136 } 138 137 139 138 if (pageSize > maxPageSize) { 140 - throw userInputError(`Page size must be less than or equal to ${maxPageSize}.`); 139 + throw userInputError( 140 + `Page size must be less than or equal to ${maxPageSize}.`, 141 + ); 141 142 } 142 143 143 144 // Meanwhile, providing both a before and after cursor is also coherent
+1 -1
server/graphql/utils/safeDepthLimit.ts
··· 1 - import depthLimit from 'graphql-depth-limit'; 2 1 import { Kind, type ValidationContext } from 'graphql'; 2 + import depthLimit from 'graphql-depth-limit'; 3 3 4 4 // `graphql-depth-limit@1.1.0` throws when a query references an undefined 5 5 // fragment (it dereferences `undefined.kind`). Swallow so other validation
+3 -3
server/iocContainer/README.md
··· 4 4 5 5 ## Why Dependency Injection? 6 6 7 - - Enables mocking services in tests (required for ESM modules) 8 - - Supports runtime injection based on environment or feature flags 7 + - Enables mocking services in tests (required for ESM modules) 8 + - Supports runtime injection based on environment or feature flags 9 9 - Makes dependencies explicit in the source code 10 10 11 11 ## How It Works ··· 26 26 27 27 ## Why BottleJS? 28 28 29 - BottleJS was chosen over alternatives like Inversify because it supports injecting functions and scalar values, not just class instances. 29 + BottleJS was chosen over alternatives like Inversify because it supports injecting functions and scalar values, not just class instances.
+1 -1
server/iocContainer/utils.ts
··· 2 2 /* eslint-disable @typescript-eslint/no-explicit-any */ 3 3 import type Bottle from '@ethanresnick/bottlejs'; 4 4 5 - import { __throw } from '../utils/misc.js'; 6 5 import { jsonStringify } from '../utils/encoding.js'; 6 + import { __throw } from '../utils/misc.js'; 7 7 import { type Dependencies as Deps } from './index.js'; 8 8 9 9 const DEPENDENCIES = Symbol();
+33 -33
server/lib/cache/Cache.ts
··· 1 - import { EventEmitter } from "events"; 2 - import _ from "lodash"; 1 + import { EventEmitter } from 'events'; 2 + import _ from 'lodash'; 3 3 4 4 import { 5 5 type Entry, 6 6 type NormalizeParamName, 7 7 type NormalizeParamValue, 8 - } from "./types/06_Normalization.js"; 8 + } from './types/06_Normalization.js'; 9 9 import { 10 10 type AnyParams, 11 11 type AnyParamValue, ··· 15 15 type ProducerResultResource, 16 16 type Store, 17 17 type Vary, 18 - } from "./types/index.js"; 19 - import { type Bind1 } from "./types/utils.js"; 18 + } from './types/index.js'; 19 + import { type Bind1 } from './types/utils.js'; 20 20 import { 21 21 normalizeParams, 22 22 normalizeProducerResultResource, 23 23 normalizeVary, 24 - } from "./utils/normalization.js"; 25 - import * as entryUtils from "./utils/normalizedProducerResultResourceHelpers.js"; 26 - import { defaultLoggersByComponent } from "./utils/utils.js"; 24 + } from './utils/normalization.js'; 25 + import * as entryUtils from './utils/normalizedProducerResultResourceHelpers.js'; 26 + import { defaultLoggersByComponent } from './utils/utils.js'; 27 27 28 28 const { sortBy, groupBy } = _; 29 29 30 - type OnRequestAfterClose = "throw" | "return-nothing"; 30 + type OnRequestAfterClose = 'throw' | 'return-nothing'; 31 31 32 32 /** 33 33 * This class implements a cache using a generalized version of HTTP's ··· 53 53 Params extends AnyParams = AnyParams, 54 54 Id extends string = string, 55 55 > { 56 - private readonly logger: Bind1<Logger, "cache">; 56 + private readonly logger: Bind1<Logger, 'cache'>; 57 57 public readonly emitter = new EventEmitter(); 58 58 private closed = false; 59 59 private readonly onGetAfterClose: OnRequestAfterClose; ··· 75 75 } = {}, 76 76 ) { 77 77 const unboundLogger = options.logger ?? defaultLoggersByComponent.cache; 78 - this.logger = unboundLogger.bind(null, "cache"); 79 - this.onGetAfterClose = options.onGetAfterClose ?? "throw"; 80 - this.onStoreAfterClose = options.onStoreAfterClose ?? "throw"; 78 + this.logger = unboundLogger.bind(null, 'cache'); 79 + this.onGetAfterClose = options.onGetAfterClose ?? 'throw'; 80 + this.onStoreAfterClose = options.onStoreAfterClose ?? 'throw'; 81 81 this.normalizeParamName = options.normalizeParamName ?? ((it) => it); 82 82 this.normalizeParamValue = 83 83 options.normalizeParamValue ?? ··· 147 147 validatable: Entry<Content, Validators, Params>[]; 148 148 }> { 149 149 if (this.closed) { 150 - if (this.onGetAfterClose === "throw") { 151 - this.logger("trace", "received request when closed and throwing"); 152 - throw new Error("Store has been closed..."); 150 + if (this.onGetAfterClose === 'throw') { 151 + this.logger('trace', 'received request when closed and throwing'); 152 + throw new Error('Store has been closed...'); 153 153 } 154 154 this.logger( 155 - "trace", 156 - "received request when closed, so returning no entries", 155 + 'trace', 156 + 'received request when closed, so returning no entries', 157 157 ); 158 158 return { 159 159 validatable: [], ··· 164 164 const now = new Date(); 165 165 const normalizedParams = this.normalizeParams(params); 166 166 167 - this.logger("trace", "received request", { id, params, normalizedParams }); 168 - this.logger("trace", "requested entries from the store"); 167 + this.logger('trace', 'received request', { id, params, normalizedParams }); 168 + this.logger('trace', 'requested entries from the store'); 169 169 170 170 const cacheEntries = await this.dataStore.get(id, normalizedParams); 171 171 const classifiedEntries = groupBy(cacheEntries, (it) => ··· 173 173 ); 174 174 175 175 this.logger( 176 - "trace", 177 - "received entries from the store, and classified them", 176 + 'trace', 177 + 'received entries from the store, and classified them', 178 178 classifiedEntries, 179 179 ); 180 180 ··· 188 188 validatable: [], 189 189 }; 190 190 191 - this.logger("trace", "chose/returned this data", res); 191 + this.logger('trace', 'chose/returned this data', res); 192 192 return res; 193 193 } 194 194 ··· 204 204 validatable: validatableEntries, 205 205 }; 206 206 207 - this.logger("trace", "chose/returned this data", res); 207 + this.logger('trace', 'chose/returned this data', res); 208 208 return res; 209 209 } 210 210 ··· 219 219 validatable: validatableEntries, 220 220 }; 221 221 222 - this.logger("trace", "chose/returned this data", res); 222 + this.logger('trace', 'chose/returned this data', res); 223 223 return res; 224 224 } 225 225 ··· 232 232 data: readonly ProducerResultResource<Content, Validators, Params>[], 233 233 ) { 234 234 if (this.closed) { 235 - if (this.onStoreAfterClose === "throw") { 236 - this.logger("trace", "received store request when closed and throwing"); 237 - throw new Error("Store has been closed..."); 235 + if (this.onStoreAfterClose === 'throw') { 236 + this.logger('trace', 'received store request when closed and throwing'); 237 + throw new Error('Store has been closed...'); 238 238 } 239 239 this.logger( 240 - "trace", 241 - "received store request after throwing and doing nothing", 240 + 'trace', 241 + 'received store request after throwing and doing nothing', 242 242 ); 243 243 return; 244 244 } ··· 254 254 }); 255 255 256 256 this.logger( 257 - "trace", 258 - "storing the following entries with (possibly inferred) storeFor times", 257 + 'trace', 258 + 'storing the following entries with (possibly inferred) storeFor times', 259 259 entriesWithTimes, 260 260 ); 261 261 262 262 entriesWithTimes.forEach(({ entry, maxStoreForSeconds }) => { 263 - this.emitter.emit("store", entry, maxStoreForSeconds); 263 + this.emitter.emit('store', entry, maxStoreForSeconds); 264 264 }); 265 265 266 266 return this.dataStore.store(entriesWithTimes);
+8 -8
server/lib/cache/index.ts
··· 3 3 birthDate, 4 4 isFresh, 5 5 isValidatable, 6 - } from "./utils/normalizedProducerResultResourceHelpers.js"; 6 + } from './utils/normalizedProducerResultResourceHelpers.js'; 7 7 8 - export { default as Cache } from "./Cache.js"; 9 - export { default as wrapProducer } from "./utils/wrapProducer.js"; 10 - export { default as collapsedTaskCreator } from "./utils/collapsedTaskCreator.js"; 11 - export { default as RedisStore } from "./stores/RedisStore/RedisStore.js"; 12 - export { default as MemoryStore } from "./stores/MemoryStore/MemoryStore.js"; 13 - export * from "./types/index.js"; 8 + export { default as Cache } from './Cache.js'; 9 + export { default as wrapProducer } from './utils/wrapProducer.js'; 10 + export { default as collapsedTaskCreator } from './utils/collapsedTaskCreator.js'; 11 + export { default as RedisStore } from './stores/RedisStore/RedisStore.js'; 12 + export { default as MemoryStore } from './stores/MemoryStore/MemoryStore.js'; 13 + export * from './types/index.js'; 14 14 15 15 export const entryUtils = { birthDate, age, isValidatable, isFresh }; 16 16 ··· 22 22 requestVariantKeyForVaryKeys, 23 23 VariantKey, 24 24 VaryKeys, 25 - } from "./utils/varyHelpers.js"; 25 + } from './utils/varyHelpers.js';
+3 -3
server/lib/cache/stores/MemoryStore/ExpiringEntryMap.ts
··· 1 - import lruMap from "lru_map"; 1 + import lruMap from 'lru_map'; 2 2 3 3 const { LRUMap } = lruMap; 4 4 ··· 33 33 this.store = numItemsLimit ? new LRUMap(numItemsLimit) : new Map(); 34 34 this.onItemEviction = onItemEviction; 35 35 36 - if (this.store instanceof LRUMap && typeof onItemEviction === "function") { 36 + if (this.store instanceof LRUMap && typeof onItemEviction === 'function') { 37 37 this.store.shift = function () { 38 38 const entry = LRUMap.prototype.shift.call(this); 39 39 if (entry !== undefined) { ··· 145 145 this.onItemEviction?.(value.value, key).catch((_e) => {}); 146 146 } 147 147 148 - return typeof existedBoolOrPriorVal === "boolean" 148 + return typeof existedBoolOrPriorVal === 'boolean' 149 149 ? existedBoolOrPriorVal 150 150 : existedBoolOrPriorVal !== undefined; 151 151 }
+5 -6
server/lib/cache/stores/MemoryStore/MemoryStore.ts
··· 5 5 type NormalizedParams, 6 6 type Store, 7 7 type StoreEntryInput, 8 - } from "../../types/index.js"; 9 - import { type JsonOf, jsonStringify } from "../../utils/utils.js"; 8 + } from '../../types/index.js'; 9 + import { jsonStringify, type JsonOf } from '../../utils/utils.js'; 10 10 import { 11 11 requestVariantKeyForVaryKeys, 12 12 resultVariantKey, 13 13 type VariantKey, 14 14 type VaryKeys, 15 - } from "../../utils/varyHelpers.js"; 16 - import ExpiringEntryMap from "./ExpiringEntryMap.js"; 15 + } from '../../utils/varyHelpers.js'; 16 + import ExpiringEntryMap from './ExpiringEntryMap.js'; 17 17 18 18 // Primary cache key for stored resources. 19 19 type ResourceId = string; ··· 33 33 Content, 34 34 Validators extends AnyValidators = AnyValidators, 35 35 Params extends AnyParams = AnyParams, 36 - > implements Store<Content, Validators, Params> 37 - { 36 + > implements Store<Content, Validators, Params> { 38 37 /** 39 38 * This map stores metadata about each distinct `ResourceId` (i.e., primary 40 39 * cache key) that's stored. Specifically...
+48 -49
server/lib/cache/stores/RedisStore/RedisStore.ts
··· 1 - import { readFileSync } from "fs"; 2 - import { fileURLToPath } from "node:url"; 3 - import path, { dirname } from "path"; 4 - import { type Pipeline, type Redis } from "ioredis"; 5 - import _InternalIORedisScript from "ioredis/built/Script.js"; 6 - import { type Transaction } from "ioredis/built/transaction.js"; 7 - import _ from "lodash"; 8 - import Segment, { TypedStringCmd } from "pipeline-segment"; 9 - import { type Jsonify } from "type-fest"; 1 + import { readFileSync } from 'fs'; 2 + import { fileURLToPath } from 'node:url'; 3 + import path, { dirname } from 'path'; 4 + import { type Pipeline, type Redis } from 'ioredis'; 5 + import _InternalIORedisScript from 'ioredis/built/Script.js'; 6 + import { type Transaction } from 'ioredis/built/transaction.js'; 7 + import _ from 'lodash'; 8 + import Segment, { TypedStringCmd } from 'pipeline-segment'; 9 + import { type Jsonify } from 'type-fest'; 10 10 11 11 import { 12 12 type AnyParams, ··· 19 19 type Store, 20 20 type StoreEntryInput, 21 21 type Vary, 22 - } from "../../types/index.js"; 23 - import { type Bind2, type JSON } from "../../types/utils.js"; 24 - import collapsedTaskCreator from "../../utils/collapsedTaskCreator.js"; 25 - import * as entryUtils from "../../utils/normalizedProducerResultResourceHelpers.js"; 26 - import TimerSet from "../../utils/TimerSet.js"; 27 - import { defaultLoggersByComponent, withRetries } from "../../utils/utils.js"; 22 + } from '../../types/index.js'; 23 + import { type Bind2, type JSON } from '../../types/utils.js'; 24 + import collapsedTaskCreator from '../../utils/collapsedTaskCreator.js'; 25 + import * as entryUtils from '../../utils/normalizedProducerResultResourceHelpers.js'; 26 + import TimerSet from '../../utils/TimerSet.js'; 27 + import { defaultLoggersByComponent, withRetries } from '../../utils/utils.js'; 28 28 import { 29 29 requestVariantKeyForVaryKeys, 30 30 resultVariantKey, 31 31 type VaryKeys, 32 - } from "../../utils/varyHelpers.js"; 32 + } from '../../utils/varyHelpers.js'; 33 33 34 34 const { throttle } = _; 35 35 const Script = _InternalIORedisScript.default; ··· 110 110 T extends JSON, 111 111 U extends AnyValidators, 112 112 V extends AnyParams, 113 - > implements Store<T, U, V> 114 - { 113 + > implements Store<T, U, V> { 115 114 private readonly keyPrefix: string; 116 115 private readonly expectedClockSkew: number; 117 116 private readonly timerSet = new TimerSet(); 118 117 119 - private readonly logWarn: Bind2<Logger, "redis-store", "warn">; 120 - private readonly logTrace: Bind2<Logger, "redis-store", "trace">; 121 - private readonly logError: Bind2<Logger, "redis-store", "error">; 118 + private readonly logWarn: Bind2<Logger, 'redis-store', 'warn'>; 119 + private readonly logTrace: Bind2<Logger, 'redis-store', 'trace'>; 120 + private readonly logError: Bind2<Logger, 'redis-store', 'error'>; 122 121 123 122 /** 124 123 * @param redis An ioredis instance. ··· 151 150 } = {}, 152 151 ) { 153 152 const unboundLogger = 154 - options.logger ?? defaultLoggersByComponent["redis-store"]; 153 + options.logger ?? defaultLoggersByComponent['redis-store']; 155 154 156 - this.keyPrefix = options.keyPrefix ?? ""; 155 + this.keyPrefix = options.keyPrefix ?? ''; 157 156 this.expectedClockSkew = options.expectedClockSkew ?? 5; // 5s. 158 - this.logWarn = unboundLogger.bind(null, "redis-store", "warn"); 159 - this.logTrace = unboundLogger.bind(null, "redis-store", "trace"); 160 - this.logError = unboundLogger.bind(null, "redis-store", "error"); 157 + this.logWarn = unboundLogger.bind(null, 'redis-store', 'warn'); 158 + this.logTrace = unboundLogger.bind(null, 'redis-store', 'trace'); 159 + this.logError = unboundLogger.bind(null, 'redis-store', 'error'); 161 160 } 162 161 163 162 /** ··· 172 171 * @param parts Array of string (hierarchical) segments to put into the key. 173 172 */ 174 173 private key(parts: readonly string[]) { 175 - return [this.keyPrefix, ...parts].join(":"); 174 + return [this.keyPrefix, ...parts].join(':'); 176 175 } 177 176 178 177 /** ··· 184 183 * and {@see {@link requestVariantKeyForVaryKeys}}. 185 184 */ 186 185 private redisKeyForVaryKeysSets(resourceId: string) { 187 - return this.key(["r", resourceId, "varyKeysSets"]); 186 + return this.key(['r', resourceId, 'varyKeysSets']); 188 187 } 189 188 190 189 private redisKeyForEntryKeys(resourceId: string) { 191 - return this.key(["r", resourceId, "entryKeys"]); 190 + return this.key(['r', resourceId, 'entryKeys']); 192 191 } 193 192 194 193 private redisKeyForVariant(resourceId: string, variantKey: string) { 195 - return this.key(["r", resourceId, "v", variantKey]); 194 + return this.key(['r', resourceId, 'v', variantKey]); 196 195 } 197 196 198 197 /** ··· 204 203 [ 205 204 // Pass the array of keys as a single argument, rather than using them 206 205 // as the arguments list, to work around https://github.com/luin/ioredis/issues/801 207 - TypedStringCmd("mget", [ 206 + TypedStringCmd('mget', [ 208 207 variantKeys.map((k) => this.redisKeyForVariant(id, k)), 209 208 ]), 210 209 ], ··· 224 223 // load might come back empty in a few cases, but, in most cases, it'll 225 224 // save us from having to make a second call altogether. 226 225 this.logTrace( 227 - "querying for default variant and param name sets for id", 226 + 'querying for default variant and param name sets for id', 228 227 id, 229 228 ); 230 229 231 230 const [varyKeysSets, emptyVaryVariant] = await Segment.from( 232 - [TypedStringCmd("smembers", [this.redisKeyForVaryKeysSets(id)])], 231 + [TypedStringCmd('smembers', [this.redisKeyForVaryKeysSets(id)])], 233 232 ([varyKeysSetsResult]) => { 234 233 // If we got an empty varyKeysSetsResult, handle the fact that that 235 234 // could be because we don't store the varyKeysSets key in redis at all ··· 245 244 .append(this.getVariantsSegment(id, [EMPTY_VARY_VARIANT_KEY])) 246 245 .run(this.redis); 247 246 248 - this.logTrace("got (processed) results", { 247 + this.logTrace('got (processed) results', { 249 248 emptyVaryVariant, 250 249 varyKeysSets, 251 250 }); ··· 254 253 requestVariantKeyForVaryKeys(normalizedParams, it), 255 254 ); 256 255 257 - this.logTrace("computed potential variant keys", { 256 + this.logTrace('computed potential variant keys', { 258 257 variantKeys, 259 258 normalizedParams, 260 259 }); ··· 264 263 variantKeysSet.delete(EMPTY_VARY_VARIANT_KEY); // TODO: necessary? 265 264 const unfetchedVariantKeys = [...variantKeysSet.values()]; 266 265 this.logTrace( 267 - "if any, fetching unfetched variants (second round trip)", 266 + 'if any, fetching unfetched variants (second round trip)', 268 267 unfetchedVariantKeys, 269 268 ); 270 269 ··· 277 276 ...extraEntries, 278 277 ]; 279 278 280 - this.logTrace("returning entries created from found data", res); 279 + this.logTrace('returning entries created from found data', res); 281 280 return res; 282 281 } 283 282 284 283 public async store(entriesWithTimes: readonly StoreEntryInput<T, U, V>[]) { 285 - this.logTrace("storing entries", entriesWithTimes); 284 + this.logTrace('storing entries', entriesWithTimes); 286 285 287 286 // Group the provided entries by their resource id, and, within each 288 287 // resource id, by their variantKey. If multiple entries for the same ··· 305 304 const entryForVariant = variantsToStoreForId[variantKey].entry; 306 305 if (entryForVariant) { 307 306 this.logWarn( 308 - "unable to store two entries for the same variant; one will be ignored", 307 + 'unable to store two entries for the same variant; one will be ignored', 309 308 { conflictingEntries: [entryForVariant, entry] }, 310 309 ); 311 310 } ··· 396 395 pipeline.zadd( 397 396 this.redisKeyForEntryKeys(id), 398 397 maxStoreForSeconds === Infinity 399 - ? "inf" 398 + ? 'inf' 400 399 : String(absoluteExpireTimeMs), 401 400 redisEntryKeyForVariant, 402 401 ); ··· 447 446 // `pipeline._queue.map(({ name, args }: any) => ({ name, args }))`, but 448 447 // that's kinda heavy for this, which is in the performance critical path. 449 448 // Users should look at their redis logs instead. 450 - this.logTrace("about to run pipeline to store all provided entries"); 449 + this.logTrace('about to run pipeline to store all provided entries'); 451 450 await tryPipeline(pipeline); 452 - this.logTrace("stored entries successfully"); 451 + this.logTrace('stored entries successfully'); 453 452 } 454 453 455 454 /** ··· 484 483 // starts, on the assumption that the items in the entryKeys key end with 485 484 // the variantKey. The +2 is +1 to account for lua's 1-based indexing, and 486 485 // +1 again because of the colon. 487 - this.redisKeyForVariant(id, "").length + 1, 486 + this.redisKeyForVariant(id, '').length + 1, 488 487 ]; 489 488 const expectedClockSkew = this.expectedClockSkew; 490 - this.logTrace("cleaning up resource", { id, args, expectedClockSkew }); 489 + this.logTrace('cleaning up resource', { id, args, expectedClockSkew }); 491 490 return cleanupResourceScript.execute(this.redis, args, {}); 492 491 } 493 492 ··· 504 503 this.redisKeyForVaryKeysSets(id), 505 504 this.redisKeyForVariant(id, EMPTY_VARY_VARIANT_KEY), 506 505 ]; 507 - this.logTrace("deleting entries for id", { args }); 506 + this.logTrace('deleting entries for id', { args }); 508 507 return deleteResourceScript.execute(this.redis, args, {}); 509 508 } 510 509 ··· 516 515 const __dirname = dirname(fileURLToPath(import.meta.url)); 517 516 518 517 const deleteResourceScript = new Script( 519 - readFileSync(path.join(__dirname, "./lua/deleteResource.lua"), { 520 - encoding: "utf-8", 518 + readFileSync(path.join(__dirname, './lua/deleteResource.lua'), { 519 + encoding: 'utf-8', 521 520 }), 522 521 3, 523 522 ); 524 523 525 524 const cleanupResourceScript = new Script( 526 - readFileSync(path.join(__dirname, "./lua/cleanupResource.lua"), { 527 - encoding: "utf-8", 525 + readFileSync(path.join(__dirname, './lua/cleanupResource.lua'), { 526 + encoding: 'utf-8', 528 527 }), 529 528 2, 530 529 );
+1 -1
server/lib/cache/types/01_Params.ts
··· 1 - import { type JSON } from "./utils.js"; 1 + import { type JSON } from './utils.js'; 2 2 3 3 /** 4 4 * A request for a potentially-cached value can include a set of "params" (i.e.,
+1 -1
server/lib/cache/types/02_Validators.ts
··· 1 - import { type JSON } from "./utils.js"; 1 + import { type JSON } from './utils.js'; 2 2 3 3 /** 4 4 * Validators are identifiers (like a last-modified date, etag, or row version)
+1 -1
server/lib/cache/types/03_ConsumerRequest.ts
··· 1 - import { type AnyParams } from "./01_Params.js"; 1 + import { type AnyParams } from './01_Params.js'; 2 2 3 3 /** 4 4 * A consumer's request. Not surprising.
+3 -3
server/lib/cache/types/04_ProducerResult.ts
··· 1 - import type { AnyParams } from "./01_Params.js"; 2 - import { type AnyValidators } from "./02_Validators.js"; 3 - import { type NormalizedProducerResultResource } from "./index.js"; 1 + import type { AnyParams } from './01_Params.js'; 2 + import { type AnyValidators } from './02_Validators.js'; 3 + import { type NormalizedProducerResultResource } from './index.js'; 4 4 5 5 /** 6 6 * ProducerResult represents the shape of messages returned by a service
+6 -6
server/lib/cache/types/05_RequestPairedProducer.ts
··· 1 - import { type AnyParams } from "./01_Params.js"; 2 - import { type AnyValidators } from "./02_Validators.js"; 3 - import { type ConsumerRequest } from "./03_ConsumerRequest.js"; 4 - import { type ProducerResult } from "./04_ProducerResult.js"; 1 + import { type AnyParams } from './01_Params.js'; 2 + import { type AnyValidators } from './02_Validators.js'; 3 + import { type ConsumerRequest } from './03_ConsumerRequest.js'; 4 + import { type ProducerResult } from './04_ProducerResult.js'; 5 5 6 6 /** 7 7 * Helper type combining ConsumerRequest and RequestPairedProducerResult. ··· 25 25 T, 26 26 U extends AnyValidators, 27 27 V extends AnyParams, 28 - > = Omit<ProducerResult<T, U, V>, "id"> & { 29 - id?: ProducerResult<T, U, V>["id"]; 28 + > = Omit<ProducerResult<T, U, V>, 'id'> & { 29 + id?: ProducerResult<T, U, V>['id']; 30 30 };
+10 -10
server/lib/cache/types/06_Normalization.ts
··· 1 - import { type Tagged } from "type-fest"; 1 + import { type Tagged } from 'type-fest'; 2 2 3 - import { type AnyParams, type AnyParamValue } from "./01_Params.js"; 4 - import { type AnyValidators } from "./02_Validators.js"; 5 - import { type ProducerResultResource, type Vary } from "./04_ProducerResult.js"; 6 - import { type ProducerDirectives } from "./index.js"; 3 + import { type AnyParams, type AnyParamValue } from './01_Params.js'; 4 + import { type AnyValidators } from './02_Validators.js'; 5 + import { type ProducerResultResource, type Vary } from './04_ProducerResult.js'; 6 + import { type ProducerDirectives } from './index.js'; 7 7 8 8 /** 9 9 * @fileoverview When the Cache asks the store to check for cached results that ··· 32 32 */ 33 33 export type NormalizedParams<Params extends AnyParams> = Tagged< 34 34 Params, 35 - "NormalizedParams" 35 + 'NormalizedParams' 36 36 >; 37 37 38 38 export type NormalizeParamName<Params extends AnyParams> = ( ··· 49 49 // The Vary object, after param names and values have been normalized. 50 50 export type NormalizedVary<Params extends AnyParams> = Tagged< 51 51 Vary<Params>, 52 - "NormalizedVary" 52 + 'NormalizedVary' 53 53 >; 54 54 55 55 export type NormalizedMaxStale = Tagged< 56 56 [number, number, number], 57 - "NormalizedMaxStale" 57 + 'NormalizedMaxStale' 58 58 >; 59 59 60 60 export type NormalizedProducerResult< ··· 82 82 Params extends AnyParams, 83 83 > = Omit< 84 84 ProducerResultResource<T, Validators, Params>, 85 - "vary" | "validators" | "directives" | "initialAge" | "date" 85 + 'vary' | 'validators' | 'directives' | 'initialAge' | 'date' 86 86 > & { 87 87 vary: NormalizedVary<Params>; 88 88 validators: Partial<Validators>; ··· 103 103 104 104 export type NormalizedProducerDirectives = Omit< 105 105 ProducerDirectives, 106 - "maxStale" 106 + 'maxStale' 107 107 > & { 108 108 maxStale?: NormalizedMaxStale; 109 109 };
+4 -4
server/lib/cache/types/06_Store.ts
··· 1 - import { variantMatchesRequest } from "../utils/varyHelpers.js"; 2 - import { type AnyParams } from "./01_Params.js"; 3 - import { type AnyValidators } from "./02_Validators.js"; 4 - import { type Entry, type NormalizedParams } from "./06_Normalization.js"; 1 + import { variantMatchesRequest } from '../utils/varyHelpers.js'; 2 + import { type AnyParams } from './01_Params.js'; 3 + import { type AnyValidators } from './02_Validators.js'; 4 + import { type Entry, type NormalizedParams } from './06_Normalization.js'; 5 5 6 6 /** 7 7 * NB: The store shouldn't mutate its input here at all, but we can't use
+5 -5
server/lib/cache/types/07_Logger.ts
··· 5 5 */ 6 6 export type Logger = ( 7 7 component: ComponentName, 8 - level: "trace" | "debug" | "info" | "warn" | "error" | "fatal", 8 + level: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal', 9 9 message: string, 10 10 data?: unknown, 11 11 ) => void; ··· 13 13 // The ids for the different components in our package that can log. 14 14 export type ComponentName = (typeof components)[number]; 15 15 export const components = [ 16 - "cache", 17 - "wrap-producer", 18 - "collapsed-task-creator", 19 - "redis-store", 16 + 'cache', 17 + 'wrap-producer', 18 + 'collapsed-task-creator', 19 + 'redis-store', 20 20 ] as const;
+8 -8
server/lib/cache/types/index.ts
··· 2 2 // various components that make up the caching system. 3 3 // 4 4 // Those that are meant to be public are re-exported below. 5 - export { type AnyParams, type AnyParamValue } from "./01_Params.js"; 6 - export { type AnyValidators } from "./02_Validators.js"; 5 + export { type AnyParams, type AnyParamValue } from './01_Params.js'; 6 + export { type AnyValidators } from './02_Validators.js'; 7 7 export { 8 8 type ConsumerRequest, 9 9 type ConsumerDirectives, 10 - } from "./03_ConsumerRequest.js"; 10 + } from './03_ConsumerRequest.js'; 11 11 export { 12 12 type Vary, 13 13 type ProducerResult, 14 14 type ProducerDirectives, 15 15 type ProducerResultResource, 16 - } from "./04_ProducerResult.js"; 16 + } from './04_ProducerResult.js'; 17 17 export { 18 18 type RequestPairedProducerResult, 19 19 type RequestPairedProducer, 20 - } from "./05_RequestPairedProducer.js"; 21 - export { type Store, type StoreEntryInput } from "./06_Store.js"; 20 + } from './05_RequestPairedProducer.js'; 21 + export { type Store, type StoreEntryInput } from './06_Store.js'; 22 22 export { 23 23 type NormalizedParams, 24 24 type NormalizedVary, 25 25 type NormalizedProducerResultResource, 26 26 type Entry, 27 27 type NormalizedProducerDirectives, 28 - } from "./06_Normalization.js"; 29 - export { type Logger, components } from "./07_Logger.js"; 28 + } from './06_Normalization.js'; 29 + export { type Logger, components } from './07_Logger.js';
+2 -2
server/lib/cache/utils/TimerSet.ts
··· 1 - import { delay } from "./utils.js"; 1 + import { delay } from './utils.js'; 2 2 3 3 /** 4 4 * Stores/runs a set of timers and can alert callers when all timers ··· 16 16 ...args: any[] 17 17 ): NodeJS.Timeout { 18 18 if (this.closed) { 19 - throw new Error("TimerSet is closed. New timers cannot be added."); 19 + throw new Error('TimerSet is closed. New timers cannot be added.'); 20 20 } 21 21 22 22 const timer = setTimeout(() => {
+9 -9
server/lib/cache/utils/collapsedTaskCreator.ts
··· 1 - import { type Logger } from "../types/index.js"; 2 - import { defaultLoggersByComponent } from "../utils/utils.js"; 1 + import { type Logger } from '../types/index.js'; 2 + import { defaultLoggersByComponent } from '../utils/utils.js'; 3 3 4 4 /** 5 5 * Imagine you have an function that, when called, kicks off a task, and ··· 43 43 // E.g., how to verify that, if JSON.stringify is used, the value is properly 44 44 // stringifyable? Would it be better to just have no default key option? 45 45 key: (args: Args) => any = JSON.stringify.bind(JSON), 46 - logger: Logger = defaultLoggersByComponent["collapsed-task-creator"], 46 + logger: Logger = defaultLoggersByComponent['collapsed-task-creator'], 47 47 ) { 48 48 // Tuple of [PromiseForTaskResult, taskStartTimestamp]. 49 49 const pendingTasks = new Map<any, [Promise<Result>, number]>(); 50 - const logTrace = logger.bind(null, "collapsed-task-creator", "trace"); 50 + const logTrace = logger.bind(null, 'collapsed-task-creator', 'trace'); 51 51 52 52 return async (...args: Args) => { 53 53 const taskKey = key(args); 54 54 const res = pendingTasks.get(taskKey); 55 55 const now = Date.now(); 56 - logTrace("requested = new state for taskKey/args", { args, taskKey }); 56 + logTrace('requested = new state for taskKey/args', { args, taskKey }); 57 57 58 58 if (!res || now - res[1] > collapseTasksMs) { 59 59 logTrace( 60 60 res 61 61 ? "started new task; there _was_ an in-progress one, but it's too old" 62 - : "started new task b/c there was no in-progress task for these args", 62 + : 'started new task b/c there was no in-progress task for these args', 63 63 args, 64 64 ); 65 65 ··· 69 69 // overwritten for taking longer than collapseTasksMs.) 70 70 const pendingValueNow = pendingTasks.get(taskKey); 71 71 if (pendingValueNow && pendingValueNow[0] === taskRes) { 72 - logTrace("completed = new state for taskKey/args", { args, taskKey }); 72 + logTrace('completed = new state for taskKey/args', { args, taskKey }); 73 73 pendingTasks.delete(taskKey); 74 74 } 75 75 }); ··· 77 77 // Save the new task as a pending task. This will be _replacing_ 78 78 // an existing pending task for this key if the other was too old. 79 79 pendingTasks.set(taskKey, [taskRes, now]); 80 - logTrace("pending = new state for taskKey/args", { args, taskKey }); 80 + logTrace('pending = new state for taskKey/args', { args, taskKey }); 81 81 return taskRes; 82 82 } 83 83 84 84 logTrace( 85 - "reusing result from prior, still-in-progress run of task for args/taskKey", 85 + 'reusing result from prior, still-in-progress run of task for args/taskKey', 86 86 { args, taskKey }, 87 87 ); 88 88 return res[0];
+2 -2
server/lib/cache/utils/normalization.ts
··· 6 6 type NormalizedVary, 7 7 type NormalizeParamName, 8 8 type NormalizeParamValue, 9 - } from "../types/06_Normalization.js"; 9 + } from '../types/06_Normalization.js'; 10 10 import { 11 11 type AnyParams, 12 12 type AnyParamValue, ··· 14 14 type ProducerResult, 15 15 type ProducerResultResource, 16 16 type Vary, 17 - } from "../types/index.js"; 17 + } from '../types/index.js'; 18 18 19 19 export function normalizeProducerResult< 20 20 Content,
+9 -9
server/lib/cache/utils/normalizedProducerResultResourceHelpers.ts
··· 1 - import _ from "lodash"; 1 + import _ from 'lodash'; 2 2 3 3 import { 4 4 type AnyParams, 5 5 type AnyValidators, 6 6 type ConsumerDirectives, 7 - } from "../index.js"; 7 + } from '../index.js'; 8 8 import { 9 9 type NormalizedMaxStale, 10 10 type NormalizedProducerResultResource, 11 - } from "../types/06_Normalization.js"; 12 - import { normalizeMaxStale } from "./normalization.js"; 13 - import { mapTuple } from "./utils.js"; 11 + } from '../types/06_Normalization.js'; 12 + import { normalizeMaxStale } from './normalization.js'; 13 + import { mapTuple } from './utils.js'; 14 14 15 15 const { zipWith } = _; 16 16 ··· 68 68 // could be helpful in fetching an updated ProducerResult that is usable. 69 69 // This was originally a numeric enum, but strings made for way better logs. 70 70 export const enum EntryClassification { 71 - Usable = "Usable", 72 - UsableWhileRevalidate = "UsableWhileRevalidate", 73 - UsableIfError = "UsableIfError", 74 - Unusable = "Unusable", 71 + Usable = 'Usable', 72 + UsableWhileRevalidate = 'UsableWhileRevalidate', 73 + UsableIfError = 'UsableIfError', 74 + Unusable = 'Unusable', 75 75 } 76 76 77 77 /**
+6 -6
server/lib/cache/utils/utils.ts
··· 1 - import { setTimeout } from "timers/promises"; 2 - import debug from "debug"; 3 - import stringify from "safe-stable-stringify"; 4 - import { type JsonValue, type Tagged } from "type-fest"; 1 + import { setTimeout } from 'timers/promises'; 2 + import debug from 'debug'; 3 + import stringify from 'safe-stable-stringify'; 4 + import { type JsonValue, type Tagged } from 'type-fest'; 5 5 6 - import { components, type Logger } from "../types/index.js"; 6 + import { components, type Logger } from '../types/index.js'; 7 7 8 8 export const defaultLoggersByComponent = Object.fromEntries( 9 9 components.map( ··· 114 114 } 115 115 116 116 declare const meta: unique symbol; 117 - export type JsonOf<T> = Tagged<string, "JSON"> & { readonly [meta]: T }; 117 + export type JsonOf<T> = Tagged<string, 'JSON'> & { readonly [meta]: T }; 118 118 119 119 // Highly incomplete code for interoperating with raw HTTP responses, 120 120 // by doing all the necessary header parsing and age inference.
+4 -4
server/lib/cache/utils/varyHelpers.ts
··· 1 - import { type Tagged } from "type-fest"; 1 + import { type Tagged } from 'type-fest'; 2 2 3 3 import type { 4 4 AnyParams, 5 5 AnyParamValue, 6 6 NormalizedParams, 7 7 NormalizedVary, 8 - } from "../types/index.js"; 9 - import { type JsonOf, jsonStringify } from "./utils.js"; 8 + } from '../types/index.js'; 9 + import { jsonStringify, type JsonOf } from './utils.js'; 10 10 11 11 // Not the secondary cache key, but a canonical list of the _names_ of the 12 12 // params that are used to to generate the secondary cache key. (I.e., the ··· 17 17 // by some of the stores (i.e., is part of the public contract). 18 18 export type VariantKey = Tagged< 19 19 JsonOf<(string | null | AnyParamValue)[]>, 20 - "VariantKey" 20 + 'VariantKey' 21 21 >; 22 22 23 23 // The only difference between VaryEntry and NormalizedVaryEntry is that the
+17 -17
server/lib/cache/utils/wrapProducer.ts
··· 1 - import stableStringify from "safe-stable-stringify"; 1 + import stableStringify from 'safe-stable-stringify'; 2 2 3 - import type Cache from "../Cache.js"; 4 - import { type NormalizedProducerResult } from "../types/06_Normalization.js"; 3 + import type Cache from '../Cache.js'; 4 + import { type NormalizedProducerResult } from '../types/06_Normalization.js'; 5 5 import { 6 - type Vary, 7 6 type AnyParams, 8 7 type AnyValidators, 9 8 type ConsumerRequest, 10 9 type Logger, 11 10 type RequestPairedProducer, 12 - } from "../types/index.js"; 13 - import collapsedTaskCreator from "./collapsedTaskCreator.js"; 14 - import { normalizeProducerResult, normalizeVary } from "./normalization.js"; 15 - import { defaultLoggersByComponent } from "./utils.js"; 11 + type Vary, 12 + } from '../types/index.js'; 13 + import collapsedTaskCreator from './collapsedTaskCreator.js'; 14 + import { normalizeProducerResult, normalizeVary } from './normalization.js'; 15 + import { defaultLoggersByComponent } from './utils.js'; 16 16 17 17 export type WrapProducerOptions<V extends AnyParams> = { 18 18 isCacheable?(this: void, id: string, params: Partial<V>): boolean; ··· 74 74 const { 75 75 isCacheable = () => true, 76 76 collapseOverlappingRequestsTime = 3, 77 - logger = defaultLoggersByComponent["wrap-producer"], 77 + logger = defaultLoggersByComponent['wrap-producer'], 78 78 } = options ?? {}; 79 79 80 - const logTrace = logger.bind(null, "wrap-producer", "trace"); 81 - const logWarning = logger.bind(null, "wrap-producer", "warn"); 80 + const logTrace = logger.bind(null, 'wrap-producer', 'trace'); 81 + const logWarning = logger.bind(null, 'wrap-producer', 'warn'); 82 82 83 83 // Suppose the caller is requesting a resource, and we're already in the 84 84 // process of requesting that resource from the producer (or storing the ··· 137 137 // 138 138 // Of course, we can only use this IF THE REQUEST IS CACHEABLE. 139 139 const callProducerAndLog: typeof producer = async (req) => { 140 - logTrace("contacting producer", req); 140 + logTrace('contacting producer', req); 141 141 const resp = await producer(req); 142 - logTrace("got response from producer", resp); 142 + logTrace('got response from producer', resp); 143 143 return resp; 144 144 }; 145 145 ··· 170 170 normalizeVary(cache.normalizeParamName, cache.normalizeParamValue, vary); 171 171 172 172 const wrappedProducer = async function ( 173 - req: Omit<ConsumerRequest<Params, Id>, "directives" | "params"> & 174 - Partial<Pick<ConsumerRequest<Params, Id>, "directives" | "params">>, 173 + req: Omit<ConsumerRequest<Params, Id>, 'directives' | 'params'> & 174 + Partial<Pick<ConsumerRequest<Params, Id>, 'directives' | 'params'>>, 175 175 ): Promise<NormalizedProducerResult<Content, Validators, Params>> { 176 176 const { id, params = {}, directives = {} } = req; 177 177 // replace undefined params + direcvites w/ empty objects ··· 233 233 // swallow error rather than crash. 234 234 newContentPromise.catch(() => { 235 235 logWarning( 236 - "error asynchronously requesting refreshed content from producer", 236 + 'error asynchronously requesting refreshed content from producer', 237 237 { id, params, directives }, 238 238 ); 239 239 }); ··· 243 243 return usableIfError 244 244 ? newContentPromise.catch((error) => { 245 245 logWarning( 246 - "error calling producer; falling back to a cached value, as permitted", 246 + 'error calling producer; falling back to a cached value, as permitted', 247 247 { error, entry: usableIfError }, 248 248 ); 249 249
+1 -1
server/plugins/README.md
··· 29 29 ``` 30 30 31 31 The `examples` folders intentionally ship tiny reference implementations (for 32 - instance a no-op adapter) so that authors can copy/paste a starting point. 32 + instance a no-op adapter) so that authors can copy/paste a starting point.
+1 -1
server/plugins/analytics/IAnalyticsAdapter.ts
··· 1 - import type SafeTracer from '../../utils/SafeTracer.js'; 2 1 import type { 3 2 CDCChange, 4 3 CDCConfig, 5 4 } from '../../storage/dataWarehouse/IDataWarehouseAnalytics.js'; 5 + import type SafeTracer from '../../utils/SafeTracer.js'; 6 6 import type { 7 7 AnalyticsEventInput, 8 8 AnalyticsQueryResult,
+1 -3
server/plugins/analytics/examples/NoOpAnalyticsAdapter.ts
··· 1 - import type { 2 - IAnalyticsAdapter, 3 - } from '../IAnalyticsAdapter.js'; 1 + import type { IAnalyticsAdapter } from '../IAnalyticsAdapter.js'; 4 2 import type { 5 3 AnalyticsEventInput, 6 4 AnalyticsQueryResult,
+5 -3
server/plugins/warehouse/adapters/ClickhouseWarehouseAdapter.ts
··· 37 37 const port = connection.port ?? 8123; 38 38 39 39 const url = `${protocol}://${connection.host}:${port}`; 40 - const password = connection.password.length ? connection.password : undefined; 40 + const password = connection.password.length 41 + ? connection.password 42 + : undefined; 41 43 this.client = createClient({ 42 44 url, 43 45 username: connection.username, ··· 59 61 ): Promise<readonly T[]> { 60 62 const execute = async () => { 61 63 const statement = formatClickhouseQuery(sql, params); 62 - 64 + 63 65 // For INSERT statements, use command() instead of query() with format 64 66 if (statement.trim().toUpperCase().startsWith('INSERT')) { 65 67 await this.client.command({ ··· 67 69 }); 68 70 return [] as readonly T[]; 69 71 } 70 - 72 + 71 73 const result = await this.client.query({ 72 74 query: statement, 73 75 format: 'JSONEachRow',
+1 -4
server/plugins/warehouse/examples/NoOpWarehouseAdapter.ts
··· 1 1 import type { IWarehouseAdapter } from '../IWarehouseAdapter.js'; 2 - import type { 3 - WarehouseQueryResult, 4 - WarehouseTransactionFn, 5 - } from '../types.js'; 2 + import type { WarehouseQueryResult, WarehouseTransactionFn } from '../types.js'; 6 3 7 4 export class NoOpWarehouseAdapter implements IWarehouseAdapter { 8 5 readonly name = 'noop-warehouse';
+1 -2
server/plugins/warehouse/queries/ClickhouseOrgCreationAdapter.ts
··· 1 - import { type IOrgCreationAdapter } from './IOrgCreationAdapter.js'; 2 1 import type { IDataWarehouse } from '../../../storage/dataWarehouse/IDataWarehouse.js'; 3 2 import type SafeTracer from '../../../utils/SafeTracer.js'; 3 + import { type IOrgCreationAdapter } from './IOrgCreationAdapter.js'; 4 4 5 5 export class ClickhouseOrgCreationAdapter implements IOrgCreationAdapter { 6 6 constructor( ··· 22 22 ); 23 23 } 24 24 } 25 -
+6 -11
server/plugins/warehouse/queries/ClickhouseReportingAnalyticsAdapter.ts
··· 1 + import type { IDataWarehouse } from '../../../storage/dataWarehouse/IDataWarehouse.js'; 2 + import type SafeTracer from '../../../utils/SafeTracer.js'; 1 3 import { 2 4 type IReportingAnalyticsAdapter, 3 - type ReportingRulePassRateInput, 4 - type ReportingRulePassRateRow, 5 5 type ReportingRulePassingContentSample, 6 6 type ReportingRulePassingContentSampleInput, 7 + type ReportingRulePassRateInput, 8 + type ReportingRulePassRateRow, 7 9 type ReportsByDayRow, 8 10 } from './IReportingAnalyticsAdapter.js'; 9 - import type { IDataWarehouse } from '../../../storage/dataWarehouse/IDataWarehouse.js'; 10 - import type SafeTracer from '../../../utils/SafeTracer.js'; 11 11 12 12 type ReportsByDayQueryRow = Record<string, unknown> & { 13 13 date: string; ··· 37 37 policy_ids: string[]; 38 38 }; 39 39 40 - export class ClickhouseReportingAnalyticsAdapter 41 - implements IReportingAnalyticsAdapter 42 - { 40 + export class ClickhouseReportingAnalyticsAdapter implements IReportingAnalyticsAdapter { 43 41 constructor( 44 42 private readonly warehouse: IDataWarehouse, 45 43 private readonly tracer: SafeTracer, ··· 109 107 const params: unknown[] = [orgId, ruleId]; 110 108 111 109 if (itemIds && itemIds.length > 0) { 112 - conditions.push( 113 - `item_id IN (${itemIds.map(() => '?').join(', ')})`, 114 - ); 110 + conditions.push(`item_id IN (${itemIds.map(() => '?').join(', ')})`); 115 111 params.push(...itemIds); 116 112 } 117 113 ··· 202 198 return result as readonly T[]; 203 199 } 204 200 } 205 -
+6 -19
server/plugins/warehouse/queries/IActionStatisticsAdapter.ts
··· 39 39 getActionedSubmissionCountsByTagByDay( 40 40 orgId: string, 41 41 startAt: Date, 42 - ): Promise< 43 - ReadonlyArray<{ date: string; tag: string; count: number }> 44 - >; 42 + ): Promise<ReadonlyArray<{ date: string; tag: string; count: number }>>; 45 43 46 44 getActionedSubmissionCountsByPolicyByDay( 47 45 orgId: string, ··· 78 76 79 77 getAllActionCountsGroupByPolicy( 80 78 input: ActionCountsInput, 81 - ): Promise< 82 - ReadonlyArray<{ count: number; policy_id: string; time: string }> 83 - >; 79 + ): Promise<ReadonlyArray<{ count: number; policy_id: string; time: string }>>; 84 80 85 81 getAllActionCountsGroupByActionId( 86 82 input: ActionCountsInput, 87 - ): Promise< 88 - ReadonlyArray<{ count: number; action_id: string; time: string }> 89 - >; 83 + ): Promise<ReadonlyArray<{ count: number; action_id: string; time: string }>>; 90 84 91 85 getAllActionCountsGroupBySource( 92 86 input: ActionCountsInput, 93 - ): Promise< 94 - ReadonlyArray<{ count: number; source: string; time: string }> 95 - >; 87 + ): Promise<ReadonlyArray<{ count: number; source: string; time: string }>>; 96 88 97 89 getAllActionCountsGroupByItemTypeId( 98 90 input: ActionCountsInput, ··· 102 94 103 95 getAllActionCountsGroupByRule( 104 96 input: ActionCountsInput, 105 - ): Promise< 106 - ReadonlyArray<{ count: number; rule_id: string; time: string }> 107 - >; 97 + ): Promise<ReadonlyArray<{ count: number; rule_id: string; time: string }>>; 108 98 109 - getAllActionCountsGroupBy( 110 - input: ActionCountsInput, 111 - ): Promise< 99 + getAllActionCountsGroupBy(input: ActionCountsInput): Promise< 112 100 ReadonlyArray<{ 113 101 count: number; 114 102 action_id?: string; ··· 118 106 }> 119 107 >; 120 108 } 121 -
-1
server/plugins/warehouse/queries/IOrgCreationAdapter.ts
··· 7 7 dateCreated: string, 8 8 ): Promise<void>; 9 9 } 10 -
+1 -5
server/plugins/warehouse/queries/IReportingAnalyticsAdapter.ts
··· 69 69 input: ReportingRulePassingContentSampleInput, 70 70 ): Promise<ReadonlyArray<ReportingRulePassingContentSample>>; 71 71 72 - getNumTimesReported( 73 - orgId: string, 74 - itemId: string, 75 - ): Promise<number | null>; 72 + getNumTimesReported(orgId: string, itemId: string): Promise<number | null>; 76 73 } 77 -
+5 -15
server/plugins/warehouse/queries/index.ts
··· 1 - export { 2 - ClickhouseActionStatisticsAdapter, 3 - } from './ClickhouseActionStatisticsAdapter.js'; 4 - export { 5 - ClickhouseReportingAnalyticsAdapter, 6 - } from './ClickhouseReportingAnalyticsAdapter.js'; 7 - export { 8 - ClickhouseActionExecutionsAdapter, 9 - } from './ClickhouseActionExecutionsAdapter.js'; 10 - export { 11 - ClickhouseContentApiRequestsAdapter, 12 - } from './ClickhouseContentApiRequestsAdapter.js'; 13 - export { 14 - ClickhouseOrgCreationAdapter, 15 - } from './ClickhouseOrgCreationAdapter.js'; 1 + export { ClickhouseActionStatisticsAdapter } from './ClickhouseActionStatisticsAdapter.js'; 2 + export { ClickhouseReportingAnalyticsAdapter } from './ClickhouseReportingAnalyticsAdapter.js'; 3 + export { ClickhouseActionExecutionsAdapter } from './ClickhouseActionExecutionsAdapter.js'; 4 + export { ClickhouseContentApiRequestsAdapter } from './ClickhouseContentApiRequestsAdapter.js'; 5 + export { ClickhouseOrgCreationAdapter } from './ClickhouseOrgCreationAdapter.js';
+1 -3
server/plugins/warehouse/types.ts
··· 7 7 params?: readonly unknown[], 8 8 ) => Promise<readonly T[]>; 9 9 10 - export type WarehouseTransactionFn<T> = ( 11 - query: WarehouseQueryFn, 12 - ) => Promise<T>; 10 + export type WarehouseTransactionFn<T> = (query: WarehouseQueryFn) => Promise<T>;
+1 -3
server/queues/itemSubmissionQueue.ts
··· 25 25 26 26 const loader: DataLoader<ItemSubmissionMessageValue, void> = new DataLoader( 27 27 async (data) => 28 - bulkWrite(queue, data).then(() => 29 - new Array(data.length).fill(undefined), 30 - ), 28 + bulkWrite(queue, data).then(() => new Array(data.length).fill(undefined)), 31 29 { 32 30 cache: false, 33 31 batch: true,
+4 -3
server/routes/action/submitAction.ts
··· 5 5 parseStoredParameters, 6 6 validateActionParameterValues, 7 7 } from '../../services/moderationConfigService/index.js'; 8 + import { hasOrgId } from '../../utils/apiKeyMiddleware.js'; 8 9 import { 9 10 fromCorrelationId, 10 11 toCorrelationId, 11 12 } from '../../utils/correlationIds.js'; 12 13 import { ErrorType, makeUnauthorizedError } from '../../utils/errors.js'; 13 14 import { type RequestHandlerWithBodies } from '../../utils/route-helpers.js'; 14 - import { hasOrgId } from '../../utils/apiKeyMiddleware.js'; 15 15 import { type SubmitActionInput } from './ActionRoutes.js'; 16 16 17 17 export default function submitAction({ ··· 36 36 }), 37 37 ); 38 38 } 39 - 39 + 40 40 const { orgId } = req; 41 41 42 42 const { body } = req; ··· 105 105 let validatedParameters: Record<string, unknown> | undefined; 106 106 try { 107 107 const validated = validateActionParameterValues(spec, parameters ?? null); 108 - validatedParameters = Object.keys(validated).length > 0 ? validated : undefined; 108 + validatedParameters = 109 + Object.keys(validated).length > 0 ? validated : undefined; 109 110 } catch (e) { 110 111 return next(e); 111 112 }
+4 -1
server/routes/integration_logos/IntegrationLogosRoutes.ts
··· 6 6 export default { 7 7 pathPrefix: '/integration-logos', 8 8 routes: [ 9 - route.get<undefined>('/:integrationId/with-background', serveIntegrationLogoWithBackground), 9 + route.get<undefined>( 10 + '/:integrationId/with-background', 11 + serveIntegrationLogoWithBackground, 12 + ), 10 13 route.get<undefined>('/:integrationId', serveIntegrationLogo), 11 14 ] as ControllerRouteList, 12 15 } satisfies Controller;
+5 -2
server/routes/items/ItemRoutes.ts
··· 2 2 rawItemSubmissionSchema, 3 3 type RawItemSubmission, 4 4 } from '../../services/itemProcessingService/index.js'; 5 - import { route } from '../../utils/route-helpers.js'; 6 5 import { createApiKeyMiddleware } from '../../utils/apiKeyMiddleware.js'; 6 + import { route } from '../../utils/route-helpers.js'; 7 7 import { type Controller } from '../index.js'; 8 8 import submitItems from './submitItems.js'; 9 9 ··· 32 32 required: ['items'], 33 33 }, 34 34 }, 35 - (deps) => [createApiKeyMiddleware<SubmitItemsInput, undefined>(deps), submitItems(deps)], 35 + (deps) => [ 36 + createApiKeyMiddleware<SubmitItemsInput, undefined>(deps), 37 + submitItems(deps), 38 + ], 36 39 ), 37 40 ], 38 41 } as Controller;
+2 -2
server/routes/policies/getPolicies.ts
··· 1 1 import { type Dependencies } from '../../iocContainer/index.js'; 2 + import { hasOrgId } from '../../utils/apiKeyMiddleware.js'; 2 3 import { makeBadRequestError } from '../../utils/errors.js'; 3 4 import { type RequestHandlerWithBodies } from '../../utils/route-helpers.js'; 4 - import { hasOrgId } from '../../utils/apiKeyMiddleware.js'; 5 5 import { type GetPoliciesOutput } from './PoliciesRoutes.js'; 6 6 7 7 export default function getPolicies({ ··· 19 19 }), 20 20 ); 21 21 } 22 - 22 + 23 23 const { orgId } = req; 24 24 25 25 const policies = await ModerationConfigService.getPolicies({ orgId });
+3 -3
server/routes/reporting/submitAppeal.ts
··· 101 101 102 102 const hasAdditionalItemsOnThreadSubmission = Boolean( 103 103 additionalItemSubmissions && 104 - additionalItemSubmissions.length > 0 && 105 - reportedItemSubmission.error === undefined && 106 - reportedItemSubmission.itemSubmission.itemType.kind === 'THREAD', 104 + additionalItemSubmissions.length > 0 && 105 + reportedItemSubmission.error === undefined && 106 + reportedItemSubmission.itemSubmission.itemType.kind === 'THREAD', 107 107 ); 108 108 109 109 const isAllValidContentItems = (
+2 -2
server/routes/user_scores/getUserScores.ts
··· 1 1 import { type Dependencies } from '../../iocContainer/index.js'; 2 + import { hasOrgId } from '../../utils/apiKeyMiddleware.js'; 2 3 import { makeBadRequestError } from '../../utils/errors.js'; 3 4 import { type RequestHandlerWithBodies } from '../../utils/route-helpers.js'; 4 - import { hasOrgId } from '../../utils/apiKeyMiddleware.js'; 5 5 import { type GetUserScoresOutput } from './UserScoresRoutes.js'; 6 6 7 7 export default function getUserScores({ ··· 31 31 }), 32 32 ); 33 33 } 34 - 34 + 35 35 const { orgId } = req; 36 36 37 37 const userScore = await getUserScoreEventuallyConsistent(orgId, {
+10 -7
server/rule_engine/RuleEvaluator.ts
··· 6 6 import makeGetDerivedFieldValueWithCache from '../condition_evaluator/getDerivedFieldValue.js'; 7 7 import { type Dependencies } from '../iocContainer/index.js'; 8 8 import { inject } from '../iocContainer/utils.js'; 9 + import type { 10 + ActionExecutionSourceType, 11 + RuleExecutionSourceType, 12 + } from '../services/analyticsLoggers/index.js'; 9 13 import { 10 14 type DerivedFieldSpec, 11 15 type DerivedFieldValue, ··· 18 22 } from '../services/moderationConfigService/index.js'; 19 23 import { type TransientRunSignalWithCache } from '../services/orgAwareSignalExecutionService/index.js'; 20 24 import { type SignalId } from '../services/signalsService/index.js'; 21 - import type { 22 - ActionExecutionSourceType, 23 - RuleExecutionSourceType, 24 - } from '../services/analyticsLoggers/index.js'; 25 25 import { instantiateOpaqueType } from '../utils/typescript-types.js'; 26 26 27 27 // A rule can be run on either a full submission, or on just the identifier of ··· 33 33 policyIds?: string[]; 34 34 sourceType?: RuleExecutionSourceType | ActionExecutionSourceType; 35 35 }) 36 - | ReadonlyDeep<{ itemId: string; itemType: Pick<ItemType, 'id' | 'kind' | 'name'> }>; 36 + | ReadonlyDeep<{ 37 + itemId: string; 38 + itemType: Pick<ItemType, 'id' | 'kind' | 'name'>; 39 + }>; 37 40 38 41 export function isFullSubmission(input: RuleInput): input is ItemSubmission { 39 42 return 'data' in input && 'submissionId' in input && Boolean(input.data); ··· 43 46 return isFullSubmission(it) 44 47 ? it.creator 45 48 : it.itemType.kind === 'USER' 46 - ? { id: it.itemId, typeId: it.itemType.id } 47 - : undefined; 49 + ? { id: it.itemId, typeId: it.itemType.id } 50 + : undefined; 48 51 } 49 52 50 53 type RuleEvaluationContextImpl = Readonly<{
+2 -3
server/services/analyticsLoggers/ActionExecutionLogger.test.ts
··· 1 - import { type ActionExecutionData } from '../../rule_engine/ActionPublisher.js'; 2 1 import { type Dependencies } from '../../iocContainer/index.js'; 2 + import { type ActionExecutionData } from '../../rule_engine/ActionPublisher.js'; 3 3 import { type AnalyticsSchema } from '../../storage/dataWarehouse/IDataWarehouseAnalytics.js'; 4 4 import { type CorrelationId } from '../../utils/correlationIds.js'; 5 5 import { jsonParse, type JsonOf } from '../../utils/encoding.js'; ··· 40 40 }, 41 41 matchingRules: undefined, 42 42 ruleEnvironment: undefined, 43 - correlationId: 44 - 'manual-action-run:abc' as CorrelationId<'manual-action-run'>, 43 + correlationId: 'manual-action-run:abc' as CorrelationId<'manual-action-run'>, 45 44 policies: [], 46 45 }; 47 46
+4 -4
server/services/analyticsLoggers/ActionExecutionLogger.ts
··· 7 7 type ActionExecutionData, 8 8 } from '../../rule_engine/ActionPublisher.js'; 9 9 import { 10 + type ActionExecutionMatchingRule, 11 + type ActionExecutionPolicy, 12 + } from '../../storage/dataWarehouse/warehouseSchema.js'; 13 + import { 10 14 fromCorrelationId, 11 15 getSourceType, 12 16 type CorrelationId, ··· 14 18 import { jsonStringify } from '../../utils/encoding.js'; 15 19 import { safePick } from '../../utils/misc.js'; 16 20 import { getUtcDateOnlyString } from '../../utils/time.js'; 17 - import { 18 - type ActionExecutionMatchingRule, 19 - type ActionExecutionPolicy, 20 - } from '../../storage/dataWarehouse/warehouseSchema.js'; 21 21 22 22 export type ActionExecutionSourceType = 23 23 | 'post-content'
+2 -2
server/services/analyticsLoggers/ItemModelScoreLogger.ts
··· 24 24 failureReason: HasFailure extends true 25 25 ? string 26 26 : HasFailure extends false 27 - ? undefined 28 - : string | undefined; 27 + ? undefined 28 + : string | undefined; 29 29 model: HasFailure extends true 30 30 ? undefined 31 31 : {
+2 -2
server/services/analyticsQueries/ItemHistoryQueries.test.ts
··· 1 1 import { Kysely, type DatabaseConnection } from 'kysely'; 2 2 3 + import { type DataWarehousePublicSchema } from '../../storage/dataWarehouse/warehouseSchema.js'; 4 + import { makeMockWarehouseDialect } from '../../test/stubs/makeMockWarehouseKyselyDialect.js'; 3 5 import { safePick } from '../../utils/misc.js'; 4 6 import { getUtcDateOnlyString, WEEK_MS } from '../../utils/time.js'; 5 - import { type DataWarehousePublicSchema } from '../../storage/dataWarehouse/warehouseSchema.js'; 6 - import { makeMockWarehouseDialect } from '../../test/stubs/makeMockWarehouseKyselyDialect.js'; 7 7 import ItemHistoryQueries from './ItemHistoryQueries.js'; 8 8 9 9 describe('ItemHistoryQueries', () => {
+9 -7
server/services/analyticsQueries/RuleActionInsights.ts
··· 2 2 import { sql, type Kysely } from 'kysely'; 3 3 import { match } from 'ts-pattern'; 4 4 5 - import { formatClickhouseQuery } from '../../plugins/warehouse/utils/clickhouseSql.js'; 6 5 import { type Dependencies } from '../../iocContainer/index.js'; 7 6 import { inject } from '../../iocContainer/utils.js'; 7 + import { formatClickhouseQuery } from '../../plugins/warehouse/utils/clickhouseSql.js'; 8 8 import { type RuleEnvironment } from '../../rule_engine/RuleEngine.js'; 9 9 import { type NormalizedItemData } from '../../services/itemProcessingService/index.js'; 10 10 import { type ConditionWithResult } from '../../services/moderationConfigService/index.js'; 11 11 import { 12 12 BuiltInThirdPartySignalType, 13 + integrationForSignalType, 13 14 SignalType, 14 15 UserCreatedExternalSignalType, 15 - integrationForSignalType, 16 16 type Integration, 17 17 } from '../../services/signalsService/index.js'; 18 - import { jsonParse, type JsonOf } from '../../utils/encoding.js'; 19 - import { getUtcDateOnlyString, YEAR_MS } from '../../utils/time.js'; 20 - import { type ConditionSetWithResultAsLogged } from '../analyticsLoggers/ruleExecutionLoggingUtils.js'; 21 18 import { 22 19 warehouseDateToDate, 23 20 warehouseDateToDateOnlyString, 24 - type WarehouseDate, 25 21 type DataWarehousePublicSchema, 22 + type WarehouseDate, 26 23 } from '../../storage/dataWarehouse/warehouseSchema.js'; 24 + import { jsonParse, type JsonOf } from '../../utils/encoding.js'; 25 + import { getUtcDateOnlyString, YEAR_MS } from '../../utils/time.js'; 26 + import { type ConditionSetWithResultAsLogged } from '../analyticsLoggers/ruleExecutionLoggingUtils.js'; 27 27 28 28 type RulePassSample = { 29 29 date: Date; ··· 411 411 ts: new Date(row.ts), 412 412 result: 413 413 row.result != null 414 - ? jsonParse<JsonOf<ConditionSetWithResultAsLogged>>(row.result as JsonOf<ConditionSetWithResultAsLogged>) 414 + ? jsonParse<JsonOf<ConditionSetWithResultAsLogged>>( 415 + row.result as JsonOf<ConditionSetWithResultAsLogged>, 416 + ) 415 417 : undefined, 416 418 contentId: row.item_id, 417 419 itemTypeName: row.item_type_name ?? '',
+15 -17
server/services/apiKeyService/apiKeyService.test.ts
··· 1 + import type { Kysely } from 'kysely'; 2 + 1 3 import { makeTestWithFixture } from '../../test/utils.js'; 4 + import { type CombinedPg } from '../combinedDbTypes.js'; 2 5 import ApiKeyService from './apiKeyService.js'; 3 - import type { Kysely } from 'kysely'; 4 - import { type CombinedPg } from '../combinedDbTypes.js'; 5 6 6 7 // Mock Kysely database 7 8 const mockDb = { ··· 44 45 created_by: null, 45 46 }), 46 47 }; 47 - 48 + 48 49 const mockUpdate = { 49 50 set: jest.fn().mockReturnThis(), 50 51 where: jest.fn().mockReturnThis(), ··· 147 148 }, 148 149 ); 149 150 150 - testWithFixtures( 151 - 'should return null for invalid key', 152 - async ({ sut }) => { 153 - const mockSelect = { 154 - select: jest.fn().mockReturnThis(), 155 - where: jest.fn().mockReturnThis(), 156 - executeTakeFirst: jest.fn().mockResolvedValue(undefined), 157 - }; 151 + testWithFixtures('should return null for invalid key', async ({ sut }) => { 152 + const mockSelect = { 153 + select: jest.fn().mockReturnThis(), 154 + where: jest.fn().mockReturnThis(), 155 + executeTakeFirst: jest.fn().mockResolvedValue(undefined), 156 + }; 158 157 159 - (mockDb.selectFrom as jest.Mock).mockReturnValue(mockSelect); 158 + (mockDb.selectFrom as jest.Mock).mockReturnValue(mockSelect); 160 159 161 - const result = await sut.validateApiKey('invalid-key'); 160 + const result = await sut.validateApiKey('invalid-key'); 162 161 163 - expect(result).toBeNull(); 164 - }, 165 - ); 162 + expect(result).toBeNull(); 163 + }); 166 164 }); 167 - }); 165 + });
+8 -2
server/services/apiKeyService/index.ts
··· 8 8 export { type ApiKeyServicePg } from './dbTypes.js'; 9 9 10 10 export interface ApiKeyStorage { 11 - store(key: string, orgId: string, meta: ApiKeyMetadata): Promise<{ keyId: string }>; 12 - fetch(orgId: string): Promise<{ key: string; metadata: ApiKeyMetadata } | false>; 11 + store( 12 + key: string, 13 + orgId: string, 14 + meta: ApiKeyMetadata, 15 + ): Promise<{ keyId: string }>; 16 + fetch( 17 + orgId: string, 18 + ): Promise<{ key: string; metadata: ApiKeyMetadata } | false>; 13 19 }
+9 -4
server/services/hmaService/hashBankService.ts
··· 1 1 import { type Kysely } from 'kysely'; 2 + 2 3 import { 4 + type CreateHashBankInput, 5 + type HashBank, 3 6 type HmaServicePg, 4 - type HashBank, 5 - type CreateHashBankInput, 6 7 type UpdateHashBankInput, 7 8 } from './dbTypes.js'; 8 9 ··· 57 58 return results; 58 59 } 59 60 60 - async update(id: number, orgId: string, updates: UpdateHashBankInput): Promise<HashBank> { 61 + async update( 62 + id: number, 63 + orgId: string, 64 + updates: UpdateHashBankInput, 65 + ): Promise<HashBank> { 61 66 const result = await this.db 62 67 .updateTable('public.hash_banks') 63 68 .set(updates) ··· 84 89 .where('org_id', '=', orgId) 85 90 .execute(); 86 91 } 87 - } 92 + }
+120 -72
server/services/hmaService/index.test.ts
··· 1 - import { HmaService, type ExchangeInfo } from './index.js'; 2 - import type { HashBank } from './dbTypes.js'; 3 1 import { jsonParse } from '../../utils/encoding.js'; 2 + import type { HashBank } from './dbTypes.js'; 3 + import { HmaService, type ExchangeInfo } from './index.js'; 4 4 5 5 const MOCK_BANK: HashBank = { 6 6 id: 1, ··· 52 52 describe('HmaService', () => { 53 53 describe('createBank', () => { 54 54 it('creates a standalone bank via POST /c/banks when no exchange is provided', async () => { 55 - const fetchHTTP = jest.fn().mockResolvedValue(ok({ name: 'COOP_ORG1_MY_BANK', matching_enabled_ratio: 1.0 })); 55 + const fetchHTTP = jest 56 + .fn() 57 + .mockResolvedValue( 58 + ok({ name: 'COOP_ORG1_MY_BANK', matching_enabled_ratio: 1.0 }), 59 + ); 56 60 const svc = makeService(fetchHTTP); 57 61 58 62 const result = await svc.createBank('org1', 'My Bank', 'desc', 1.0); ··· 106 110 svc.createBank('org1', 'My Bank', 'desc', 1.0, { 107 111 apiName: 'fb_threatexchange', 108 112 apiJson: { privacy_group: 123 }, 109 - }) 113 + }), 110 114 ).rejects.toThrow('Failed to create exchange in HMA'); 111 115 }); 112 116 ··· 115 119 const svc = makeService(fetchHTTP); 116 120 117 121 await expect( 118 - svc.createBank('org1', 'My Bank', 'desc', 1.0) 122 + svc.createBank('org1', 'My Bank', 'desc', 1.0), 119 123 ).rejects.toThrow('Failed to create HMA bank'); 120 124 }); 121 125 }); ··· 140 144 const svc = makeService(fetchHTTP); 141 145 142 146 await expect( 143 - svc.setExchangeCredentials('ncmec', { user: 'u', password: 'p' }) 147 + svc.setExchangeCredentials('ncmec', { user: 'u', password: 'p' }), 144 148 ).rejects.toThrow("Failed to set exchange credentials for 'ncmec'"); 145 149 }); 146 150 }); ··· 176 180 }); 177 181 178 182 it('returns full exchange info with fetch status on success', async () => { 179 - const fetchHTTP = jest.fn() 180 - .mockResolvedValueOnce(ok({ 181 - api: 'fb_threatexchange', 182 - enabled: true, 183 - name: 'COOP_ORG1_BANK', 184 - })) 185 - .mockResolvedValueOnce(ok({ 186 - supports_authentification: true, 187 - has_set_authentification: true, 188 - })) 189 - .mockResolvedValueOnce(ok({ 190 - last_fetch_succeeded: true, 191 - last_fetch_complete_ts: 1700000000, 192 - up_to_date: true, 193 - fetched_items: 42, 194 - running_fetch_start_ts: null, 195 - checkpoint_ts: 1700000000, 196 - })); 183 + const fetchHTTP = jest 184 + .fn() 185 + .mockResolvedValueOnce( 186 + ok({ 187 + api: 'fb_threatexchange', 188 + enabled: true, 189 + name: 'COOP_ORG1_BANK', 190 + }), 191 + ) 192 + .mockResolvedValueOnce( 193 + ok({ 194 + supports_authentification: true, 195 + has_set_authentification: true, 196 + }), 197 + ) 198 + .mockResolvedValueOnce( 199 + ok({ 200 + last_fetch_succeeded: true, 201 + last_fetch_complete_ts: 1700000000, 202 + up_to_date: true, 203 + fetched_items: 42, 204 + running_fetch_start_ts: null, 205 + checkpoint_ts: 1700000000, 206 + }), 207 + ); 197 208 const svc = makeService(fetchHTTP); 198 209 199 210 const result = await svc.getExchangeForBank('COOP_ORG1_BANK'); ··· 211 222 }); 212 223 213 224 it('returns fetch failed status', async () => { 214 - const fetchHTTP = jest.fn() 215 - .mockResolvedValueOnce(ok({ 216 - api: 'ncmec', 217 - enabled: true, 218 - name: 'COOP_ORG1_BANK', 219 - })) 220 - .mockResolvedValueOnce(ok({ 221 - supports_authentification: true, 222 - has_set_authentification: false, 223 - })) 224 - .mockResolvedValueOnce(ok({ 225 - last_fetch_succeeded: false, 226 - last_fetch_complete_ts: 1700000000, 227 - up_to_date: false, 228 - fetched_items: 0, 229 - running_fetch_start_ts: null, 230 - checkpoint_ts: null, 231 - })); 225 + const fetchHTTP = jest 226 + .fn() 227 + .mockResolvedValueOnce( 228 + ok({ 229 + api: 'ncmec', 230 + enabled: true, 231 + name: 'COOP_ORG1_BANK', 232 + }), 233 + ) 234 + .mockResolvedValueOnce( 235 + ok({ 236 + supports_authentification: true, 237 + has_set_authentification: false, 238 + }), 239 + ) 240 + .mockResolvedValueOnce( 241 + ok({ 242 + last_fetch_succeeded: false, 243 + last_fetch_complete_ts: 1700000000, 244 + up_to_date: false, 245 + fetched_items: 0, 246 + running_fetch_start_ts: null, 247 + checkpoint_ts: null, 248 + }), 249 + ); 232 250 const svc = makeService(fetchHTTP); 233 251 234 252 const result = await svc.getExchangeForBank('COOP_ORG1_BANK'); ··· 239 257 }); 240 258 241 259 it('detects active fetch in progress', async () => { 242 - const fetchHTTP = jest.fn() 243 - .mockResolvedValueOnce(ok({ 244 - api: 'fb_threatexchange', 245 - enabled: true, 246 - name: 'COOP_ORG1_BANK', 247 - })) 248 - .mockResolvedValueOnce(ok({ 249 - supports_authentification: true, 250 - has_set_authentification: true, 251 - })) 252 - .mockResolvedValueOnce(ok({ 253 - last_fetch_succeeded: true, 254 - last_fetch_complete_ts: 1700000000, 255 - up_to_date: false, 256 - fetched_items: 10, 257 - running_fetch_start_ts: 1700001000, 258 - checkpoint_ts: 1700000000, 259 - })); 260 + const fetchHTTP = jest 261 + .fn() 262 + .mockResolvedValueOnce( 263 + ok({ 264 + api: 'fb_threatexchange', 265 + enabled: true, 266 + name: 'COOP_ORG1_BANK', 267 + }), 268 + ) 269 + .mockResolvedValueOnce( 270 + ok({ 271 + supports_authentification: true, 272 + has_set_authentification: true, 273 + }), 274 + ) 275 + .mockResolvedValueOnce( 276 + ok({ 277 + last_fetch_succeeded: true, 278 + last_fetch_complete_ts: 1700000000, 279 + up_to_date: false, 280 + fetched_items: 10, 281 + running_fetch_start_ts: 1700001000, 282 + checkpoint_ts: 1700000000, 283 + }), 284 + ); 260 285 const svc = makeService(fetchHTTP); 261 286 262 287 const result = await svc.getExchangeForBank('COOP_ORG1_BANK'); ··· 265 290 }); 266 291 267 292 it('gracefully handles status endpoint failure', async () => { 268 - const fetchHTTP = jest.fn() 269 - .mockResolvedValueOnce(ok({ 270 - api: 'fb_threatexchange', 271 - enabled: true, 272 - name: 'COOP_ORG1_BANK', 273 - })) 274 - .mockResolvedValueOnce(ok({ 275 - supports_authentification: true, 276 - has_set_authentification: true, 277 - })) 293 + const fetchHTTP = jest 294 + .fn() 295 + .mockResolvedValueOnce( 296 + ok({ 297 + api: 'fb_threatexchange', 298 + enabled: true, 299 + name: 'COOP_ORG1_BANK', 300 + }), 301 + ) 302 + .mockResolvedValueOnce( 303 + ok({ 304 + supports_authentification: true, 305 + has_set_authentification: true, 306 + }), 307 + ) 278 308 .mockRejectedValueOnce(new Error('timeout')); 279 309 const svc = makeService(fetchHTTP); 280 310 ··· 290 320 it('returns schema from HMA when endpoint is available', async () => { 291 321 const hmaSchema = { 292 322 config_schema: { 293 - fields: [{ name: 'privacy_group', type: 'number', required: true, default: null, help: 'PG ID', choices: null }], 323 + fields: [ 324 + { 325 + name: 'privacy_group', 326 + type: 'number', 327 + required: true, 328 + default: null, 329 + help: 'PG ID', 330 + choices: null, 331 + }, 332 + ], 294 333 }, 295 334 credentials_schema: { 296 - fields: [{ name: 'api_token', type: 'string', required: true, default: null, help: 'Token', choices: null }], 335 + fields: [ 336 + { 337 + name: 'api_token', 338 + type: 'string', 339 + required: true, 340 + default: null, 341 + help: 'Token', 342 + choices: null, 343 + }, 344 + ], 297 345 }, 298 346 }; 299 347 const fetchHTTP = jest.fn().mockResolvedValue(ok(hmaSchema));
+9 -9
server/services/itemProcessingService/toNormalizedItemDataOrErrors.ts
··· 61 61 !fieldDefinition 62 62 ? value 63 63 : fieldDefinition.type === 'ARRAY' || fieldDefinition.type === 'MAP' 64 - ? fieldTypeHandlers[fieldDefinition.type].coerce( 65 - value, 66 - legalItemTypeIds, 67 - fieldDefinition.container as never, 68 - ) 69 - : fieldTypeHandlers[fieldDefinition.type].coerce( 70 - value, 71 - legalItemTypeIds, 72 - ), 64 + ? fieldTypeHandlers[fieldDefinition.type].coerce( 65 + value, 66 + legalItemTypeIds, 67 + fieldDefinition.container as never, 68 + ) 69 + : fieldTypeHandlers[fieldDefinition.type].coerce( 70 + value, 71 + legalItemTypeIds, 72 + ), 73 73 ] as const; 74 74 }) 75 75 .filter(([key, value]) => {
+6 -7
server/services/moderationConfigService/modules/ActionOperations.ts
··· 1 - import { type Kysely, sql } from 'kysely'; 1 + import { sql, type Kysely } from 'kysely'; 2 2 import { type JsonObject, type JsonValue, type Writable } from 'type-fest'; 3 3 import { uid } from 'uid'; 4 4 ··· 18 18 import { type Action, type CustomAction } from '../index.js'; 19 19 import { type ItemTypeKind } from '../types/itemTypes.js'; 20 20 import { 21 - type RawActionParameterInput, 22 21 serializeParameters, 23 22 validateActionParameters, 23 + type RawActionParameterInput, 24 24 } from './actionParametersValidation.js'; 25 25 26 26 function assertCustomAction(action: Action): asserts action is CustomAction { 27 27 if (action.actionType !== 'CUSTOM_ACTION') { 28 - throw new Error( 29 - `Expected CUSTOM_ACTION but received ${action.actionType}`, 30 - ); 28 + throw new Error(`Expected CUSTOM_ACTION but received ${action.actionType}`); 31 29 } 32 30 } 33 31 ··· 463 461 callbackUrl: it.callbackUrl, 464 462 callbackUrlBody: it.callbackUrlBody, 465 463 callbackUrlHeaders: it.callbackUrlHeaders, 466 - customMrtApiParams: 467 - ActionOperations.customMrtApiParamsFromDb(it.customMrtApiParams), 464 + customMrtApiParams: ActionOperations.customMrtApiParamsFromDb( 465 + it.customMrtApiParams, 466 + ), 468 467 }; 469 468 case 'ENQUEUE_TO_MRT': 470 469 case 'ENQUEUE_TO_NCMEC':
+1 -5
server/services/moderationConfigService/modules/actionParameterValueValidation.ts
··· 1 1 import { makeBadRequestError } from '../../../utils/errors.js'; 2 2 import { assertUnreachable } from '../../../utils/misc.js'; 3 - 4 3 import { type ActionParameter } from './actionParametersValidation.js'; 5 4 6 5 function makeInvalidParameterValueError(detail: string) { ··· 79 78 // whitespace-only strings as missing for STRING/SELECT, and `[]` as missing 80 79 // for MULTISELECT — these would otherwise pass the required check while 81 80 // being semantically empty. NUMBER 0 and BOOLEAN false remain valid. 82 - function isMissingForRequired( 83 - param: ActionParameter, 84 - value: unknown, 85 - ): boolean { 81 + function isMissingForRequired(param: ActionParameter, value: unknown): boolean { 86 82 if (value === undefined || value === null) return true; 87 83 switch (param.type) { 88 84 case 'STRING':
+7 -2
server/services/moderationConfigService/modules/actionParametersValidation.test.ts
··· 1 - import { validateActionParameterValues } from './actionParameterValueValidation.js'; 2 1 import { 3 2 parseStoredParameters, 4 3 validateActionParameters, 5 4 } from './actionParametersValidation.js'; 5 + import { validateActionParameterValues } from './actionParameterValueValidation.js'; 6 6 7 7 describe('validateActionParameters', () => { 8 8 it('returns an empty array for null/undefined/empty', () => { ··· 82 82 }); 83 83 84 84 it('accepts snake_case, kebab-case, and dotted names', () => { 85 - for (const name of ['ban_duration', 'ban-duration', 'org.user.id', 'a.b-c_1']) { 85 + for (const name of [ 86 + 'ban_duration', 87 + 'ban-duration', 88 + 'org.user.id', 89 + 'a.b-c_1', 90 + ]) { 86 91 expect(() => 87 92 validateActionParameters([ 88 93 { name, displayName: 'OK', type: 'STRING', required: false },
+17 -8
server/services/moderationConfigService/modules/actionParametersValidation.ts
··· 147 147 } 148 148 } 149 149 150 - const at = (index: number, suffix: string) => 151 - `parameters[${index}].${suffix}`; 150 + const at = (index: number, suffix: string) => `parameters[${index}].${suffix}`; 152 151 153 152 function validateStringRules(param: ActionParameter, index: number): void { 154 153 if (param.options !== undefined) { ··· 307 306 } 308 307 } 309 308 310 - function formatAjvErrors(errors: readonly ErrorObject[] | null | undefined): string { 309 + function formatAjvErrors( 310 + errors: readonly ErrorObject[] | null | undefined, 311 + ): string { 311 312 if (!errors || errors.length === 0) return 'invalid action parameters'; 312 313 return errors 313 - .map((err) => `${err.instancePath || '/'}: ${err.message ?? 'invalid value'}`) 314 + .map( 315 + (err) => `${err.instancePath || '/'}: ${err.message ?? 'invalid value'}`, 316 + ) 314 317 .join('; '); 315 318 } 316 319 ··· 332 335 if (typeof raw !== 'object' || raw === null) continue; 333 336 const obj = raw as Record<string, unknown>; 334 337 const name = typeof obj.name === 'string' ? obj.name : null; 335 - const displayName = typeof obj.displayName === 'string' ? obj.displayName : null; 338 + const displayName = 339 + typeof obj.displayName === 'string' ? obj.displayName : null; 336 340 const typeRaw = typeof obj.type === 'string' ? obj.type : null; 337 341 if (name === null || displayName === null || typeRaw === null) continue; 338 342 if (!allowedTypes.includes(typeRaw)) continue; ··· 342 346 type: typeRaw as ActionParameterType, 343 347 required: obj.required === true, 344 348 }; 345 - if (typeof obj.description === 'string') parsed.description = obj.description; 349 + if (typeof obj.description === 'string') 350 + parsed.description = obj.description; 346 351 if (Array.isArray(obj.options)) { 347 352 const options: ActionParameterOption[] = []; 348 353 for (const opt of obj.options) { ··· 387 392 }; 388 393 if (param.description !== undefined) out.description = param.description; 389 394 if (param.options !== undefined) { 390 - out.options = param.options.map((o) => ({ value: o.value, label: o.label })); 395 + out.options = param.options.map((o) => ({ 396 + value: o.value, 397 + label: o.label, 398 + })); 391 399 } 392 400 if (param.min !== undefined) out.min = param.min; 393 401 if (param.max !== undefined) out.max = param.max; 394 402 if (param.maxLength !== undefined) out.maxLength = param.maxLength; 395 - if (param.defaultValue !== undefined) out.defaultValue = param.defaultValue as JsonValue; 403 + if (param.defaultValue !== undefined) 404 + out.defaultValue = param.defaultValue as JsonValue; 396 405 return out; 397 406 }); 398 407 }
+1 -3
server/services/policyActionPenalties.ts
··· 65 65 actions.map((action) => ({ 66 66 actionId: action.id, 67 67 policyId: policy.id, 68 - penalties: [ 69 - computeActionPolicyPenalty(action.penalty, policy.penalty), 70 - ], 68 + penalties: [computeActionPolicyPenalty(action.penalty, policy.penalty)], 71 69 })), 72 70 ); 73 71 }
+4 -4
server/services/ruleAnomalyDetectionService/getCurrentPeriodRuleAlarmStatuses.ts
··· 60 60 lastPeriodPassRate: !applicableStats.length 61 61 ? undefined 62 62 : applicableStats[0].runs === 0 63 - ? 0 64 - : applicableStats[0].passes / applicableStats[0].runs, 63 + ? 0 64 + : applicableStats[0].passes / applicableStats[0].runs, 65 65 secondToLastPeriodPassRate: 66 66 applicableStats.length < 2 67 67 ? undefined 68 68 : applicableStats[1].runs === 0 69 - ? 0 70 - : applicableStats[1].passes / applicableStats[1].runs, 69 + ? 0 70 + : applicableStats[1].passes / applicableStats[1].runs, 71 71 }, 72 72 }; 73 73 });
+5 -5
server/services/sendEmailService/sendEmailService.test.ts
··· 1 - import { type SESClient, SendEmailCommand } from '@aws-sdk/client-ses'; 1 + import { SendEmailCommand, type SESClient } from '@aws-sdk/client-ses'; 2 2 3 3 import makeSendEmail, { 4 4 CoopEmailAddress, ··· 7 7 } from './sendEmailService.js'; 8 8 9 9 function makeMockClient() { 10 - const mockSend = jest.fn().mockResolvedValue({ MessageId: 'test-message-id' }); 10 + const mockSend = jest 11 + .fn() 12 + .mockResolvedValue({ MessageId: 'test-message-id' }); 11 13 const mockClient = { send: mockSend } as unknown as SESClient; 12 14 return { mockSend, mockClient }; 13 15 } ··· 64 66 await sendEmail(msg); 65 67 66 68 const command = mockSend.mock.calls[0][0]; 67 - expect(command.input.Source).toBe( 68 - `Coop <${CoopEmailAddress.NoReply}>`, 69 - ); 69 + expect(command.input.Source).toBe(`Coop <${CoopEmailAddress.NoReply}>`); 70 70 }); 71 71 72 72 it('should use Body.Text instead of Body.Html for text content', async () => {
-1
server/services/signalAuthService/index.ts
··· 7 7 type SignalAuthService, 8 8 type ConfigurableIntegration, 9 9 type CredentialTypes, 10 - 11 10 } from './signalAuthService.js';
+4 -4
server/services/signalAuthService/signalAuthService.ts
··· 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) => {
+4 -2
server/services/signalsService/signals/third_party_signals/google/content_safety/fetchUtils.ts
··· 1 - import type { CoopResponse, FetchHTTP } from '../../../../../networkingService/index.js'; 1 + import type { 2 + CoopResponse, 3 + FetchHTTP, 4 + } from '../../../../../networkingService/index.js'; 2 5 3 6 export async function fetchWithTimeout( 4 7 fetchHTTP: FetchHTTP, ··· 40 43 41 44 return Buffer.from(response.body); 42 45 } 43 -
+4 -8
server/services/signingKeyPairService/secretsManagerSigningKeyPairStorage.ts
··· 24 24 privateKeyWithAlgorithm: JWTWithAlgorithm; 25 25 }; 26 26 27 - export class SecretsManagerSigningKeyPairStorage 28 - implements SigningKeyPairStorage 29 - { 30 - private readonly client = new SecretsManagerClient({ 31 - region: process.env.AWS_REGION ?? 'us-east-2' 27 + export class SecretsManagerSigningKeyPairStorage implements SigningKeyPairStorage { 28 + private readonly client = new SecretsManagerClient({ 29 + region: process.env.AWS_REGION ?? 'us-east-2', 32 30 }); 33 31 34 32 private getSecretIdForKeyId(keyId: SigningKeyId) { ··· 59 57 const secretId = this.getSecretIdForKeyId(keyId); 60 58 61 59 try { 62 - await this.client.send( 63 - new GetSecretValueCommand({ SecretId: secretId }), 64 - ); 60 + await this.client.send(new GetSecretValueCommand({ SecretId: secretId })); 65 61 await this.client.send( 66 62 new PutSecretValueCommand({ 67 63 SecretId: secretId,
+1 -1
server/services/userStatisticsService/computeUserScore.ts
··· 1 1 import _ from 'lodash'; 2 2 import { type ReadonlyDeep } from 'type-fest'; 3 3 4 - import { type PolicyActionPenalties } from '../policyActionPenalties.js'; 5 4 import { jsonStringify, type JsonOf } from '../../utils/encoding.js'; 6 5 import { unzip2 } from '../../utils/fp-helpers.js'; 6 + import { type PolicyActionPenalties } from '../policyActionPenalties.js'; 7 7 import { type UserActionStatistics } from './fetchUserActionStatistics.js'; 8 8 import { type UserSubmissionStatistics } from './fetchUserSubmissionStatistics.js'; 9 9
+1 -1
server/services/userStatisticsService/userStatisticsService.test.ts
··· 1 1 import { Kysely, type DatabaseConnection } from 'kysely'; 2 2 3 - import { makeMockWarehouseDialect } from '../../test/stubs/makeMockWarehouseKyselyDialect.js'; 4 3 import { 5 4 makeMockPgDialect, 6 5 type MockPgExecute, 7 6 } from '../../test/stubs/KyselyPg.js'; 7 + import { makeMockWarehouseDialect } from '../../test/stubs/makeMockWarehouseKyselyDialect.js'; 8 8 import { type UserStatisticsServiceWarehouse } from './dbTypes.js'; 9 9 import { type makeFetchUserActionStatistics } from './fetchUserActionStatistics.js'; 10 10 import { type makeFetchUserSubmissionStatistics } from './fetchUserSubmissionStatistics.js';
+1 -1
server/storage/dataWarehouse/IDataWarehouse.ts
··· 5 5 */ 6 6 7 7 import { type Kysely } from 'kysely'; 8 + 8 9 import type SafeTracer from '../../utils/SafeTracer.js'; 9 10 10 11 /** ··· 95 96 */ 96 97 destroy(): Promise<void>; 97 98 } 98 -
-1
server/storage/dataWarehouse/index.ts
··· 24 24 type IDataWarehouseProvider, 25 25 type DataWarehouseConfig, 26 26 } from './DataWarehouseFactory.js'; 27 -
+1 -2
server/storage/index.ts
··· 1 1 /** 2 2 * Data Warehouse Abstraction Layer 3 - * 3 + * 4 4 * Provides abstraction for data warehouses: Clickhouse, PostgreSQL, etc. 5 5 * Switch warehouses by setting WAREHOUSE_ADAPTER 6 6 */ 7 7 8 8 export * from './dataWarehouse/index.js'; 9 -
+4 -3
server/test/fixtureHelpers/createOrg.ts
··· 36 36 null, 37 37 ).catch(logErrorAndThrow); 38 38 39 - const defaultUserItemType = await deps.ModerationConfigService.createDefaultUserType( 40 - orgId, 41 - ).catch(logErrorAndThrow); 39 + const defaultUserItemType = 40 + await deps.ModerationConfigService.createDefaultUserType(orgId).catch( 41 + logErrorAndThrow, 42 + ); 42 43 43 44 await deps.ModerationConfigService.upsertBuiltInActions(orgId).catch( 44 45 logErrorAndThrow,
+7 -5
server/test/stubs/makeMockWarehouseKyselyDialect.ts
··· 88 88 supportsTransactionalDdl: false, 89 89 supportsReturning: false, 90 90 async acquireMigrationLock(): Promise<void> { 91 - throw new Error('Migrations with kysely not supported in test dialect.'); 91 + throw new Error( 92 + 'Migrations with kysely not supported in test dialect.', 93 + ); 92 94 }, 93 95 async releaseMigrationLock(): Promise<void> { 94 - throw new Error('Migrations with kysely not supported in test dialect.'); 96 + throw new Error( 97 + 'Migrations with kysely not supported in test dialect.', 98 + ); 95 99 }, 96 100 }; 97 101 } ··· 110 114 } 111 115 112 116 export function makeMockWarehouseDialect( 113 - executeMockFn: MockedFn< 114 - (it: CompiledQuery) => Promise<QueryResult<unknown>> 115 - >, 117 + executeMockFn: MockedFn<(it: CompiledQuery) => Promise<QueryResult<unknown>>>, 116 118 ) { 117 119 async function* emptyStream(): AsyncIterableIterator<never> { 118 120 throw new Error('not supported');
+1 -1
server/tsconfig.json
··· 20 20 "allowJs": true, 21 21 "typeRoots": ["./node_modules/@types", "./shim"], 22 22 "skipLibCheck": true, 23 - "sourceMap": true, 23 + "sourceMap": true 24 24 }, 25 25 // https://stackoverflow.com/questions/51610583/ts-node-ignores-d-ts-files-while-tsc-successfully-compiles-the-project 26 26 "files": ["decs.d.ts"],
+1 -1
server/utils/bodySchemaValidation.ts
··· 1 - import type { RequestHandler } from 'express'; 2 1 import _Ajv, { type ErrorObject } from 'ajv-draft-04'; 2 + import type { RequestHandler } from 'express'; 3 3 4 4 import { makeBadRequestError } from './errors.js'; 5 5
+12 -3
server/utils/caching.test.ts
··· 4 4 describe('cached', () => { 5 5 it('should transparently stringify + parse cache key', async () => { 6 6 const producer = async (orgId: string) => orgId; 7 - const cachedProducer = cached({ producer, directives: { freshUntilAge: 1 } }); 7 + const cachedProducer = cached({ 8 + producer, 9 + directives: { freshUntilAge: 1 }, 10 + }); 8 11 const res = await cachedProducer('test'); 9 12 expect(res).toBe('test'); 10 13 ··· 18 21 19 22 it('should not cache promise rejections, and should propagate them', async () => { 20 23 const producer = jest.fn(async (_it: string) => Promise.reject('anything')); 21 - const cachedProducer = cached({ producer, directives: { freshUntilAge: 1 } }); 24 + const cachedProducer = cached({ 25 + producer, 26 + directives: { freshUntilAge: 1 }, 27 + }); 22 28 23 29 await cachedProducer('').catch(() => {}); 24 30 const res = await cachedProducer('').catch((e) => e); ··· 29 35 30 36 it("should only call the producer once during the cache's freshUntilAge", async () => { 31 37 const producer = jest.fn(async (it: string) => it); 32 - const cachedProducer = cached({ producer, directives: { freshUntilAge: 1 } }); 38 + const cachedProducer = cached({ 39 + producer, 40 + directives: { freshUntilAge: 1 }, 41 + }); 33 42 34 43 await cachedProducer('test'); 35 44 await cachedProducer('test');
+2 -2
server/utils/caching.ts
··· 1 + import { type ReadonlyDeep } from 'type-fest'; 2 + 1 3 import { 2 4 Cache, 3 5 MemoryStore, ··· 8 10 type Entry, 9 11 type ProducerDirectives, 10 12 } from '../lib/cache/index.js'; 11 - import { type ReadonlyDeep } from 'type-fest'; 12 - 13 13 import { jsonParse, jsonStringify } from './encoding.js'; 14 14 import { type JSON } from './json-schema-types.js'; 15 15
+71 -68
server/utils/json-schema-types.ts
··· 54 54 type: readonly (T extends number 55 55 ? JSONType<'number' | 'integer', IsPartial> 56 56 : T extends string 57 - ? JSONType<'string', IsPartial> 58 - : T extends boolean 59 - ? JSONType<'boolean', IsPartial> 60 - : T extends null 61 - ? JSONType<'null', IsPartial> 62 - : never)[]; 57 + ? JSONType<'string', IsPartial> 58 + : T extends boolean 59 + ? JSONType<'boolean', IsPartial> 60 + : T extends null 61 + ? JSONType<'null', IsPartial> 62 + : never)[]; 63 63 } & UnionToIntersection< 64 64 T extends number 65 65 ? NumberKeywords 66 66 : T extends string 67 - ? StringKeywords 68 - : T extends boolean 69 - ? // eslint-disable-next-line @typescript-eslint/no-restricted-types 70 - {} 71 - : never 67 + ? StringKeywords 68 + : T extends boolean 69 + ? // eslint-disable-next-line @typescript-eslint/no-restricted-types 70 + {} 71 + : never 72 72 >) 73 73 // this covers "normal" types; it's last so typescript looks to it first for errors 74 74 | ((T extends number ··· 76 76 type: JSONType<'number' | 'integer', IsPartial>; 77 77 } & NumberKeywords 78 78 : T extends string 79 - ? { 80 - type: JSONType<'string', IsPartial>; 81 - } & StringKeywords 82 - : T extends boolean 83 - ? { 84 - type: JSONType<'boolean', IsPartial>; 85 - } 86 - : T extends readonly [any, ...any[]] 87 - ? { 88 - // JSON AnySchema for tuple 89 - type: JSONType<'array', IsPartial>; 90 - items: { 91 - readonly [K in keyof T]-?: UncheckedJSONSchemaType<T[K], false> & 92 - Nullable<T[K]>; 93 - } & { length: T['length'] }; 94 - minItems: T['length']; 95 - } & ({ maxItems: T['length'] } | { additionalItems: false }) 96 - : T extends readonly any[] 97 - ? { 98 - type: JSONType<'array', IsPartial>; 99 - items: UncheckedJSONSchemaType<T[0], false>; 100 - minItems?: number; 101 - maxItems?: number; 102 - uniqueItems?: true; 103 - additionalItems?: never; 104 - } 105 - : T extends Record<string, any> 106 - ? { 107 - // JSON AnySchema for records and dictionaries 108 - // "required" is not optional because it is often forgotten 109 - // "properties" are optional for more concise dictionary schemas 110 - // "patternProperties" and can be only used with interfaces that have string index 111 - type: JSONType<'object', IsPartial>; 112 - additionalProperties?: 113 - | boolean 114 - | UncheckedJSONSchemaType<T[string], false>; 115 - properties?: IsPartial extends true 116 - ? Partial<PropertiesSchema<T>> 117 - : PropertiesSchema<T>; 118 - patternProperties?: Record< 119 - string, 120 - UncheckedJSONSchemaType<T[string], false> 121 - >; 122 - dependencies?: { 123 - [K in keyof T]?: Readonly<(keyof T)[]> | PartialSchema<T>; 124 - }; 125 - dependentSchemas?: { [K in keyof T]?: PartialSchema<T> }; 126 - minProperties?: number; 127 - maxProperties?: number; 128 - } & (IsPartial extends true // "required" is not necessary if it's a non-partial type with no required keys // are listed it only asserts that optional cannot be listed. // "required" type does not guarantee that all required properties 129 - ? { required: Readonly<(keyof T)[]> } 130 - : [RequiredMembers<T>] extends [never] 131 - ? { required?: Readonly<RequiredMembers<T>[]> } 132 - : { required: Readonly<RequiredMembers<T>[]> }) 133 - : T extends null 134 - ? { type: JSONType<'null', IsPartial> } 135 - : never) & { 79 + ? { 80 + type: JSONType<'string', IsPartial>; 81 + } & StringKeywords 82 + : T extends boolean 83 + ? { 84 + type: JSONType<'boolean', IsPartial>; 85 + } 86 + : T extends readonly [any, ...any[]] 87 + ? { 88 + // JSON AnySchema for tuple 89 + type: JSONType<'array', IsPartial>; 90 + items: { 91 + readonly [K in keyof T]-?: UncheckedJSONSchemaType< 92 + T[K], 93 + false 94 + > & 95 + Nullable<T[K]>; 96 + } & { length: T['length'] }; 97 + minItems: T['length']; 98 + } & ({ maxItems: T['length'] } | { additionalItems: false }) 99 + : T extends readonly any[] 100 + ? { 101 + type: JSONType<'array', IsPartial>; 102 + items: UncheckedJSONSchemaType<T[0], false>; 103 + minItems?: number; 104 + maxItems?: number; 105 + uniqueItems?: true; 106 + additionalItems?: never; 107 + } 108 + : T extends Record<string, any> 109 + ? { 110 + // JSON AnySchema for records and dictionaries 111 + // "required" is not optional because it is often forgotten 112 + // "properties" are optional for more concise dictionary schemas 113 + // "patternProperties" and can be only used with interfaces that have string index 114 + type: JSONType<'object', IsPartial>; 115 + additionalProperties?: 116 + | boolean 117 + | UncheckedJSONSchemaType<T[string], false>; 118 + properties?: IsPartial extends true 119 + ? Partial<PropertiesSchema<T>> 120 + : PropertiesSchema<T>; 121 + patternProperties?: Record< 122 + string, 123 + UncheckedJSONSchemaType<T[string], false> 124 + >; 125 + dependencies?: { 126 + [K in keyof T]?: Readonly<(keyof T)[]> | PartialSchema<T>; 127 + }; 128 + dependentSchemas?: { [K in keyof T]?: PartialSchema<T> }; 129 + minProperties?: number; 130 + maxProperties?: number; 131 + } & (IsPartial extends true // "required" is not necessary if it's a non-partial type with no required keys // are listed it only asserts that optional cannot be listed. // "required" type does not guarantee that all required properties 132 + ? { required: Readonly<(keyof T)[]> } 133 + : [RequiredMembers<T>] extends [never] 134 + ? { required?: Readonly<RequiredMembers<T>[]> } 135 + : { required: Readonly<RequiredMembers<T>[]> }) 136 + : T extends null 137 + ? { type: JSONType<'null', IsPartial> } 138 + : never) & { 136 139 allOf?: Readonly<PartialSchema<T>[]>; 137 140 anyOf?: Readonly<PartialSchema<T>[]>; 138 141 oneOf?: Readonly<PartialSchema<T>[]>;
+16 -16
server/utils/kysely.ts
··· 142 142 : never 143 143 : never 144 144 : Builder extends 145 - | SelectQueryBuilder< 146 - infer DB, 147 - infer TB, 148 - Selection<infer DB, infer TB, infer SelectionType> 149 - > 150 - | InsertQueryBuilder< 151 - infer DB, 152 - infer TB, 153 - Selection<infer DB, infer TB, infer SelectionType> 154 - > 155 - ? FixKyselyRowCorrelation< 156 - DB[TB] & WhereClause, 157 - readonly (SelectionType & string)[], 158 - [] 159 - > 160 - : never; 145 + | SelectQueryBuilder< 146 + infer DB, 147 + infer TB, 148 + Selection<infer DB, infer TB, infer SelectionType> 149 + > 150 + | InsertQueryBuilder< 151 + infer DB, 152 + infer TB, 153 + Selection<infer DB, infer TB, infer SelectionType> 154 + > 155 + ? FixKyselyRowCorrelation< 156 + DB[TB] & WhereClause, 157 + readonly (SelectionType & string)[], 158 + [] 159 + > 160 + : never; 161 161 162 162 /** 163 163 * Creates an object with a key, where the type of the value for that key
+9 -8
server/utils/logging.ts
··· 7 7 // Serialize error objects properly since they don't have enumerable properties 8 8 const serialized = { 9 9 ...m, 10 - error: m.error instanceof Error 11 - ? { 12 - name: m.error.name, 13 - message: m.error.message, 14 - stack: m.error.stack, 15 - ...(m.error as unknown as Record<string, unknown>) 16 - } 17 - : m.error 10 + error: 11 + m.error instanceof Error 12 + ? { 13 + name: m.error.name, 14 + message: m.error.message, 15 + stack: m.error.stack, 16 + ...(m.error as unknown as Record<string, unknown>), 17 + } 18 + : m.error, 18 19 }; 19 20 console.error(jsonStringify(serialized)); 20 21 }
+7 -7
server/utils/misc.ts
··· 163 163 _camelCaseObjectKeysToSnakeCaseDeepHelper(it), 164 164 ) as SnakeCasedPropertiesDeepWithArrays<O>) 165 165 : _.isPlainObject(o) 166 - ? (Object.fromEntries( 167 - Object.entries(o as { [k: string]: unknown }).map(([k, v]) => [ 168 - camelToSnakeCase(k), 169 - _camelCaseObjectKeysToSnakeCaseDeepHelper(v), 170 - ]), 171 - ) as SnakeCasedPropertiesDeepWithArrays<O>) 172 - : (o as SnakeCasedPropertiesDeepWithArrays<O>); 166 + ? (Object.fromEntries( 167 + Object.entries(o as { [k: string]: unknown }).map(([k, v]) => [ 168 + camelToSnakeCase(k), 169 + _camelCaseObjectKeysToSnakeCaseDeepHelper(v), 170 + ]), 171 + ) as SnakeCasedPropertiesDeepWithArrays<O>) 172 + : (o as SnakeCasedPropertiesDeepWithArrays<O>); 173 173 } 174 174 175 175 /**
+54 -46
server/utils/typescript-types.ts
··· 26 26 | readonly [...never[]] 27 27 ? readonly [] 28 28 : T extends readonly [infer U, ...infer V] 29 - ? readonly [ 30 - SnakeCasedPropertiesDeepWithArrays<U>, 31 - ...SnakeCasedPropertiesDeepWithArrays<V>, 32 - ] 33 - : T extends readonly [...infer U, infer V] 34 - ? readonly [ 35 - ...SnakeCasedPropertiesDeepWithArrays<U>, 36 - SnakeCasedPropertiesDeepWithArrays<V>, 37 - ] 38 - : T extends ReadonlyArray<infer ItemType> 39 - ? ReadonlyArray<SnakeCasedPropertiesDeepWithArrays<ItemType>> 40 - : T extends object 41 - ? { [K in keyof T as SnakeCase<K>]: SnakeCasedPropertiesDeepWithArrays<T[K]> } 42 - : T; 29 + ? readonly [ 30 + SnakeCasedPropertiesDeepWithArrays<U>, 31 + ...SnakeCasedPropertiesDeepWithArrays<V>, 32 + ] 33 + : T extends readonly [...infer U, infer V] 34 + ? readonly [ 35 + ...SnakeCasedPropertiesDeepWithArrays<U>, 36 + SnakeCasedPropertiesDeepWithArrays<V>, 37 + ] 38 + : T extends ReadonlyArray<infer ItemType> 39 + ? ReadonlyArray<SnakeCasedPropertiesDeepWithArrays<ItemType>> 40 + : T extends object 41 + ? { 42 + [K in keyof T as SnakeCase<K>]: SnakeCasedPropertiesDeepWithArrays< 43 + T[K] 44 + >; 45 + } 46 + : T; 43 47 44 48 export type StringKeys<T extends object> = keyof T & string; 45 49 ··· 98 102 export type If<Cond extends boolean, IfTrue, IfFalse> = Cond extends true 99 103 ? IfTrue 100 104 : Cond extends false 101 - ? IfFalse 102 - : IfTrue | IfFalse; 105 + ? IfFalse 106 + : IfTrue | IfFalse; 103 107 104 108 export type NullableKeysOf<T> = { 105 109 [K in keyof T]-?: null extends T[K] ? K : never; ··· 158 162 export type DropN<T extends readonly unknown[], N extends number> = N extends 0 159 163 ? T 160 164 : T extends readonly [unknown, ...infer U] // @ts-ignore 161 - ? DropN<U, Nat2Number<Dec<Number2Nat<N>>>> 162 - : never; 165 + ? DropN<U, Nat2Number<Dec<Number2Nat<N>>>> 166 + : never; 163 167 164 168 // Types for doing math in TS, by representing each natural number as a tuple 165 169 // type with n elements, and exploiting its ability to track a tuple's length ··· 171 175 ? T 172 176 : never; 173 177 type Nat2Number<N extends SomeNat> = N['length']; 174 - type Number2Nat< 175 - I extends number, 176 - N extends SomeNat = Zero, 177 - > = I extends Nat2Number<N> ? N : Number2Nat<I, Succ<N>>; 178 + type Number2Nat<I extends number, N extends SomeNat = Zero> = 179 + I extends Nat2Number<N> ? N : Number2Nat<I, Succ<N>>; 178 180 // type NumericToNat<I extends string> = I extends `${infer T extends number}` 179 181 // ? Number2Nat<T> 180 182 // : never; ··· 441 443 TagWithValues extends object, 442 444 Map extends { [K in TagValues]: unknown }, 443 445 TagKey extends keyof TagWithValues = keyof TagWithValues, 444 - TagValues extends TagWithValues[TagKey] & 445 - (string | number | symbol) = TagWithValues[TagKey] & 446 - (string | number | symbol), 446 + TagValues extends TagWithValues[TagKey] & (string | number | symbol) = 447 + TagWithValues[TagKey] & (string | number | symbol), 447 448 > = { [K in TagValues]: Simplify<{ [K2 in TagKey]: K } & Map[K]> }[TagValues]; 448 449 449 450 /** ··· 512 513 > = Type extends Search 513 514 ? Replacement 514 515 : Type extends Opaque<unknown, infer Tag> 515 - ? Opaque<ReplaceDeep<UnwrapOpaque<Type>, Search, Replacement>, Tag> 516 - : Type extends Primitive | Date | RegExp 517 - ? Type 518 - : // Treat Promises and AsyncIterables specially because, while it's possible 519 - // to do replacement totally structurally, that's likely undesirable and might 520 - // push up against TS limits deep in the replacement 521 - Type extends Promise<infer T> 522 - ? Promise<ReplaceDeep<T, Search, Replacement, IncludeFunctions>> 523 - : Type extends AsyncIterable<infer T> 524 - ? Simplify< 525 - AsyncIterable<ReplaceDeep<T, Search, Replacement, IncludeFunctions>> & 526 - Omit<Type, typeof Symbol.asyncIterator> 527 - > 528 - : IncludeFunctions extends true 529 - ? Type extends (...args: infer Args) => infer R 530 - ? ( 531 - ...args: ReplaceDeep<Args, Search, Replacement, IncludeFunctions> & 532 - unknown[] 533 - ) => ReplaceDeep<R, Search, Replacement, IncludeFunctions> 534 - : ReplaceObjectDeep<Type, Search, Replacement, IncludeFunctions> 535 - : ReplaceObjectDeep<Type, Search, Replacement, IncludeFunctions>; 516 + ? Opaque<ReplaceDeep<UnwrapOpaque<Type>, Search, Replacement>, Tag> 517 + : Type extends Primitive | Date | RegExp 518 + ? Type 519 + : // Treat Promises and AsyncIterables specially because, while it's possible 520 + // to do replacement totally structurally, that's likely undesirable and might 521 + // push up against TS limits deep in the replacement 522 + Type extends Promise<infer T> 523 + ? Promise<ReplaceDeep<T, Search, Replacement, IncludeFunctions>> 524 + : Type extends AsyncIterable<infer T> 525 + ? Simplify< 526 + AsyncIterable< 527 + ReplaceDeep<T, Search, Replacement, IncludeFunctions> 528 + > & 529 + Omit<Type, typeof Symbol.asyncIterator> 530 + > 531 + : IncludeFunctions extends true 532 + ? Type extends (...args: infer Args) => infer R 533 + ? ( 534 + ...args: ReplaceDeep< 535 + Args, 536 + Search, 537 + Replacement, 538 + IncludeFunctions 539 + > & 540 + unknown[] 541 + ) => ReplaceDeep<R, Search, Replacement, IncludeFunctions> 542 + : ReplaceObjectDeep<Type, Search, Replacement, IncludeFunctions> 543 + : ReplaceObjectDeep<Type, Search, Replacement, IncludeFunctions>; 536 544 537 545 type ReplaceObjectDeep< 538 546 Type,
+9 -7
server/workers_jobs/ItemProcessingWorker.ts
··· 1 - 2 - import { Queue, Worker as BullWorker, type Job as BullJob } from 'bullmq'; 1 + import { Worker as BullWorker, Queue, type Job as BullJob } from 'bullmq'; 3 2 import { type Cluster } from 'ioredis'; 4 3 import type IORedis from 'ioredis'; 5 4 ··· 151 150 performance.now() - jobStartTime, 152 151 ); 153 152 154 - queue!.getJobCounts('waiting', 'active').then((counts) => { 155 - Meter.itemProcessingQueueDepth.record( 156 - counts.waiting + counts.active, 157 - ); 158 - }).catch(() => {}); 153 + queue! 154 + .getJobCounts('waiting', 'active') 155 + .then((counts) => { 156 + Meter.itemProcessingQueueDepth.record( 157 + counts.waiting + counts.active, 158 + ); 159 + }) 160 + .catch(() => {}); 159 161 } catch (e: unknown) { 160 162 tracer.logActiveSpanFailedIfAny(e); 161 163 Meter.itemProcessingFailuresCounter.add(1, {
+43 -27
server/workers_jobs/RunUserRulesJob.ts
··· 36 36 37 37 await userStatisticsService.handleUsersWithChangedScores( 38 38 'user-rules-runner', 39 - async (rescoredUsers: readonly { userId: string; userTypeId: string; orgId: string }[]) => { 39 + async ( 40 + rescoredUsers: readonly { 41 + userId: string; 42 + userTypeId: string; 43 + orgId: string; 44 + }[], 45 + ) => { 40 46 await Promise.all( 41 - rescoredUsers.map(async ({ userId, userTypeId, orgId }: { userId: string; userTypeId: string; orgId: string }) => { 42 - const rulesForUser = userRulesByOrgId[orgId]; 43 - 44 - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 45 - if (!rulesForUser?.length) { 46 - return; 47 - } 48 - 49 - const itemType = await getItemTypeEventuallyConsistent({ 47 + rescoredUsers.map( 48 + async ({ 49 + userId, 50 + userTypeId, 50 51 orgId, 51 - typeSelector: { id: userTypeId }, 52 - }); 52 + }: { 53 + userId: string; 54 + userTypeId: string; 55 + orgId: string; 56 + }) => { 57 + const rulesForUser = userRulesByOrgId[orgId]; 53 58 54 - await RuleEngine.runRuleSet( 55 - rulesForUser, 56 - RuleEngine.makeRuleExecutionContext({ 59 + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 60 + if (!rulesForUser?.length) { 61 + return; 62 + } 63 + 64 + const itemType = await getItemTypeEventuallyConsistent({ 57 65 orgId, 58 - input: { 59 - itemId: userId, 60 - itemType: { 61 - id: userTypeId, 62 - kind: 'USER', 63 - name: itemType?.name ?? "unknown", 66 + typeSelector: { id: userTypeId }, 67 + }); 68 + 69 + await RuleEngine.runRuleSet( 70 + rulesForUser, 71 + RuleEngine.makeRuleExecutionContext({ 72 + orgId, 73 + input: { 74 + itemId: userId, 75 + itemType: { 76 + id: userTypeId, 77 + kind: 'USER', 78 + name: itemType?.name ?? 'unknown', 79 + }, 64 80 }, 65 - }, 66 - }), 67 - RuleEnvironment.LIVE, 68 - toCorrelationId({ type: 'user-rule-run', id: nowString }), 69 - ); 70 - }), 81 + }), 82 + RuleEnvironment.LIVE, 83 + toCorrelationId({ type: 'user-rule-run', id: nowString }), 84 + ); 85 + }, 86 + ), 71 87 ); 72 88 }, 73 89 );
+8 -4
types/integration.ts
··· 157 157 * LOGO/IMAGE SECTION: 158 158 * ------------------------------------------------------------ 159 159 * The following logo/image sections are optional. If none provided will use a fallback Coop logo. 160 - * 160 + * 161 161 * Provide either logoUrl and logoWithBackgroundUrl or logoPath and logoWithBackgroundPath. 162 - * 162 + * 163 163 * If you provide logoPath and logoWithBackgroundPath, the server will serve the files at 164 164 * GET /api/v1/integration-logos/:integrationId and GET /api/v1/integration-logos/:integrationId/with-background 165 165 * and set logoUrl and logoWithBackgroundUrl accordingly. ··· 205 205 getCost: () => number; 206 206 /** Run the signal. Input shape is platform-defined; result must have outputType and score. */ 207 207 run: (input: unknown) => Promise<unknown>; 208 - getDisabledInfo: (orgId: string) => Promise< 208 + getDisabledInfo: ( 209 + orgId: string, 210 + ) => Promise< 209 211 | { disabled: false; disabledMessage?: string } 210 212 | { disabled: true; disabledMessage: string } 211 213 >; ··· 249 251 */ 250 252 createSignals?: ( 251 253 context: PluginSignalContext, 252 - ) => ReadonlyArray<Readonly<{ signalTypeId: string; signal: PluginSignalDescriptor }>>; 254 + ) => ReadonlyArray< 255 + Readonly<{ signalTypeId: string; signal: PluginSignalDescriptor }> 256 + >; 253 257 }>; 254 258 255 259 /**