Mirror of https://github.com/roostorg/coop github.com/roostorg/coop
0
fork

Configure Feed

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

[Kysely] Migrate Org murations off sequelize (#292)

* [Kysely] Migrate Org murations off sequelize

* fix lint

* fix test ordering

* test fixes

* fix(routes/tests): drop restricted graphql/datasources import

Replace direct kyselyOrgDeleteById imports in 5 route tests with the
cleanup() function returned by createOrg, satisfying the
import/no-restricted-paths rule that forbids routes/* from importing
graphql/*.

* validation of org using validator package to match sequelize

authored by

Juan Mrad and committed by
GitHub
416d3881 3c43a838

+1628 -346
+5 -6
codegen.yaml
··· 99 99 ContentType: ../models/rules/ItemTypeModel.js#ItemType 100 100 DerivedFieldSource: ../services/derivedFieldsService/helpers.js#DerivedFieldSpecSource 101 101 HashBank: ../models/HashBankModel.js#HashBank 102 - Org: ../models/OrgModel.js#Org 103 - # NB: for now we always resolve MatchingBanks with an Org model, and 104 - # then call methods on the org model to resolve the sub-fields. This is 105 - # a bit unusual, as matchingBanks could in theory be a standalone type, 106 - # so we might have to expand this later. 107 - MatchingBanks: ../models/OrgModel.js#Org 102 + Org: ../graphql/datasources/orgKyselyPersistence.js#GraphQLOrgParent 103 + # NB: `MatchingBanks` is currently resolved by returning the `Org` 104 + # parent, and its sub-resolvers only read `org.id`. A bit unusual, as 105 + # `MatchingBanks` could be a standalone type — may expand later. 106 + MatchingBanks: ../graphql/datasources/orgKyselyPersistence.js#GraphQLOrgParent 108 107 User: ../models/UserModel.js#User 109 108 LocationBank: ./datasources/LocationBankApi.js#LocationBankWithoutFullPlacesAPIResponse 110 109 # TODO(Kysely migration): these parents will flip to
+12 -14
server/bin/create-org-and-user.ts
··· 17 17 import yargs from 'yargs'; 18 18 import { hideBin } from 'yargs/helpers'; 19 19 20 + import { kyselyOrgInsert } from '../graphql/datasources/orgKyselyPersistence.js'; 20 21 import getBottle from '../iocContainer/index.js'; 21 22 import { hashPassword } from '../services/userManagementService/index.js'; 22 23 ··· 64 65 const orgId = uid(); 65 66 const userId = uid(); 66 67 67 - // Create the organization 68 - const org = await container.Sequelize.Org.create({ 69 - id: orgId, 70 - email: argv.email, 71 - name: argv.name, 72 - websiteUrl: argv.website, 73 - }); 74 - 75 - // Create signing keys 68 + // Create signing keys 76 69 await container.SigningKeyPairService.createAndStoreSigningKeys(orgId); 77 70 78 - // Create API key 79 - const { apiKey: rawApiKey, record: apiKeyRecord } = 71 + // Create API key 72 + const { apiKey: rawApiKey, record: apiKeyRecord } = 80 73 await container.ApiKeyService.createApiKey( 81 74 orgId, 82 75 'Main API Key', ··· 84 77 null, 85 78 ); 86 79 87 - // Update the org with the API key ID 88 - org.apiKeyId = apiKeyRecord.id; 89 - await org.save(); 80 + const org = await kyselyOrgInsert({ 81 + db: container.KyselyPg, 82 + id: orgId, 83 + email: argv.email, 84 + name: argv.name, 85 + websiteUrl: argv.website, 86 + apiKeyId: apiKeyRecord.id, 87 + }); 90 88 91 89 // Initialize org settings 92 90 await Promise.all([
+171
server/graphql/datasources/OrgApi.test.ts
··· 1 + import { uid } from 'uid'; 2 + 3 + import createOrg from '../../test/fixtureHelpers/createOrg.js'; 4 + import { makeMockedServer } from '../../test/setupMockedServer.js'; 5 + import { makeTestWithFixture } from '../../test/utils.js'; 6 + import { CoopError } from '../../utils/errors.js'; 7 + 8 + describe('OrgAPI', () => { 9 + const testWithFixture = makeTestWithFixture(async () => { 10 + const { deps, shutdown } = await makeMockedServer(); 11 + const { org, cleanup: orgCleanup } = await createOrg( 12 + { 13 + KyselyPg: deps.KyselyPg, 14 + ModerationConfigService: deps.ModerationConfigService, 15 + ApiKeyService: deps.ApiKeyService, 16 + }, 17 + uid(), 18 + ); 19 + return { 20 + deps, 21 + org, 22 + async cleanup() { 23 + await orgCleanup(); 24 + await shutdown(); 25 + }, 26 + }; 27 + }); 28 + 29 + describe('getGraphQLOrgFromId', () => { 30 + testWithFixture('returns the org parent for an existing id', async ({ 31 + deps, 32 + org, 33 + }) => { 34 + const result = await deps.OrgAPIDataSource.getGraphQLOrgFromId(org.id); 35 + expect(result).toMatchObject({ 36 + id: org.id, 37 + name: org.name, 38 + email: org.email, 39 + }); 40 + }); 41 + 42 + testWithFixture( 43 + 'throws when the org does not exist (replaces Sequelize rejectOnEmpty)', 44 + async ({ deps }) => { 45 + const missingId = `missing-${uid()}`; 46 + await expect( 47 + deps.OrgAPIDataSource.getGraphQLOrgFromId(missingId), 48 + ).rejects.toThrow(/Organization not found/); 49 + }, 50 + ); 51 + }); 52 + 53 + describe('updateOrgInfo', () => { 54 + testWithFixture( 55 + 'throws when the org does not exist', 56 + async ({ deps }) => { 57 + await expect( 58 + deps.OrgAPIDataSource.updateOrgInfo(`missing-${uid()}`, { 59 + name: 'whatever', 60 + }), 61 + ).rejects.toThrow(/Organization not found/); 62 + }, 63 + ); 64 + 65 + testWithFixture( 66 + 'returns the updated parent when the org exists', 67 + async ({ deps, org }) => { 68 + const newName = `Renamed_${uid()}`; 69 + const result = await deps.OrgAPIDataSource.updateOrgInfo(org.id, { 70 + name: newName, 71 + }); 72 + expect(result.id).toBe(org.id); 73 + expect(result.name).toBe(newName); 74 + }, 75 + ); 76 + 77 + testWithFixture( 78 + 'throws a BadRequest with a pointer for malformed email', 79 + async ({ deps, org }) => { 80 + await expect( 81 + deps.OrgAPIDataSource.updateOrgInfo(org.id, { 82 + email: 'not-an-email', 83 + }), 84 + ).rejects.toMatchObject({ 85 + name: 'BadRequestError', 86 + status: 400, 87 + pointer: '/input/email', 88 + }); 89 + }, 90 + ); 91 + 92 + testWithFixture( 93 + 'throws a BadRequest for malformed websiteUrl (javascript: scheme)', 94 + async ({ deps, org }) => { 95 + await expect( 96 + deps.OrgAPIDataSource.updateOrgInfo(org.id, { 97 + // eslint-disable-next-line no-script-url 98 + websiteUrl: 'javascript:alert(1)', 99 + }), 100 + ).rejects.toMatchObject({ 101 + name: 'BadRequestError', 102 + pointer: '/input/websiteUrl', 103 + }); 104 + }, 105 + ); 106 + 107 + testWithFixture( 108 + 'throws a BadRequest for empty name', 109 + async ({ deps, org }) => { 110 + await expect( 111 + deps.OrgAPIDataSource.updateOrgInfo(org.id, { name: '' }), 112 + ).rejects.toBeInstanceOf(CoopError); 113 + }, 114 + ); 115 + 116 + testWithFixture( 117 + 'does not touch the DB when validation fails (org not found only surfaces after validation passes)', 118 + async ({ deps }) => { 119 + // If validation ran AFTER the DB lookup we'd get "Organization not 120 + // found" here; ensure we see the BadRequest instead. 121 + await expect( 122 + deps.OrgAPIDataSource.updateOrgInfo(`missing-${uid()}`, { 123 + email: 'not-an-email', 124 + }), 125 + ).rejects.toMatchObject({ 126 + name: 'BadRequestError', 127 + pointer: '/input/email', 128 + }); 129 + }, 130 + ); 131 + }); 132 + 133 + describe('createOrg', () => { 134 + testWithFixture( 135 + 'throws a BadRequest with /input/website pointer for bad website', 136 + async ({ deps }) => { 137 + await expect( 138 + deps.OrgAPIDataSource.createOrg({ 139 + input: { 140 + name: `NewOrg_${uid()}`, 141 + email: `new_${uid()}@example.com`, 142 + // eslint-disable-next-line no-script-url 143 + website: 'javascript:alert(1)', 144 + }, 145 + }), 146 + ).rejects.toMatchObject({ 147 + name: 'BadRequestError', 148 + pointer: '/input/website', 149 + }); 150 + }, 151 + ); 152 + 153 + testWithFixture( 154 + 'throws a BadRequest for malformed email', 155 + async ({ deps }) => { 156 + await expect( 157 + deps.OrgAPIDataSource.createOrg({ 158 + input: { 159 + name: `NewOrg_${uid()}`, 160 + email: 'not-an-email', 161 + website: 'https://example.com', 162 + }, 163 + }), 164 + ).rejects.toMatchObject({ 165 + name: 'BadRequestError', 166 + pointer: '/input/email', 167 + }); 168 + }, 169 + ); 170 + }); 171 + });
+94 -34
server/graphql/datasources/OrgApi.ts
··· 9 9 import { 10 10 CoopError, 11 11 ErrorType, 12 + makeBadRequestError, 12 13 type ErrorInstanceData, 13 14 isCoopErrorOfType, 14 15 } from '../../utils/errors.js'; ··· 18 19 type GQLMutationCreateOrgArgs, 19 20 type GQLRequestDemoInput, 20 21 } from '../generated.js'; 22 + import { 23 + kyselyOrgFindAll, 24 + kyselyOrgFindByEmail, 25 + kyselyOrgFindById, 26 + kyselyOrgFindByName, 27 + kyselyOrgInsert, 28 + kyselyOrgUpdate, 29 + type GraphQLOrgParent, 30 + } from './orgKyselyPersistence.js'; 31 + import { 32 + validateOrgCreateInput, 33 + validateOrgUpdatePatch, 34 + type OrgValidationFailure, 35 + } from './orgValidation.js'; 21 36 22 37 class OrgAPI { 23 38 constructor( 24 39 private readonly orgCreationLogger: Dependencies['OrgCreationLogger'], 25 40 private readonly apiKeyService: Dependencies['ApiKeyService'], 26 41 private readonly sendEmail: Dependencies['sendEmail'], 27 - private readonly sequelize: Dependencies['Sequelize'], 28 42 private readonly signingKeyPairService: Dependencies['SigningKeyPairService'], 29 43 private readonly tracer: Dependencies['Tracer'], 30 44 private readonly moderationConfigService: Dependencies['ModerationConfigService'], ··· 32 46 private readonly config: Dependencies['ConfigService'], 33 47 private readonly orgSettingsService: Dependencies['OrgSettingsService'], 34 48 private readonly manualReviewToolService: Dependencies['ManualReviewToolService'], 35 - ) { 36 - } 49 + private readonly kysely: Dependencies['KyselyPg'], 50 + private readonly sequelize: Dependencies['Sequelize'], 51 + ) {} 37 52 38 53 async createOrg(params: GQLMutationCreateOrgArgs) { 39 54 const { email, name, website } = params.input; 40 - const existingOrgByName = await this.sequelize.Org.findOne({ 41 - where: { name }, 55 + 56 + const validation = validateOrgCreateInput({ 57 + name, 58 + email, 59 + websiteUrl: website, 42 60 }); 61 + if (!validation.ok) { 62 + throw orgValidationFailureToBadRequestError( 63 + validation.failure, 64 + 'createOrg', 65 + ); 66 + } 67 + 68 + const existingOrgByName = await kyselyOrgFindByName(this.kysely, name); 43 69 if (existingOrgByName != null) { 44 70 throw makeOrgNameExistsError({ shouldErrorSpan: true }); 45 71 } 46 - const existingOrgByEmail = await this.sequelize.Org.findOne({ 47 - where: { email }, 48 - }); 72 + const existingOrgByEmail = await kyselyOrgFindByEmail(this.kysely, email); 49 73 50 74 if (existingOrgByEmail != null) { 51 75 throw makeOrgEmailExistsError({ shouldErrorSpan: true }); ··· 67 91 await this.signingKeyPairService.createAndStoreSigningKeys(id); 68 92 69 93 try { 70 - const org = await this.sequelize.Org.create({ 94 + const org = await kyselyOrgInsert({ 95 + db: this.kysely, 71 96 id, 72 97 email, 73 98 name, ··· 100 125 // Create invite token and optionally send email 101 126 async inviteUser(input: GQLInviteUserInput, orgId: string) { 102 127 const { email, role } = input; 103 - const org = await this.sequelize.Org.findByPk(orgId, { 104 - rejectOnEmpty: true, 105 - }); 128 + const org = await kyselyOrgFindById(this.kysely, orgId); 129 + if (org == null) { 130 + throw new Error(`Organization not found: ${orgId}`); 131 + } 106 132 107 133 const token = await this.userManagementService.createInviteUserToken({ 108 134 email, ··· 171 197 return true; 172 198 } 173 199 174 - async getGraphQLOrgFromId(id: string) { 175 - return this.sequelize.Org.findByPk(id, { rejectOnEmpty: true }); 200 + async getGraphQLOrgFromId(id: string): Promise<GraphQLOrgParent> { 201 + const org = await kyselyOrgFindById(this.kysely, id); 202 + if (org == null) { 203 + throw new Error(`Organization not found: ${id}`); 204 + } 205 + return org; 176 206 } 177 207 178 - async getAllGraphQLOrgs() { 179 - return this.sequelize.Org.findAll(); 208 + async getAllGraphQLOrgs(): Promise<GraphQLOrgParent[]> { 209 + return kyselyOrgFindAll(this.kysely); 180 210 } 181 211 182 212 async updateOrgInfo( ··· 187 217 websiteUrl?: string | null; 188 218 onCallAlertEmail?: string | null; 189 219 }, 190 - ) { 191 - const org = await this.sequelize.Org.findByPk(orgId); 192 - if (!org) { 193 - throw new Error('Organization not found'); 220 + ): Promise<GraphQLOrgParent> { 221 + const validation = validateOrgUpdatePatch(input); 222 + if (!validation.ok) { 223 + throw orgValidationFailureToBadRequestError( 224 + validation.failure, 225 + 'updateOrgInfo', 226 + ); 194 227 } 195 228 196 - if (input.name != null) { 197 - org.name = input.name; 198 - } 199 - if (input.email != null) { 200 - org.email = input.email; 201 - } 202 - if (input.websiteUrl != null && input.websiteUrl !== '') { 203 - org.websiteUrl = input.websiteUrl; 204 - } 205 - if (input.onCallAlertEmail !== undefined) { 206 - org.onCallAlertEmail = input.onCallAlertEmail ?? undefined; 229 + const updated = await kyselyOrgUpdate(this.kysely, orgId, { 230 + name: input.name ?? undefined, 231 + email: input.email ?? undefined, 232 + websiteUrl: input.websiteUrl ?? undefined, 233 + onCallAlertEmail: input.onCallAlertEmail, 234 + }); 235 + if (updated == null) { 236 + throw new Error('Organization not found'); 207 237 } 208 238 209 - await org.save(); 239 + return updated; 240 + } 210 241 211 - return org; 242 + /** 243 + * Legacy GraphQL `ContentType` parents still use Sequelize `getActions` on 244 + * item types; load them from the ORM until item types are fully migrated. 245 + */ 246 + async getSequelizeContentTypesForOrg(orgId: string) { 247 + return this.sequelize.ItemType.findAll({ 248 + where: { orgId }, 249 + }); 250 + } 251 + 252 + /** GraphQL `Org.users` / permission filters still use Sequelize `User` models. */ 253 + async getOrgUsersForGraphQL(orgId: string) { 254 + return this.sequelize.User.findAll({ where: { orgId } }); 212 255 } 213 256 214 257 // TODO: ApiKeyService should maybe be its own dataSource, ··· 267 310 | 'InviteUserTokenExpiredError' 268 311 | 'InviteUserTokenMissingError'; 269 312 313 + function orgValidationFailureToBadRequestError( 314 + failure: OrgValidationFailure, 315 + mutation: 'createOrg' | 'updateOrgInfo', 316 + ) { 317 + // `createOrg` exposes `websiteUrl` as `website` in its GraphQL input; 318 + // `updateOrgInfo` uses the same field name. 319 + const gqlField = 320 + mutation === 'createOrg' && failure.field === 'websiteUrl' 321 + ? 'website' 322 + : failure.field; 323 + return makeBadRequestError(failure.message, { 324 + pointer: `/input/${gqlField}`, 325 + shouldErrorSpan: false, 326 + }); 327 + } 328 + 270 329 export const makeOrgEmailExistsError = (data: ErrorInstanceData) => 271 330 new CoopError({ 272 331 status: 409, ··· 308 367 'OrgCreationLogger', 309 368 'ApiKeyService', 310 369 'sendEmail', 311 - 'Sequelize', 312 370 'SigningKeyPairService', 313 371 'Tracer', 314 372 'ModerationConfigService', ··· 316 374 'ConfigService', 317 375 'OrgSettingsService', 318 376 'ManualReviewToolService', 377 + 'KyselyPg', 378 + 'Sequelize', 319 379 ], 320 380 OrgAPI, 321 381 );
+5 -3
server/graphql/datasources/RuleApi.test.ts
··· 27 27 const { deps, shutdown } = await makeMockedServer(); 28 28 const { ModerationConfigService } = deps; 29 29 const { org, cleanup: orgCleanup } = await createOrg( 30 - deps.Sequelize, 31 - ModerationConfigService, 32 - deps.ApiKeyService, 30 + { 31 + KyselyPg: deps.KyselyPg, 32 + ModerationConfigService, 33 + ApiKeyService: deps.ApiKeyService, 34 + }, 33 35 uid(), 34 36 ); 35 37 const { user, cleanup: userCleanup } = await createUser(
+8
server/graphql/datasources/RuleApi.ts
··· 331 331 return buildGraphqlRuleParent(plain, this.graphQlRuleParentDeps); 332 332 } 333 333 334 + /** GraphQL rule parents for `Org.rules` and MRT enqueue-source payloads. */ 335 + async getGraphQLRulesForOrg(orgId: string) { 336 + const plains = await this.moderationConfigService.getRulesForOrg(orgId); 337 + return plains.map((plain) => 338 + buildGraphqlRuleParent(plain, this.graphQlRuleParentDeps), 339 + ); 340 + } 341 + 334 342 async createContentRule( 335 343 input: GQLCreateContentRuleInput, 336 344 userId: string,
+238
server/graphql/datasources/orgKyselyPersistence.test.ts
··· 1 + import { faker } from '@faker-js/faker'; 2 + import { uid } from 'uid'; 3 + 4 + import createOrg from '../../test/fixtureHelpers/createOrg.js'; 5 + import { makeMockedServer } from '../../test/setupMockedServer.js'; 6 + import { makeTestWithFixture } from '../../test/utils.js'; 7 + import { 8 + kyselyOrgDeleteById, 9 + kyselyOrgFindByEmail, 10 + kyselyOrgFindById, 11 + kyselyOrgFindByName, 12 + kyselyOrgInsert, 13 + kyselyOrgUpdate, 14 + } from './orgKyselyPersistence.js'; 15 + 16 + describe('orgKyselyPersistence', () => { 17 + const testWithFixture = makeTestWithFixture(async () => { 18 + const { deps, shutdown } = await makeMockedServer(); 19 + const { org, cleanup: orgCleanup } = await createOrg( 20 + { 21 + KyselyPg: deps.KyselyPg, 22 + ModerationConfigService: deps.ModerationConfigService, 23 + ApiKeyService: deps.ApiKeyService, 24 + }, 25 + uid(), 26 + ); 27 + return { 28 + deps, 29 + org, 30 + async cleanup() { 31 + await orgCleanup(); 32 + await shutdown(); 33 + }, 34 + }; 35 + }); 36 + 37 + describe('kyselyOrgFindBy*', () => { 38 + testWithFixture( 39 + 'findById / findByName / findByEmail return the row when it exists', 40 + async ({ deps, org }) => { 41 + const byId = await kyselyOrgFindById(deps.KyselyPg, org.id); 42 + const byName = await kyselyOrgFindByName(deps.KyselyPg, org.name); 43 + const byEmail = await kyselyOrgFindByEmail(deps.KyselyPg, org.email); 44 + 45 + expect(byId).toMatchObject({ id: org.id, name: org.name }); 46 + expect(byName).toMatchObject({ id: org.id }); 47 + expect(byEmail).toMatchObject({ id: org.id }); 48 + }, 49 + ); 50 + 51 + testWithFixture( 52 + 'findById / findByName / findByEmail return undefined (not null) when missing', 53 + async ({ deps }) => { 54 + const byId = await kyselyOrgFindById(deps.KyselyPg, `missing-${uid()}`); 55 + const byName = await kyselyOrgFindByName( 56 + deps.KyselyPg, 57 + `missing-${uid()}`, 58 + ); 59 + const byEmail = await kyselyOrgFindByEmail( 60 + deps.KyselyPg, 61 + `missing-${uid()}@example.com`, 62 + ); 63 + 64 + // Callers use `== null` checks, but pin `undefined` explicitly to 65 + // catch silent drift to `null`. 66 + expect(byId).toBeUndefined(); 67 + expect(byName).toBeUndefined(); 68 + expect(byEmail).toBeUndefined(); 69 + }, 70 + ); 71 + }); 72 + 73 + describe('kyselyOrgInsert', () => { 74 + testWithFixture( 75 + 'throws an invariant error for malformed input (defense-in-depth)', 76 + async ({ deps }) => { 77 + await expect( 78 + kyselyOrgInsert({ 79 + db: deps.KyselyPg, 80 + id: uid(), 81 + email: 'not-an-email', 82 + name: `Bad_${uid()}`, 83 + websiteUrl: 'https://example.com', 84 + }), 85 + ).rejects.toThrow(/kyselyOrgInsert invariant violated: email/); 86 + }, 87 + ); 88 + 89 + testWithFixture( 90 + 'apiKeyId is optional and defaults to NULL in the database', 91 + async ({ deps }) => { 92 + const id = uid(); 93 + const inserted = await kyselyOrgInsert({ 94 + db: deps.KyselyPg, 95 + id, 96 + email: faker.internet.email(), 97 + name: `Insert_NoApiKey_${id}`, 98 + websiteUrl: faker.internet.url(), 99 + // apiKeyId intentionally omitted 100 + }); 101 + 102 + try { 103 + expect(inserted.id).toBe(id); 104 + 105 + const row = await deps.KyselyPg 106 + .selectFrom('public.orgs') 107 + .select(['api_key_id', 'on_call_alert_email']) 108 + .where('id', '=', id) 109 + .executeTakeFirstOrThrow(); 110 + expect(row.api_key_id).toBeNull(); 111 + expect(row.on_call_alert_email).toBeNull(); 112 + } finally { 113 + await kyselyOrgDeleteById(deps.KyselyPg, id); 114 + } 115 + }, 116 + ); 117 + }); 118 + 119 + describe('kyselyOrgUpdate', () => { 120 + testWithFixture( 121 + 'throws an invariant error for malformed patch (defense-in-depth)', 122 + async ({ deps, org }) => { 123 + await expect( 124 + kyselyOrgUpdate(deps.KyselyPg, org.id, { 125 + // eslint-disable-next-line no-script-url 126 + websiteUrl: 'javascript:alert(1)', 127 + }), 128 + ).rejects.toThrow(/kyselyOrgUpdate invariant violated: websiteUrl/); 129 + }, 130 + ); 131 + 132 + testWithFixture( 133 + 'returns undefined when the org does not exist', 134 + async ({ deps }) => { 135 + const result = await kyselyOrgUpdate( 136 + deps.KyselyPg, 137 + `missing-${uid()}`, 138 + { name: 'whatever' }, 139 + ); 140 + expect(result).toBeUndefined(); 141 + }, 142 + ); 143 + 144 + testWithFixture( 145 + 'empty-string websiteUrl is treated as "no change" (Sequelize parity)', 146 + async ({ deps, org }) => { 147 + const before = await kyselyOrgFindById(deps.KyselyPg, org.id); 148 + expect(before).toBeDefined(); 149 + 150 + const updated = await kyselyOrgUpdate(deps.KyselyPg, org.id, { 151 + websiteUrl: '', 152 + }); 153 + 154 + expect(updated).toBeDefined(); 155 + expect(updated!.websiteUrl).toBe(before!.websiteUrl); 156 + }, 157 + ); 158 + 159 + testWithFixture( 160 + 'null name / email / websiteUrl are skipped (only updated_at changes)', 161 + async ({ deps, org }) => { 162 + const before = await kyselyOrgFindById(deps.KyselyPg, org.id); 163 + expect(before).toBeDefined(); 164 + 165 + const updated = await kyselyOrgUpdate(deps.KyselyPg, org.id, { 166 + name: null, 167 + email: null, 168 + websiteUrl: null, 169 + }); 170 + 171 + expect(updated).toBeDefined(); 172 + expect(updated!.name).toBe(before!.name); 173 + expect(updated!.email).toBe(before!.email); 174 + expect(updated!.websiteUrl).toBe(before!.websiteUrl); 175 + }, 176 + ); 177 + 178 + testWithFixture( 179 + 'onCallAlertEmail: undefined skips, null clears, string sets', 180 + async ({ deps, org }) => { 181 + // Start by setting a value, then verify the three semantics. 182 + const initial = 'oncall@example.com'; 183 + const set = await kyselyOrgUpdate(deps.KyselyPg, org.id, { 184 + onCallAlertEmail: initial, 185 + }); 186 + expect(set!.onCallAlertEmail).toBe(initial); 187 + 188 + // undefined -> skip (value is preserved) 189 + const skipped = await kyselyOrgUpdate(deps.KyselyPg, org.id, { 190 + name: 'unrelated-touch', 191 + }); 192 + expect(skipped!.onCallAlertEmail).toBe(initial); 193 + 194 + // null -> clear 195 + const cleared = await kyselyOrgUpdate(deps.KyselyPg, org.id, { 196 + onCallAlertEmail: null, 197 + }); 198 + expect(cleared!.onCallAlertEmail).toBeNull(); 199 + }, 200 + ); 201 + 202 + testWithFixture( 203 + 'updates the provided fields and bumps updated_at', 204 + async ({ deps, org }) => { 205 + const newName = `Renamed_${uid()}`; 206 + const newWebsite = 'https://renamed.example.com'; 207 + 208 + // Read updated_at directly since it isn't part of GraphQLOrgParent. 209 + const beforeRow = await deps.KyselyPg 210 + .selectFrom('public.orgs') 211 + .select(['updated_at']) 212 + .where('id', '=', org.id) 213 + .executeTakeFirstOrThrow(); 214 + 215 + // Tiny wait so the new updated_at is strictly greater. Without this, 216 + // sub-millisecond updates can produce equal timestamps on fast hosts. 217 + await new Promise((resolve) => setTimeout(resolve, 5)); 218 + 219 + const updated = await kyselyOrgUpdate(deps.KyselyPg, org.id, { 220 + name: newName, 221 + websiteUrl: newWebsite, 222 + }); 223 + 224 + expect(updated!.name).toBe(newName); 225 + expect(updated!.websiteUrl).toBe(newWebsite); 226 + 227 + const afterRow = await deps.KyselyPg 228 + .selectFrom('public.orgs') 229 + .select(['updated_at']) 230 + .where('id', '=', org.id) 231 + .executeTakeFirstOrThrow(); 232 + expect(afterRow.updated_at.getTime()).toBeGreaterThan( 233 + beforeRow.updated_at.getTime(), 234 + ); 235 + }, 236 + ); 237 + }); 238 + });
+192
server/graphql/datasources/orgKyselyPersistence.ts
··· 1 + import { type Kysely } from 'kysely'; 2 + 3 + import { type CoreAppTablesPg } from '../../services/coreAppTables.js'; 4 + import { 5 + validateOrgCreateInput, 6 + validateOrgUpdatePatch, 7 + } from './orgValidation.js'; 8 + 9 + /** 10 + * GraphQL `Org` parent shape. Field resolvers only read `id` from this; the 11 + * remaining columns are exposed for callers that need them (e.g. the invite 12 + * email uses `org.name`). Intentionally mirrors the columns on `public.orgs` 13 + * that are exposed via GraphQL — no Sequelize associations. 14 + */ 15 + export type GraphQLOrgParent = { 16 + id: string; 17 + name: string; 18 + email: string; 19 + websiteUrl: string; 20 + onCallAlertEmail: string | null; 21 + }; 22 + 23 + /** 24 + * Functions in this module only touch `public.orgs`; typing the param as 25 + * `Kysely<CoreAppTablesPg>` lets callers pass either `Dependencies['KyselyPg']` 26 + * (`Kysely<any>`) or a more specific `Kysely<CombinedPg>` without casting. 27 + */ 28 + type OrgsDb = Kysely<CoreAppTablesPg>; 29 + 30 + function rowToGraphQLOrgParent(row: { 31 + id: string; 32 + name: string; 33 + email: string; 34 + website_url: string; 35 + on_call_alert_email: string | null; 36 + }): GraphQLOrgParent { 37 + return { 38 + id: row.id, 39 + name: row.name, 40 + email: row.email, 41 + websiteUrl: row.website_url, 42 + onCallAlertEmail: row.on_call_alert_email, 43 + }; 44 + } 45 + 46 + export async function kyselyOrgFindById( 47 + db: OrgsDb, 48 + id: string, 49 + ): Promise<GraphQLOrgParent | undefined> { 50 + const row = await db 51 + .selectFrom('public.orgs') 52 + .selectAll() 53 + .where('id', '=', id) 54 + .executeTakeFirst(); 55 + return row === undefined ? undefined : rowToGraphQLOrgParent(row); 56 + } 57 + 58 + export async function kyselyOrgFindByName( 59 + db: OrgsDb, 60 + name: string, 61 + ): Promise<GraphQLOrgParent | undefined> { 62 + const row = await db 63 + .selectFrom('public.orgs') 64 + .selectAll() 65 + .where('name', '=', name) 66 + .executeTakeFirst(); 67 + return row === undefined ? undefined : rowToGraphQLOrgParent(row); 68 + } 69 + 70 + export async function kyselyOrgFindByEmail( 71 + db: OrgsDb, 72 + email: string, 73 + ): Promise<GraphQLOrgParent | undefined> { 74 + const row = await db 75 + .selectFrom('public.orgs') 76 + .selectAll() 77 + .where('email', '=', email) 78 + .executeTakeFirst(); 79 + return row === undefined ? undefined : rowToGraphQLOrgParent(row); 80 + } 81 + 82 + export async function kyselyOrgFindAll( 83 + db: OrgsDb, 84 + ): Promise<GraphQLOrgParent[]> { 85 + const rows = await db 86 + .selectFrom('public.orgs') 87 + .selectAll() 88 + .orderBy('name', 'asc') 89 + .execute(); 90 + return rows.map(rowToGraphQLOrgParent); 91 + } 92 + 93 + export async function kyselyOrgInsert(opts: { 94 + db: OrgsDb; 95 + id: string; 96 + email: string; 97 + name: string; 98 + websiteUrl: string; 99 + // `api_key_id` is nullable in the schema and was optional on the Sequelize 100 + // model. Keep it optional here so callers that don't yet have an API key 101 + // (e.g. legacy fixtures) can still insert. 102 + apiKeyId?: string | null; 103 + onCallAlertEmail?: string | null; 104 + }): Promise<GraphQLOrgParent> { 105 + // Defense-in-depth so non-GraphQL callers (fixtures, scripts) can't insert 106 + // invalid rows; user-facing validation lives in `OrgAPI`. 107 + const validation = validateOrgCreateInput({ 108 + name: opts.name, 109 + email: opts.email, 110 + websiteUrl: opts.websiteUrl, 111 + onCallAlertEmail: opts.onCallAlertEmail, 112 + }); 113 + if (!validation.ok) { 114 + throw new Error( 115 + `kyselyOrgInsert invariant violated: ${validation.failure.field}: ${validation.failure.message}`, 116 + ); 117 + } 118 + 119 + const now = new Date(); 120 + const row = await opts.db 121 + .insertInto('public.orgs') 122 + .values({ 123 + id: opts.id, 124 + email: opts.email, 125 + name: opts.name, 126 + website_url: opts.websiteUrl, 127 + api_key_id: opts.apiKeyId ?? null, 128 + created_at: now, 129 + updated_at: now, 130 + on_call_alert_email: opts.onCallAlertEmail ?? null, 131 + }) 132 + .returningAll() 133 + .executeTakeFirstOrThrow(); 134 + return rowToGraphQLOrgParent(row); 135 + } 136 + 137 + export async function kyselyOrgUpdate( 138 + db: OrgsDb, 139 + orgId: string, 140 + patch: { 141 + name?: string | null; 142 + email?: string | null; 143 + websiteUrl?: string | null; 144 + onCallAlertEmail?: string | null; 145 + }, 146 + ): Promise<GraphQLOrgParent | undefined> { 147 + const validation = validateOrgUpdatePatch(patch); 148 + if (!validation.ok) { 149 + throw new Error( 150 + `kyselyOrgUpdate invariant violated: ${validation.failure.field}: ${validation.failure.message}`, 151 + ); 152 + } 153 + 154 + // `onCallAlertEmail` is intentionally the only field where `null` is set 155 + // on the row (clears the value); other fields treat `null` as skip. 156 + const update: { 157 + name?: string; 158 + email?: string; 159 + website_url?: string; 160 + on_call_alert_email?: string | null; 161 + updated_at: Date; 162 + } = { updated_at: new Date() }; 163 + 164 + if (patch.name != null) { 165 + update.name = patch.name; 166 + } 167 + if (patch.email != null) { 168 + update.email = patch.email; 169 + } 170 + if (patch.websiteUrl != null && patch.websiteUrl !== '') { 171 + update.website_url = patch.websiteUrl; 172 + } 173 + if (patch.onCallAlertEmail !== undefined) { 174 + update.on_call_alert_email = patch.onCallAlertEmail; 175 + } 176 + 177 + const row = await db 178 + .updateTable('public.orgs') 179 + .set(update) 180 + .where('id', '=', orgId) 181 + .returningAll() 182 + .executeTakeFirst(); 183 + 184 + return row === undefined ? undefined : rowToGraphQLOrgParent(row); 185 + } 186 + 187 + export async function kyselyOrgDeleteById( 188 + db: OrgsDb, 189 + orgId: string, 190 + ): Promise<void> { 191 + await db.deleteFrom('public.orgs').where('id', '=', orgId).execute(); 192 + }
+139
server/graphql/datasources/orgValidation.test.ts
··· 1 + import { 2 + validateOrgCreateInput, 3 + validateOrgUpdatePatch, 4 + } from './orgValidation.js'; 5 + 6 + describe('orgValidation', () => { 7 + describe('validateOrgCreateInput', () => { 8 + const validInput = { 9 + name: 'Acme', 10 + email: 'ops@acme.example.com', 11 + websiteUrl: 'https://acme.example.com', 12 + }; 13 + 14 + test('accepts a fully valid input', () => { 15 + expect(validateOrgCreateInput(validInput)).toEqual({ ok: true }); 16 + }); 17 + 18 + test('accepts a valid optional onCallAlertEmail', () => { 19 + expect( 20 + validateOrgCreateInput({ 21 + ...validInput, 22 + onCallAlertEmail: 'oncall@acme.example.com', 23 + }), 24 + ).toEqual({ ok: true }); 25 + }); 26 + 27 + test.each([ 28 + ['empty', ''], 29 + ['whitespace only', ' '], 30 + ])('rejects name that is %s', (_label, name) => { 31 + const result = validateOrgCreateInput({ ...validInput, name }); 32 + expect(result.ok).toBe(false); 33 + if (!result.ok) { 34 + expect(result.failure.field).toBe('name'); 35 + } 36 + }); 37 + 38 + test.each([ 39 + ['empty', ''], 40 + ['missing @', 'not-an-email'], 41 + ['missing domain', 'foo@'], 42 + ['missing tld', 'foo@bar'], 43 + ['contains space', 'foo @bar.com'], 44 + ])('rejects email that is %s', (_label, email) => { 45 + const result = validateOrgCreateInput({ ...validInput, email }); 46 + expect(result.ok).toBe(false); 47 + if (!result.ok) { 48 + expect(result.failure.field).toBe('email'); 49 + } 50 + }); 51 + 52 + test.each([ 53 + ['empty', ''], 54 + ['not a URL', 'definitely not a url'], 55 + // eslint-disable-next-line no-script-url 56 + ['javascript scheme', 'javascript:alert(1)'], 57 + ['ftp scheme', 'ftp://acme.example.com'], 58 + ])('rejects websiteUrl that is %s', (_label, websiteUrl) => { 59 + const result = validateOrgCreateInput({ ...validInput, websiteUrl }); 60 + expect(result.ok).toBe(false); 61 + if (!result.ok) { 62 + expect(result.failure.field).toBe('websiteUrl'); 63 + } 64 + }); 65 + 66 + test('rejects invalid onCallAlertEmail when provided', () => { 67 + const result = validateOrgCreateInput({ 68 + ...validInput, 69 + onCallAlertEmail: 'not-an-email', 70 + }); 71 + expect(result.ok).toBe(false); 72 + if (!result.ok) { 73 + expect(result.failure.field).toBe('onCallAlertEmail'); 74 + } 75 + }); 76 + 77 + test('accepts onCallAlertEmail that is null or empty (optional)', () => { 78 + expect( 79 + validateOrgCreateInput({ ...validInput, onCallAlertEmail: null }), 80 + ).toEqual({ ok: true }); 81 + expect( 82 + validateOrgCreateInput({ ...validInput, onCallAlertEmail: '' }), 83 + ).toEqual({ ok: true }); 84 + }); 85 + }); 86 + 87 + describe('validateOrgUpdatePatch', () => { 88 + test('accepts an empty patch (all fields undefined)', () => { 89 + expect(validateOrgUpdatePatch({})).toEqual({ ok: true }); 90 + }); 91 + 92 + test('accepts explicit-null fields (skip / clear semantics)', () => { 93 + expect( 94 + validateOrgUpdatePatch({ 95 + name: null, 96 + email: null, 97 + websiteUrl: null, 98 + onCallAlertEmail: null, 99 + }), 100 + ).toEqual({ ok: true }); 101 + }); 102 + 103 + test('accepts empty-string websiteUrl (Sequelize parity: treated as skip)', () => { 104 + expect(validateOrgUpdatePatch({ websiteUrl: '' })).toEqual({ ok: true }); 105 + }); 106 + 107 + test('rejects empty / whitespace name', () => { 108 + expect(validateOrgUpdatePatch({ name: '' }).ok).toBe(false); 109 + expect(validateOrgUpdatePatch({ name: ' ' }).ok).toBe(false); 110 + }); 111 + 112 + test('rejects malformed email', () => { 113 + const result = validateOrgUpdatePatch({ email: 'not-an-email' }); 114 + expect(result.ok).toBe(false); 115 + if (!result.ok) { 116 + expect(result.failure.field).toBe('email'); 117 + } 118 + }); 119 + 120 + test('rejects malformed websiteUrl', () => { 121 + const result = validateOrgUpdatePatch({ 122 + // eslint-disable-next-line no-script-url 123 + websiteUrl: 'javascript:alert(1)', 124 + }); 125 + expect(result.ok).toBe(false); 126 + if (!result.ok) { 127 + expect(result.failure.field).toBe('websiteUrl'); 128 + } 129 + }); 130 + 131 + test('rejects malformed onCallAlertEmail (non-null string)', () => { 132 + const result = validateOrgUpdatePatch({ onCallAlertEmail: 'nope' }); 133 + expect(result.ok).toBe(false); 134 + if (!result.ok) { 135 + expect(result.failure.field).toBe('onCallAlertEmail'); 136 + } 137 + }); 138 + }); 139 + });
+127
server/graphql/datasources/orgValidation.ts
··· 1 + import { createRequire } from 'node:module'; 2 + import type { IsEmailOptions } from 'validator/lib/isEmail.js'; 3 + 4 + import { validateUrl } from '../../utils/url.js'; 5 + 6 + // `validator` is CJS with UMD-style types whose `default` doesn't resolve to 7 + // a callable under `module: NodeNext`; `createRequire` gives us `module.exports` 8 + // directly, typed against the per-function defs that ship with `@types/validator`. 9 + type ValidatorLib = { 10 + isEmail: (str: string, options?: IsEmailOptions) => boolean; 11 + }; 12 + const validator = createRequire(import.meta.url)('validator') as ValidatorLib; 13 + 14 + /** 15 + * Server-side validation for `Org` inputs, replacing the `isEmail`, `notEmpty`, 16 + * and `validateUrl` checks that lived on the Sequelize model. 17 + * 18 + * Returned rather than thrown so each caller picks the right error surface: 19 + * the data source wraps failures in `makeBadRequestError` (with a JSON pointer 20 + * to the offending field), while persistence treats them as invariants. 21 + * 22 + * A follow-up move of these checks to GraphQL schema-level scalars 23 + * (`EmailAddress`, `URL` from `graphql-scalars`) is tracked in the Sequelize 24 + * → Kysely migration plan. 25 + */ 26 + 27 + export type OrgValidationFailure = { 28 + /** Kept stable for GraphQL JSON pointers. */ 29 + field: 'name' | 'email' | 'websiteUrl' | 'onCallAlertEmail'; 30 + message: string; 31 + }; 32 + 33 + export type OrgValidationResult = 34 + | { ok: true } 35 + | { ok: false; failure: OrgValidationFailure }; 36 + 37 + function isEmailShape(value: string): boolean { 38 + return validator.isEmail(value); 39 + } 40 + 41 + function isNonEmptyTrimmed(value: string): boolean { 42 + return value.trim().length > 0; 43 + } 44 + 45 + function fail( 46 + field: OrgValidationFailure['field'], 47 + message: string, 48 + ): OrgValidationResult { 49 + return { ok: false, failure: { field, message } }; 50 + } 51 + 52 + function validateWebsiteUrlShape(value: string): OrgValidationResult { 53 + try { 54 + validateUrl(value); 55 + return { ok: true }; 56 + } catch { 57 + return fail('websiteUrl', 'websiteUrl must be a valid http(s) URL'); 58 + } 59 + } 60 + 61 + export function validateOrgCreateInput(input: { 62 + name: string; 63 + email: string; 64 + websiteUrl: string; 65 + onCallAlertEmail?: string | null; 66 + }): OrgValidationResult { 67 + if (!isNonEmptyTrimmed(input.name)) { 68 + return fail('name', 'name must not be empty'); 69 + } 70 + if (!isNonEmptyTrimmed(input.email) || !isEmailShape(input.email)) { 71 + return fail('email', 'email must be a valid email address'); 72 + } 73 + if (!isNonEmptyTrimmed(input.websiteUrl)) { 74 + return fail('websiteUrl', 'websiteUrl must not be empty'); 75 + } 76 + const websiteResult = validateWebsiteUrlShape(input.websiteUrl); 77 + if (!websiteResult.ok) { 78 + return websiteResult; 79 + } 80 + if ( 81 + input.onCallAlertEmail != null && 82 + input.onCallAlertEmail !== '' && 83 + !isEmailShape(input.onCallAlertEmail) 84 + ) { 85 + return fail( 86 + 'onCallAlertEmail', 87 + 'onCallAlertEmail must be a valid email address', 88 + ); 89 + } 90 + return { ok: true }; 91 + } 92 + 93 + /** 94 + * Partial-update semantics match the Sequelize model: 95 + * - `undefined` fields are skipped 96 + * - `websiteUrl: ''` is treated as "no change" (legacy behavior) 97 + * - `onCallAlertEmail: null` is a meaningful value (clears the column) 98 + */ 99 + export function validateOrgUpdatePatch(patch: { 100 + name?: string | null; 101 + email?: string | null; 102 + websiteUrl?: string | null; 103 + onCallAlertEmail?: string | null; 104 + }): OrgValidationResult { 105 + if (patch.name != null && !isNonEmptyTrimmed(patch.name)) { 106 + return fail('name', 'name must not be empty'); 107 + } 108 + if ( 109 + patch.email != null && 110 + (!isNonEmptyTrimmed(patch.email) || !isEmailShape(patch.email)) 111 + ) { 112 + return fail('email', 'email must be a valid email address'); 113 + } 114 + if (patch.websiteUrl != null && patch.websiteUrl !== '') { 115 + const websiteResult = validateWebsiteUrlShape(patch.websiteUrl); 116 + if (!websiteResult.ok) { 117 + return websiteResult; 118 + } 119 + } 120 + if (patch.onCallAlertEmail != null && !isEmailShape(patch.onCallAlertEmail)) { 121 + return fail( 122 + 'onCallAlertEmail', 123 + 'onCallAlertEmail must be a valid email address', 124 + ); 125 + } 126 + return { ok: true }; 127 + }
+5 -5
server/graphql/generated.ts
··· 7 7 import { JsonObject, JsonValue } from 'type-fest'; 8 8 9 9 import type { UserHistoryForGQL } from '../graphql/datasources/InvestigationApi.js'; 10 + import type { GraphQLOrgParent } from '../graphql/datasources/orgKyselyPersistence.js'; 10 11 import type { 11 12 ContentItemTypeResolversParentType, 12 13 ItemTypeResolversParentType, ··· 17 18 } from '../graphql/modules/itemType.js'; 18 19 import type { ReportingInsights } from '../graphql/modules/reporting.js'; 19 20 import type { HashBank } from '../models/HashBankModel.js'; 20 - import type { Org } from '../models/OrgModel.js'; 21 21 import type { Backtest } from '../models/rules/BacktestModel.js'; 22 22 import type { ItemType } from '../models/rules/ItemTypeModel.js'; 23 23 import type { ··· 5795 5795 ManualReviewQueue: ResolverTypeWrapper<ManualReviewQueue>; 5796 5796 ManualReviewQueueNameExistsError: ResolverTypeWrapper<GQLManualReviewQueueNameExistsError>; 5797 5797 MatchingBankNameExistsError: ResolverTypeWrapper<GQLMatchingBankNameExistsError>; 5798 - MatchingBanks: ResolverTypeWrapper<Org>; 5798 + MatchingBanks: ResolverTypeWrapper<GraphQLOrgParent>; 5799 5799 MatchingValues: ResolverTypeWrapper<GQLMatchingValues>; 5800 5800 MessageWithIpAddress: ResolverTypeWrapper< 5801 5801 Omit<GQLMessageWithIpAddress, 'message'> & { ··· 5922 5922 NotificationType: GQLNotificationType; 5923 5923 OpenAiIntegrationApiCredential: ResolverTypeWrapper<GQLOpenAiIntegrationApiCredential>; 5924 5924 OpenAiIntegrationApiCredentialInput: GQLOpenAiIntegrationApiCredentialInput; 5925 - Org: ResolverTypeWrapper<Org>; 5925 + Org: ResolverTypeWrapper<GraphQLOrgParent>; 5926 5926 OrgWithEmailExistsError: ResolverTypeWrapper<GQLOrgWithEmailExistsError>; 5927 5927 OrgWithNameExistsError: ResolverTypeWrapper<GQLOrgWithNameExistsError>; 5928 5928 PageInfo: ResolverTypeWrapper<GQLPageInfo>; ··· 6530 6530 ManualReviewQueue: ManualReviewQueue; 6531 6531 ManualReviewQueueNameExistsError: GQLManualReviewQueueNameExistsError; 6532 6532 MatchingBankNameExistsError: GQLMatchingBankNameExistsError; 6533 - MatchingBanks: Org; 6533 + MatchingBanks: GraphQLOrgParent; 6534 6534 MatchingValues: GQLMatchingValues; 6535 6535 MessageWithIpAddress: Omit<GQLMessageWithIpAddress, 'message'> & { 6536 6536 message: GQLResolversParentTypes['ContentItem']; ··· 6619 6619 Notification: Notification; 6620 6620 OpenAiIntegrationApiCredential: GQLOpenAiIntegrationApiCredential; 6621 6621 OpenAiIntegrationApiCredentialInput: GQLOpenAiIntegrationApiCredentialInput; 6622 - Org: Org; 6622 + Org: GraphQLOrgParent; 6623 6623 OrgWithEmailExistsError: GQLOrgWithEmailExistsError; 6624 6624 OrgWithNameExistsError: GQLOrgWithNameExistsError; 6625 6625 PageInfo: GQLPageInfo;
+16 -20
server/graphql/modules/manualReviewTool.ts
··· 1102 1102 case 'REPORT': 1103 1103 case 'POST_ACTIONS': 1104 1104 return { kind: enqueueSourceInfo.kind }; 1105 - case 'RULE_EXECUTION': 1106 - const org = await context.dataSources.orgAPI.getGraphQLOrgFromId( 1107 - user.orgId, 1108 - ); 1109 - const rules = await org.getRules(); 1105 + case 'RULE_EXECUTION': { 1106 + const rules = 1107 + await context.dataSources.ruleAPI.getGraphQLRulesForOrg(user.orgId); 1110 1108 return { 1111 1109 kind: enqueueSourceInfo.kind, 1112 1110 rules: rules.filter((rule) => 1113 1111 enqueueSourceInfo.rules.includes(rule.id), 1114 1112 ), 1115 1113 }; 1114 + } 1116 1115 default: 1117 1116 assertUnreachable(enqueueSourceInfo); 1118 1117 } ··· 1318 1317 case 'REPORT': 1319 1318 case 'POST_ACTIONS': 1320 1319 return { kind: enqueueSourceInfo.kind }; 1321 - case 'RULE_EXECUTION': 1322 - const org = await context.dataSources.orgAPI.getGraphQLOrgFromId( 1323 - user.orgId, 1324 - ); 1325 - const rules = await org.getRules(); 1320 + case 'RULE_EXECUTION': { 1321 + const rules = 1322 + await context.dataSources.ruleAPI.getGraphQLRulesForOrg(user.orgId); 1326 1323 return { 1327 1324 kind: enqueueSourceInfo.kind, 1328 1325 rules: rules.filter((rule) => 1329 1326 enqueueSourceInfo.rules.includes(rule.id), 1330 1327 ), 1331 1328 }; 1329 + } 1332 1330 default: 1333 1331 assertUnreachable(enqueueSourceInfo); 1334 1332 } ··· 1487 1485 case 'REPORT': 1488 1486 case 'POST_ACTIONS': 1489 1487 return { kind: enqueueSourceInfo.kind }; 1490 - case 'RULE_EXECUTION': 1491 - const org = await context.dataSources.orgAPI.getGraphQLOrgFromId( 1492 - user.orgId, 1493 - ); 1494 - const rules = await org.getRules(); 1488 + case 'RULE_EXECUTION': { 1489 + const rules = 1490 + await context.dataSources.ruleAPI.getGraphQLRulesForOrg(user.orgId); 1495 1491 return { 1496 1492 kind: enqueueSourceInfo.kind, 1497 1493 rules: rules.filter((rule) => 1498 1494 enqueueSourceInfo.rules.includes(rule.id), 1499 1495 ), 1500 1496 }; 1497 + } 1501 1498 default: 1502 1499 assertUnreachable(enqueueSourceInfo); 1503 1500 } ··· 1613 1610 case 'REPORT': 1614 1611 case 'POST_ACTIONS': 1615 1612 return { kind: enqueueSourceInfo.kind }; 1616 - case 'RULE_EXECUTION': 1617 - const org = await context.dataSources.orgAPI.getGraphQLOrgFromId( 1618 - user.orgId, 1619 - ); 1620 - const rules = await org.getRules(); 1613 + case 'RULE_EXECUTION': { 1614 + const rules = 1615 + await context.dataSources.ruleAPI.getGraphQLRulesForOrg(user.orgId); 1621 1616 return { 1622 1617 kind: enqueueSourceInfo.kind, 1623 1618 rules: rules.filter((rule) => 1624 1619 enqueueSourceInfo.rules.includes(rule.id), 1625 1620 ), 1626 1621 }; 1622 + } 1627 1623 default: 1628 1624 assertUnreachable(enqueueSourceInfo); 1629 1625 }
+7 -7
server/graphql/modules/org.ts
··· 205 205 if (!user || user.orgId !== org.id) { 206 206 throw unauthenticatedError('User required.'); 207 207 } 208 - 209 208 return context.services.ModerationConfigService.getActions({ 210 209 orgId: org.id, 211 210 readFromReplica: true, ··· 216 215 if (!user || user.orgId !== org.id) { 217 216 throw unauthenticatedError('User required.'); 218 217 } 219 - return org.getContentTypes(); 218 + return context.dataSources.orgAPI.getSequelizeContentTypesForOrg(org.id); 220 219 }, 221 220 async itemTypes(org, _, context) { 222 221 const user = context.getUser(); ··· 232 231 if (!user || user.orgId !== org.id) { 233 232 throw unauthenticatedError('User required.'); 234 233 } 235 - return org.getUsers(); 234 + return context.dataSources.orgAPI.getOrgUsersForGraphQL(org.id); 236 235 }, 237 236 async pendingInvites(org, _, context): Promise<GQLPendingInvite[]> { 238 237 const user = context.getUser(); ··· 253 252 if (!user || user.orgId !== org.id) { 254 253 throw unauthenticatedError('User required.'); 255 254 } 256 - return org.getRules(); 255 + return context.dataSources.ruleAPI.getGraphQLRulesForOrg(org.id); 257 256 }, 258 257 async routingRules(org, _, context) { 259 258 const user = context.getUser(); ··· 466 465 org.id, 467 466 ); 468 467 }, 469 - async usersWhoCanReviewEveryQueue(org, _, __) { 470 - return (await org.getUsers()).filter((user) => 471 - user.getPermissions().includes('EDIT_MRT_QUEUES'), 468 + async usersWhoCanReviewEveryQueue(org, _, context) { 469 + const users = await context.dataSources.orgAPI.getOrgUsersForGraphQL( 470 + org.id, 472 471 ); 472 + return users.filter((u) => u.getPermissions().includes('EDIT_MRT_QUEUES')); 473 473 }, 474 474 async defaultInterfacePreferences(org, _, context) { 475 475 const orgDefaults =
+426 -145
server/package-lock.json
··· 94 94 "unhomoglyph": "^1.0.6", 95 95 "uuid": "^8.3.2", 96 96 "uuid-apikey": "^1.5.3", 97 + "validator": "^13.15.35", 97 98 "xml-js": "^1.6.11", 98 99 "yargs": "^17.7.2" 99 100 }, ··· 133 134 "tsc-watch": "^4.6.0", 134 135 "typescript": "^5.5.2", 135 136 "typescript-eslint": "^8.57.2" 136 - } 137 - }, 138 - "node_modules/@aashutoshrathi/word-wrap": { 139 - "version": "1.2.6", 140 - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", 141 - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", 142 - "engines": { 143 - "node": ">=0.10.0" 144 137 } 145 138 }, 146 139 "node_modules/@apollo/cache-control-types": { ··· 2556 2549 "license": "Apache-2.0" 2557 2550 }, 2558 2551 "node_modules/@humanfs/core": { 2559 - "version": "0.19.1", 2560 - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", 2561 - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", 2552 + "version": "0.19.2", 2553 + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", 2554 + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", 2562 2555 "license": "Apache-2.0", 2556 + "dependencies": { 2557 + "@humanfs/types": "^0.15.0" 2558 + }, 2563 2559 "engines": { 2564 2560 "node": ">=18.18.0" 2565 2561 } 2566 2562 }, 2567 2563 "node_modules/@humanfs/node": { 2568 - "version": "0.16.7", 2569 - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", 2570 - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", 2564 + "version": "0.16.8", 2565 + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", 2566 + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", 2571 2567 "license": "Apache-2.0", 2572 2568 "dependencies": { 2573 - "@humanfs/core": "^0.19.1", 2569 + "@humanfs/core": "^0.19.2", 2570 + "@humanfs/types": "^0.15.0", 2574 2571 "@humanwhocodes/retry": "^0.4.0" 2575 2572 }, 2576 2573 "engines": { 2577 2574 "node": ">=18.18.0" 2578 2575 } 2579 2576 }, 2577 + "node_modules/@humanfs/types": { 2578 + "version": "0.15.0", 2579 + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", 2580 + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", 2581 + "license": "Apache-2.0", 2582 + "engines": { 2583 + "node": ">=18.18.0" 2584 + } 2585 + }, 2580 2586 "node_modules/@humanwhocodes/module-importer": { 2581 2587 "version": "1.0.1", 2582 2588 "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", 2583 2589 "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", 2590 + "license": "Apache-2.0", 2584 2591 "engines": { 2585 2592 "node": ">=12.22" 2586 2593 }, ··· 11068 11075 "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==" 11069 11076 }, 11070 11077 "node_modules/@typescript-eslint/eslint-plugin": { 11071 - "version": "8.57.2", 11072 - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz", 11073 - "integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==", 11078 + "version": "8.59.0", 11079 + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.0.tgz", 11080 + "integrity": "sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw==", 11074 11081 "dev": true, 11075 11082 "license": "MIT", 11076 11083 "dependencies": { 11077 11084 "@eslint-community/regexpp": "^4.12.2", 11078 - "@typescript-eslint/scope-manager": "8.57.2", 11079 - "@typescript-eslint/type-utils": "8.57.2", 11080 - "@typescript-eslint/utils": "8.57.2", 11081 - "@typescript-eslint/visitor-keys": "8.57.2", 11085 + "@typescript-eslint/scope-manager": "8.59.0", 11086 + "@typescript-eslint/type-utils": "8.59.0", 11087 + "@typescript-eslint/utils": "8.59.0", 11088 + "@typescript-eslint/visitor-keys": "8.59.0", 11082 11089 "ignore": "^7.0.5", 11083 11090 "natural-compare": "^1.4.0", 11084 - "ts-api-utils": "^2.4.0" 11091 + "ts-api-utils": "^2.5.0" 11085 11092 }, 11086 11093 "engines": { 11087 11094 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" ··· 11091 11098 "url": "https://opencollective.com/typescript-eslint" 11092 11099 }, 11093 11100 "peerDependencies": { 11094 - "@typescript-eslint/parser": "^8.57.2", 11101 + "@typescript-eslint/parser": "^8.59.0", 11095 11102 "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", 11096 - "typescript": ">=4.8.4 <6.0.0" 11103 + "typescript": ">=4.8.4 <6.1.0" 11097 11104 } 11098 11105 }, 11099 11106 "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { 11100 - "version": "8.57.2", 11101 - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz", 11102 - "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==", 11107 + "version": "8.59.0", 11108 + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.0.tgz", 11109 + "integrity": "sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==", 11103 11110 "dev": true, 11104 11111 "license": "MIT", 11105 11112 "dependencies": { 11106 - "@typescript-eslint/types": "8.57.2", 11107 - "@typescript-eslint/visitor-keys": "8.57.2" 11113 + "@typescript-eslint/types": "8.59.0", 11114 + "@typescript-eslint/visitor-keys": "8.59.0" 11108 11115 }, 11109 11116 "engines": { 11110 11117 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" ··· 11115 11122 } 11116 11123 }, 11117 11124 "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { 11118 - "version": "8.57.2", 11119 - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", 11120 - "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", 11125 + "version": "8.59.0", 11126 + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.0.tgz", 11127 + "integrity": "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==", 11121 11128 "dev": true, 11122 11129 "license": "MIT", 11123 11130 "engines": { ··· 11129 11136 } 11130 11137 }, 11131 11138 "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { 11132 - "version": "8.57.2", 11133 - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz", 11134 - "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==", 11139 + "version": "8.59.0", 11140 + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.0.tgz", 11141 + "integrity": "sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==", 11135 11142 "dev": true, 11136 11143 "license": "MIT", 11137 11144 "dependencies": { 11138 - "@typescript-eslint/types": "8.57.2", 11145 + "@typescript-eslint/types": "8.59.0", 11139 11146 "eslint-visitor-keys": "^5.0.0" 11140 11147 }, 11141 11148 "engines": { ··· 11183 11190 } 11184 11191 }, 11185 11192 "node_modules/@typescript-eslint/parser": { 11186 - "version": "8.57.2", 11187 - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz", 11188 - "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", 11193 + "version": "8.59.0", 11194 + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.0.tgz", 11195 + "integrity": "sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==", 11189 11196 "dev": true, 11190 11197 "license": "MIT", 11191 11198 "dependencies": { 11192 - "@typescript-eslint/scope-manager": "8.57.2", 11193 - "@typescript-eslint/types": "8.57.2", 11194 - "@typescript-eslint/typescript-estree": "8.57.2", 11195 - "@typescript-eslint/visitor-keys": "8.57.2", 11199 + "@typescript-eslint/scope-manager": "8.59.0", 11200 + "@typescript-eslint/types": "8.59.0", 11201 + "@typescript-eslint/typescript-estree": "8.59.0", 11202 + "@typescript-eslint/visitor-keys": "8.59.0", 11196 11203 "debug": "^4.4.3" 11197 11204 }, 11198 11205 "engines": { ··· 11204 11211 }, 11205 11212 "peerDependencies": { 11206 11213 "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", 11207 - "typescript": ">=4.8.4 <6.0.0" 11214 + "typescript": ">=4.8.4 <6.1.0" 11215 + } 11216 + }, 11217 + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/project-service": { 11218 + "version": "8.59.0", 11219 + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.0.tgz", 11220 + "integrity": "sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==", 11221 + "dev": true, 11222 + "license": "MIT", 11223 + "dependencies": { 11224 + "@typescript-eslint/tsconfig-utils": "^8.59.0", 11225 + "@typescript-eslint/types": "^8.59.0", 11226 + "debug": "^4.4.3" 11227 + }, 11228 + "engines": { 11229 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 11230 + }, 11231 + "funding": { 11232 + "type": "opencollective", 11233 + "url": "https://opencollective.com/typescript-eslint" 11234 + }, 11235 + "peerDependencies": { 11236 + "typescript": ">=4.8.4 <6.1.0" 11208 11237 } 11209 11238 }, 11210 11239 "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { 11211 - "version": "8.57.2", 11212 - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz", 11213 - "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==", 11240 + "version": "8.59.0", 11241 + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.0.tgz", 11242 + "integrity": "sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==", 11214 11243 "dev": true, 11215 11244 "license": "MIT", 11216 11245 "dependencies": { 11217 - "@typescript-eslint/types": "8.57.2", 11218 - "@typescript-eslint/visitor-keys": "8.57.2" 11246 + "@typescript-eslint/types": "8.59.0", 11247 + "@typescript-eslint/visitor-keys": "8.59.0" 11219 11248 }, 11220 11249 "engines": { 11221 11250 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" ··· 11225 11254 "url": "https://opencollective.com/typescript-eslint" 11226 11255 } 11227 11256 }, 11257 + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/tsconfig-utils": { 11258 + "version": "8.59.0", 11259 + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.0.tgz", 11260 + "integrity": "sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==", 11261 + "dev": true, 11262 + "license": "MIT", 11263 + "engines": { 11264 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 11265 + }, 11266 + "funding": { 11267 + "type": "opencollective", 11268 + "url": "https://opencollective.com/typescript-eslint" 11269 + }, 11270 + "peerDependencies": { 11271 + "typescript": ">=4.8.4 <6.1.0" 11272 + } 11273 + }, 11228 11274 "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { 11229 - "version": "8.57.2", 11230 - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", 11231 - "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", 11275 + "version": "8.59.0", 11276 + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.0.tgz", 11277 + "integrity": "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==", 11232 11278 "dev": true, 11233 11279 "license": "MIT", 11234 11280 "engines": { ··· 11240 11286 } 11241 11287 }, 11242 11288 "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { 11243 - "version": "8.57.2", 11244 - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", 11245 - "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", 11289 + "version": "8.59.0", 11290 + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.0.tgz", 11291 + "integrity": "sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==", 11246 11292 "dev": true, 11247 11293 "license": "MIT", 11248 11294 "dependencies": { 11249 - "@typescript-eslint/project-service": "8.57.2", 11250 - "@typescript-eslint/tsconfig-utils": "8.57.2", 11251 - "@typescript-eslint/types": "8.57.2", 11252 - "@typescript-eslint/visitor-keys": "8.57.2", 11295 + "@typescript-eslint/project-service": "8.59.0", 11296 + "@typescript-eslint/tsconfig-utils": "8.59.0", 11297 + "@typescript-eslint/types": "8.59.0", 11298 + "@typescript-eslint/visitor-keys": "8.59.0", 11253 11299 "debug": "^4.4.3", 11254 11300 "minimatch": "^10.2.2", 11255 11301 "semver": "^7.7.3", 11256 11302 "tinyglobby": "^0.2.15", 11257 - "ts-api-utils": "^2.4.0" 11303 + "ts-api-utils": "^2.5.0" 11258 11304 }, 11259 11305 "engines": { 11260 11306 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" ··· 11264 11310 "url": "https://opencollective.com/typescript-eslint" 11265 11311 }, 11266 11312 "peerDependencies": { 11267 - "typescript": ">=4.8.4 <6.0.0" 11313 + "typescript": ">=4.8.4 <6.1.0" 11268 11314 } 11269 11315 }, 11270 11316 "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { 11271 - "version": "8.57.2", 11272 - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz", 11273 - "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==", 11317 + "version": "8.59.0", 11318 + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.0.tgz", 11319 + "integrity": "sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==", 11274 11320 "dev": true, 11275 11321 "license": "MIT", 11276 11322 "dependencies": { 11277 - "@typescript-eslint/types": "8.57.2", 11323 + "@typescript-eslint/types": "8.59.0", 11278 11324 "eslint-visitor-keys": "^5.0.0" 11279 11325 }, 11280 11326 "engines": { ··· 11322 11368 } 11323 11369 }, 11324 11370 "node_modules/@typescript-eslint/parser/node_modules/minimatch": { 11325 - "version": "10.2.4", 11326 - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", 11327 - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", 11371 + "version": "10.2.5", 11372 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", 11373 + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", 11328 11374 "dev": true, 11329 11375 "license": "BlueOak-1.0.0", 11330 11376 "dependencies": { 11331 - "brace-expansion": "^5.0.2" 11377 + "brace-expansion": "^5.0.5" 11332 11378 }, 11333 11379 "engines": { 11334 11380 "node": "18 || 20 || >=22" ··· 11421 11467 } 11422 11468 }, 11423 11469 "node_modules/@typescript-eslint/type-utils": { 11424 - "version": "8.57.2", 11425 - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz", 11426 - "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==", 11470 + "version": "8.59.0", 11471 + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.0.tgz", 11472 + "integrity": "sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg==", 11427 11473 "dev": true, 11428 11474 "license": "MIT", 11429 11475 "dependencies": { 11430 - "@typescript-eslint/types": "8.57.2", 11431 - "@typescript-eslint/typescript-estree": "8.57.2", 11432 - "@typescript-eslint/utils": "8.57.2", 11476 + "@typescript-eslint/types": "8.59.0", 11477 + "@typescript-eslint/typescript-estree": "8.59.0", 11478 + "@typescript-eslint/utils": "8.59.0", 11433 11479 "debug": "^4.4.3", 11434 - "ts-api-utils": "^2.4.0" 11480 + "ts-api-utils": "^2.5.0" 11435 11481 }, 11436 11482 "engines": { 11437 11483 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" ··· 11442 11488 }, 11443 11489 "peerDependencies": { 11444 11490 "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", 11445 - "typescript": ">=4.8.4 <6.0.0" 11491 + "typescript": ">=4.8.4 <6.1.0" 11492 + } 11493 + }, 11494 + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/project-service": { 11495 + "version": "8.59.0", 11496 + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.0.tgz", 11497 + "integrity": "sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==", 11498 + "dev": true, 11499 + "license": "MIT", 11500 + "dependencies": { 11501 + "@typescript-eslint/tsconfig-utils": "^8.59.0", 11502 + "@typescript-eslint/types": "^8.59.0", 11503 + "debug": "^4.4.3" 11504 + }, 11505 + "engines": { 11506 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 11507 + }, 11508 + "funding": { 11509 + "type": "opencollective", 11510 + "url": "https://opencollective.com/typescript-eslint" 11511 + }, 11512 + "peerDependencies": { 11513 + "typescript": ">=4.8.4 <6.1.0" 11514 + } 11515 + }, 11516 + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/tsconfig-utils": { 11517 + "version": "8.59.0", 11518 + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.0.tgz", 11519 + "integrity": "sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==", 11520 + "dev": true, 11521 + "license": "MIT", 11522 + "engines": { 11523 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 11524 + }, 11525 + "funding": { 11526 + "type": "opencollective", 11527 + "url": "https://opencollective.com/typescript-eslint" 11528 + }, 11529 + "peerDependencies": { 11530 + "typescript": ">=4.8.4 <6.1.0" 11446 11531 } 11447 11532 }, 11448 11533 "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { 11449 - "version": "8.57.2", 11450 - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", 11451 - "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", 11534 + "version": "8.59.0", 11535 + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.0.tgz", 11536 + "integrity": "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==", 11452 11537 "dev": true, 11453 11538 "license": "MIT", 11454 11539 "engines": { ··· 11460 11545 } 11461 11546 }, 11462 11547 "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { 11463 - "version": "8.57.2", 11464 - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", 11465 - "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", 11548 + "version": "8.59.0", 11549 + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.0.tgz", 11550 + "integrity": "sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==", 11466 11551 "dev": true, 11467 11552 "license": "MIT", 11468 11553 "dependencies": { 11469 - "@typescript-eslint/project-service": "8.57.2", 11470 - "@typescript-eslint/tsconfig-utils": "8.57.2", 11471 - "@typescript-eslint/types": "8.57.2", 11472 - "@typescript-eslint/visitor-keys": "8.57.2", 11554 + "@typescript-eslint/project-service": "8.59.0", 11555 + "@typescript-eslint/tsconfig-utils": "8.59.0", 11556 + "@typescript-eslint/types": "8.59.0", 11557 + "@typescript-eslint/visitor-keys": "8.59.0", 11473 11558 "debug": "^4.4.3", 11474 11559 "minimatch": "^10.2.2", 11475 11560 "semver": "^7.7.3", 11476 11561 "tinyglobby": "^0.2.15", 11477 - "ts-api-utils": "^2.4.0" 11562 + "ts-api-utils": "^2.5.0" 11478 11563 }, 11479 11564 "engines": { 11480 11565 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" ··· 11484 11569 "url": "https://opencollective.com/typescript-eslint" 11485 11570 }, 11486 11571 "peerDependencies": { 11487 - "typescript": ">=4.8.4 <6.0.0" 11572 + "typescript": ">=4.8.4 <6.1.0" 11488 11573 } 11489 11574 }, 11490 11575 "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { 11491 - "version": "8.57.2", 11492 - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz", 11493 - "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==", 11576 + "version": "8.59.0", 11577 + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.0.tgz", 11578 + "integrity": "sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==", 11494 11579 "dev": true, 11495 11580 "license": "MIT", 11496 11581 "dependencies": { 11497 - "@typescript-eslint/types": "8.57.2", 11582 + "@typescript-eslint/types": "8.59.0", 11498 11583 "eslint-visitor-keys": "^5.0.0" 11499 11584 }, 11500 11585 "engines": { ··· 11542 11627 } 11543 11628 }, 11544 11629 "node_modules/@typescript-eslint/type-utils/node_modules/minimatch": { 11545 - "version": "10.2.4", 11546 - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", 11547 - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", 11630 + "version": "10.2.5", 11631 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", 11632 + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", 11548 11633 "dev": true, 11549 11634 "license": "BlueOak-1.0.0", 11550 11635 "dependencies": { 11551 - "brace-expansion": "^5.0.2" 11636 + "brace-expansion": "^5.0.5" 11552 11637 }, 11553 11638 "engines": { 11554 11639 "node": "18 || 20 || >=22" ··· 11636 11721 } 11637 11722 }, 11638 11723 "node_modules/@typescript-eslint/utils": { 11639 - "version": "8.57.2", 11640 - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz", 11641 - "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==", 11724 + "version": "8.59.0", 11725 + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.0.tgz", 11726 + "integrity": "sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==", 11642 11727 "dev": true, 11643 11728 "license": "MIT", 11644 11729 "dependencies": { 11645 11730 "@eslint-community/eslint-utils": "^4.9.1", 11646 - "@typescript-eslint/scope-manager": "8.57.2", 11647 - "@typescript-eslint/types": "8.57.2", 11648 - "@typescript-eslint/typescript-estree": "8.57.2" 11731 + "@typescript-eslint/scope-manager": "8.59.0", 11732 + "@typescript-eslint/types": "8.59.0", 11733 + "@typescript-eslint/typescript-estree": "8.59.0" 11649 11734 }, 11650 11735 "engines": { 11651 11736 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" ··· 11656 11741 }, 11657 11742 "peerDependencies": { 11658 11743 "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", 11659 - "typescript": ">=4.8.4 <6.0.0" 11744 + "typescript": ">=4.8.4 <6.1.0" 11745 + } 11746 + }, 11747 + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/project-service": { 11748 + "version": "8.59.0", 11749 + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.0.tgz", 11750 + "integrity": "sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==", 11751 + "dev": true, 11752 + "license": "MIT", 11753 + "dependencies": { 11754 + "@typescript-eslint/tsconfig-utils": "^8.59.0", 11755 + "@typescript-eslint/types": "^8.59.0", 11756 + "debug": "^4.4.3" 11757 + }, 11758 + "engines": { 11759 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 11760 + }, 11761 + "funding": { 11762 + "type": "opencollective", 11763 + "url": "https://opencollective.com/typescript-eslint" 11764 + }, 11765 + "peerDependencies": { 11766 + "typescript": ">=4.8.4 <6.1.0" 11660 11767 } 11661 11768 }, 11662 11769 "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": { 11663 - "version": "8.57.2", 11664 - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz", 11665 - "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==", 11770 + "version": "8.59.0", 11771 + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.0.tgz", 11772 + "integrity": "sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==", 11666 11773 "dev": true, 11667 11774 "license": "MIT", 11668 11775 "dependencies": { 11669 - "@typescript-eslint/types": "8.57.2", 11670 - "@typescript-eslint/visitor-keys": "8.57.2" 11776 + "@typescript-eslint/types": "8.59.0", 11777 + "@typescript-eslint/visitor-keys": "8.59.0" 11671 11778 }, 11672 11779 "engines": { 11673 11780 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" ··· 11677 11784 "url": "https://opencollective.com/typescript-eslint" 11678 11785 } 11679 11786 }, 11787 + "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/tsconfig-utils": { 11788 + "version": "8.59.0", 11789 + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.0.tgz", 11790 + "integrity": "sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==", 11791 + "dev": true, 11792 + "license": "MIT", 11793 + "engines": { 11794 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 11795 + }, 11796 + "funding": { 11797 + "type": "opencollective", 11798 + "url": "https://opencollective.com/typescript-eslint" 11799 + }, 11800 + "peerDependencies": { 11801 + "typescript": ">=4.8.4 <6.1.0" 11802 + } 11803 + }, 11680 11804 "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": { 11681 - "version": "8.57.2", 11682 - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", 11683 - "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", 11805 + "version": "8.59.0", 11806 + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.0.tgz", 11807 + "integrity": "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==", 11684 11808 "dev": true, 11685 11809 "license": "MIT", 11686 11810 "engines": { ··· 11692 11816 } 11693 11817 }, 11694 11818 "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { 11695 - "version": "8.57.2", 11696 - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", 11697 - "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", 11819 + "version": "8.59.0", 11820 + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.0.tgz", 11821 + "integrity": "sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==", 11698 11822 "dev": true, 11699 11823 "license": "MIT", 11700 11824 "dependencies": { 11701 - "@typescript-eslint/project-service": "8.57.2", 11702 - "@typescript-eslint/tsconfig-utils": "8.57.2", 11703 - "@typescript-eslint/types": "8.57.2", 11704 - "@typescript-eslint/visitor-keys": "8.57.2", 11825 + "@typescript-eslint/project-service": "8.59.0", 11826 + "@typescript-eslint/tsconfig-utils": "8.59.0", 11827 + "@typescript-eslint/types": "8.59.0", 11828 + "@typescript-eslint/visitor-keys": "8.59.0", 11705 11829 "debug": "^4.4.3", 11706 11830 "minimatch": "^10.2.2", 11707 11831 "semver": "^7.7.3", 11708 11832 "tinyglobby": "^0.2.15", 11709 - "ts-api-utils": "^2.4.0" 11833 + "ts-api-utils": "^2.5.0" 11710 11834 }, 11711 11835 "engines": { 11712 11836 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" ··· 11716 11840 "url": "https://opencollective.com/typescript-eslint" 11717 11841 }, 11718 11842 "peerDependencies": { 11719 - "typescript": ">=4.8.4 <6.0.0" 11843 + "typescript": ">=4.8.4 <6.1.0" 11720 11844 } 11721 11845 }, 11722 11846 "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { 11723 - "version": "8.57.2", 11724 - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz", 11725 - "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==", 11847 + "version": "8.59.0", 11848 + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.0.tgz", 11849 + "integrity": "sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==", 11726 11850 "dev": true, 11727 11851 "license": "MIT", 11728 11852 "dependencies": { 11729 - "@typescript-eslint/types": "8.57.2", 11853 + "@typescript-eslint/types": "8.59.0", 11730 11854 "eslint-visitor-keys": "^5.0.0" 11731 11855 }, 11732 11856 "engines": { ··· 11774 11898 } 11775 11899 }, 11776 11900 "node_modules/@typescript-eslint/utils/node_modules/minimatch": { 11777 - "version": "10.2.4", 11778 - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", 11779 - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", 11901 + "version": "10.2.5", 11902 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", 11903 + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", 11780 11904 "dev": true, 11781 11905 "license": "BlueOak-1.0.0", 11782 11906 "dependencies": { 11783 - "brace-expansion": "^5.0.2" 11907 + "brace-expansion": "^5.0.5" 11784 11908 }, 11785 11909 "engines": { 11786 11910 "node": "18 || 20 || >=22" ··· 13217 13341 "node_modules/deep-is": { 13218 13342 "version": "0.1.4", 13219 13343 "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", 13220 - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" 13344 + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", 13345 + "license": "MIT" 13221 13346 }, 13222 13347 "node_modules/deepmerge": { 13223 13348 "version": "4.3.1", ··· 13832 13957 "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", 13833 13958 "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", 13834 13959 "dev": true, 13960 + "license": "MIT", 13835 13961 "dependencies": { 13836 13962 "eslint-utils": "^2.0.0", 13837 13963 "regexpp": "^3.0.0" ··· 14184 14310 } 14185 14311 }, 14186 14312 "node_modules/eslint/node_modules/ajv": { 14187 - "version": "6.14.0", 14188 - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", 14189 - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", 14313 + "version": "6.15.0", 14314 + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", 14315 + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", 14190 14316 "license": "MIT", 14191 14317 "dependencies": { 14192 14318 "fast-deep-equal": "^3.1.1", ··· 14262 14388 "version": "4.3.0", 14263 14389 "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", 14264 14390 "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", 14391 + "license": "BSD-2-Clause", 14265 14392 "dependencies": { 14266 14393 "estraverse": "^5.2.0" 14267 14394 }, ··· 14571 14698 "node_modules/fast-levenshtein": { 14572 14699 "version": "2.0.6", 14573 14700 "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", 14574 - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" 14701 + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", 14702 + "license": "MIT" 14575 14703 }, 14576 14704 "node_modules/fast-safe-stringify": { 14577 14705 "version": "2.1.1", ··· 14744 14872 "version": "5.0.0", 14745 14873 "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", 14746 14874 "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", 14875 + "license": "MIT", 14747 14876 "dependencies": { 14748 14877 "locate-path": "^6.0.0", 14749 14878 "path-exists": "^4.0.0" ··· 15187 15316 "version": "6.0.2", 15188 15317 "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", 15189 15318 "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", 15319 + "license": "ISC", 15190 15320 "dependencies": { 15191 15321 "is-glob": "^4.0.3" 15192 15322 }, ··· 17422 17552 "node_modules/json-stable-stringify-without-jsonify": { 17423 17553 "version": "1.0.1", 17424 17554 "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", 17425 - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" 17555 + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", 17556 + "license": "MIT" 17426 17557 }, 17427 17558 "node_modules/json5": { 17428 17559 "version": "2.2.3", ··· 17545 17676 "version": "0.4.1", 17546 17677 "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", 17547 17678 "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", 17679 + "license": "MIT", 17548 17680 "dependencies": { 17549 17681 "prelude-ls": "^1.2.1", 17550 17682 "type-check": "~0.4.0" ··· 17564 17696 "version": "6.0.0", 17565 17697 "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", 17566 17698 "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", 17699 + "license": "MIT", 17567 17700 "dependencies": { 17568 17701 "p-locate": "^5.0.0" 17569 17702 }, ··· 17629 17762 "node_modules/lodash.merge": { 17630 17763 "version": "4.6.2", 17631 17764 "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", 17632 - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" 17765 + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", 17766 + "license": "MIT" 17633 17767 }, 17634 17768 "node_modules/lodash.once": { 17635 17769 "version": "4.1.1", ··· 18343 18477 } 18344 18478 }, 18345 18479 "node_modules/optionator": { 18346 - "version": "0.9.3", 18347 - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", 18348 - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", 18480 + "version": "0.9.4", 18481 + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", 18482 + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", 18483 + "license": "MIT", 18349 18484 "dependencies": { 18350 - "@aashutoshrathi/word-wrap": "^1.2.3", 18351 18485 "deep-is": "^0.1.3", 18352 18486 "fast-levenshtein": "^2.0.6", 18353 18487 "levn": "^0.4.1", 18354 18488 "prelude-ls": "^1.2.1", 18355 - "type-check": "^0.4.0" 18489 + "type-check": "^0.4.0", 18490 + "word-wrap": "^1.2.5" 18356 18491 }, 18357 18492 "engines": { 18358 18493 "node": ">= 0.8.0" ··· 18394 18529 "version": "5.0.0", 18395 18530 "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", 18396 18531 "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", 18532 + "license": "MIT", 18397 18533 "dependencies": { 18398 18534 "p-limit": "^3.0.2" 18399 18535 }, ··· 18408 18544 "version": "3.1.0", 18409 18545 "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", 18410 18546 "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", 18547 + "license": "MIT", 18411 18548 "dependencies": { 18412 18549 "yocto-queue": "^0.1.0" 18413 18550 }, ··· 18422 18559 "version": "0.1.0", 18423 18560 "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", 18424 18561 "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", 18562 + "license": "MIT", 18425 18563 "engines": { 18426 18564 "node": ">=10" 18427 18565 }, ··· 18912 19050 "version": "1.2.1", 18913 19051 "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", 18914 19052 "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", 19053 + "license": "MIT", 18915 19054 "engines": { 18916 19055 "node": ">= 0.8.0" 18917 19056 } ··· 19253 19392 "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", 19254 19393 "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", 19255 19394 "dev": true, 19395 + "license": "MIT", 19256 19396 "engines": { 19257 19397 "node": ">=8" 19258 19398 }, ··· 20838 20978 "version": "0.4.0", 20839 20979 "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", 20840 20980 "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", 20981 + "license": "MIT", 20841 20982 "dependencies": { 20842 20983 "prelude-ls": "^1.2.1" 20843 20984 }, ··· 21018 21159 "typescript": ">=4.8.4 <6.0.0" 21019 21160 } 21020 21161 }, 21162 + "node_modules/typescript-eslint/node_modules/@typescript-eslint/eslint-plugin": { 21163 + "version": "8.57.2", 21164 + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz", 21165 + "integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==", 21166 + "dev": true, 21167 + "license": "MIT", 21168 + "dependencies": { 21169 + "@eslint-community/regexpp": "^4.12.2", 21170 + "@typescript-eslint/scope-manager": "8.57.2", 21171 + "@typescript-eslint/type-utils": "8.57.2", 21172 + "@typescript-eslint/utils": "8.57.2", 21173 + "@typescript-eslint/visitor-keys": "8.57.2", 21174 + "ignore": "^7.0.5", 21175 + "natural-compare": "^1.4.0", 21176 + "ts-api-utils": "^2.4.0" 21177 + }, 21178 + "engines": { 21179 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 21180 + }, 21181 + "funding": { 21182 + "type": "opencollective", 21183 + "url": "https://opencollective.com/typescript-eslint" 21184 + }, 21185 + "peerDependencies": { 21186 + "@typescript-eslint/parser": "^8.57.2", 21187 + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", 21188 + "typescript": ">=4.8.4 <6.0.0" 21189 + } 21190 + }, 21191 + "node_modules/typescript-eslint/node_modules/@typescript-eslint/parser": { 21192 + "version": "8.57.2", 21193 + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz", 21194 + "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", 21195 + "dev": true, 21196 + "license": "MIT", 21197 + "dependencies": { 21198 + "@typescript-eslint/scope-manager": "8.57.2", 21199 + "@typescript-eslint/types": "8.57.2", 21200 + "@typescript-eslint/typescript-estree": "8.57.2", 21201 + "@typescript-eslint/visitor-keys": "8.57.2", 21202 + "debug": "^4.4.3" 21203 + }, 21204 + "engines": { 21205 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 21206 + }, 21207 + "funding": { 21208 + "type": "opencollective", 21209 + "url": "https://opencollective.com/typescript-eslint" 21210 + }, 21211 + "peerDependencies": { 21212 + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", 21213 + "typescript": ">=4.8.4 <6.0.0" 21214 + } 21215 + }, 21216 + "node_modules/typescript-eslint/node_modules/@typescript-eslint/scope-manager": { 21217 + "version": "8.57.2", 21218 + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz", 21219 + "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==", 21220 + "dev": true, 21221 + "license": "MIT", 21222 + "dependencies": { 21223 + "@typescript-eslint/types": "8.57.2", 21224 + "@typescript-eslint/visitor-keys": "8.57.2" 21225 + }, 21226 + "engines": { 21227 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 21228 + }, 21229 + "funding": { 21230 + "type": "opencollective", 21231 + "url": "https://opencollective.com/typescript-eslint" 21232 + } 21233 + }, 21234 + "node_modules/typescript-eslint/node_modules/@typescript-eslint/type-utils": { 21235 + "version": "8.57.2", 21236 + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz", 21237 + "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==", 21238 + "dev": true, 21239 + "license": "MIT", 21240 + "dependencies": { 21241 + "@typescript-eslint/types": "8.57.2", 21242 + "@typescript-eslint/typescript-estree": "8.57.2", 21243 + "@typescript-eslint/utils": "8.57.2", 21244 + "debug": "^4.4.3", 21245 + "ts-api-utils": "^2.4.0" 21246 + }, 21247 + "engines": { 21248 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 21249 + }, 21250 + "funding": { 21251 + "type": "opencollective", 21252 + "url": "https://opencollective.com/typescript-eslint" 21253 + }, 21254 + "peerDependencies": { 21255 + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", 21256 + "typescript": ">=4.8.4 <6.0.0" 21257 + } 21258 + }, 21021 21259 "node_modules/typescript-eslint/node_modules/@typescript-eslint/types": { 21022 21260 "version": "8.57.2", 21023 21261 "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", ··· 21060 21298 "typescript": ">=4.8.4 <6.0.0" 21061 21299 } 21062 21300 }, 21301 + "node_modules/typescript-eslint/node_modules/@typescript-eslint/utils": { 21302 + "version": "8.57.2", 21303 + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz", 21304 + "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==", 21305 + "dev": true, 21306 + "license": "MIT", 21307 + "dependencies": { 21308 + "@eslint-community/eslint-utils": "^4.9.1", 21309 + "@typescript-eslint/scope-manager": "8.57.2", 21310 + "@typescript-eslint/types": "8.57.2", 21311 + "@typescript-eslint/typescript-estree": "8.57.2" 21312 + }, 21313 + "engines": { 21314 + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 21315 + }, 21316 + "funding": { 21317 + "type": "opencollective", 21318 + "url": "https://opencollective.com/typescript-eslint" 21319 + }, 21320 + "peerDependencies": { 21321 + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", 21322 + "typescript": ">=4.8.4 <6.0.0" 21323 + } 21324 + }, 21063 21325 "node_modules/typescript-eslint/node_modules/@typescript-eslint/visitor-keys": { 21064 21326 "version": "8.57.2", 21065 21327 "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz", ··· 21114 21376 "url": "https://opencollective.com/eslint" 21115 21377 } 21116 21378 }, 21379 + "node_modules/typescript-eslint/node_modules/ignore": { 21380 + "version": "7.0.5", 21381 + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", 21382 + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", 21383 + "dev": true, 21384 + "license": "MIT", 21385 + "engines": { 21386 + "node": ">= 4" 21387 + } 21388 + }, 21117 21389 "node_modules/typescript-eslint/node_modules/minimatch": { 21118 21390 "version": "10.2.4", 21119 21391 "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", ··· 21321 21593 } 21322 21594 }, 21323 21595 "node_modules/validator": { 21324 - "version": "13.15.26", 21325 - "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz", 21326 - "integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==", 21596 + "version": "13.15.35", 21597 + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", 21598 + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", 21327 21599 "license": "MIT", 21328 21600 "engines": { 21329 21601 "node": ">= 0.10" ··· 21491 21763 "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==", 21492 21764 "dependencies": { 21493 21765 "@types/node": "*" 21766 + } 21767 + }, 21768 + "node_modules/word-wrap": { 21769 + "version": "1.2.5", 21770 + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", 21771 + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", 21772 + "license": "MIT", 21773 + "engines": { 21774 + "node": ">=0.10.0" 21494 21775 } 21495 21776 }, 21496 21777 "node_modules/wrap-ansi": {
+1
server/package.json
··· 108 108 "unhomoglyph": "^1.0.6", 109 109 "uuid": "^8.3.2", 110 110 "uuid-apikey": "^1.5.3", 111 + "validator": "^13.15.35", 111 112 "xml-js": "^1.6.11", 112 113 "yargs": "^17.7.2" 113 114 },
+9 -8
server/routes/content/ContentRoutes.test.ts
··· 20 20 let request: Awaited<ReturnType<typeof makeMockedServer>>['request'], 21 21 shutdown: Awaited<ReturnType<typeof makeMockedServer>>['shutdown'], 22 22 apiKey: Awaited<ReturnType<typeof createOrg>>['apiKey'], 23 + orgCleanup: Awaited<ReturnType<typeof createOrg>>['cleanup'], 23 24 models: Dependencies['Sequelize'], 24 25 ModerationConfigService: Dependencies['ModerationConfigService'], 25 26 ApiKeyService: Dependencies['ApiKeyService'], 26 - analytics: Dependencies['DataWarehouseAnalytics']; 27 + analytics: Dependencies['DataWarehouseAnalytics'], 28 + KyselyPg: Dependencies['KyselyPg']; 27 29 28 30 beforeAll(async () => { 29 31 ({ ··· 34 36 DataWarehouseAnalytics: analytics, 35 37 ModerationConfigService, 36 38 ApiKeyService, 39 + KyselyPg, 37 40 }, 38 41 } = await makeMockedServer()); 39 42 40 - const { User, Org } = models; 43 + const { User } = models; 41 44 42 - ({ apiKey } = await createOrg( 43 - { Org }, 44 - ModerationConfigService, 45 - ApiKeyService, 45 + ({ apiKey, cleanup: orgCleanup } = await createOrg( 46 + { KyselyPg, ModerationConfigService, ApiKeyService }, 46 47 orgId, 47 48 )); 48 49 ··· 92 93 }); 93 94 94 95 afterAll(async () => { 95 - const { Org, User } = models; 96 - await Org.destroy({ where: { id: orgId } }); 96 + const { User } = models; 97 + await orgCleanup(); 97 98 await ModerationConfigService.deleteItemType({ 98 99 orgId, 99 100 itemTypeId: contentType1.id,
+6 -10
server/routes/gdpr/gdprRoutes.test.ts
··· 13 13 let request: Awaited<ReturnType<typeof makeMockedServer>>['request'], 14 14 shutdown: Awaited<ReturnType<typeof makeMockedServer>>['shutdown'], 15 15 apiKey: Awaited<ReturnType<typeof createOrg>>['apiKey'], 16 + orgCleanup: Awaited<ReturnType<typeof createOrg>>['cleanup'], 16 17 ApiKeyService: Dependencies['ApiKeyService'], 17 18 ModerationConfigService: Dependencies['ModerationConfigService'], 18 - models: Dependencies['Sequelize']; 19 + KyselyPg: Dependencies['KyselyPg']; 19 20 20 21 beforeAll(async () => { 21 22 try { 22 23 ({ 23 24 request, 24 25 shutdown, 25 - deps: { Sequelize: models, ModerationConfigService, ApiKeyService }, 26 + deps: { ModerationConfigService, ApiKeyService, KyselyPg }, 26 27 } = await makeMockedServer()); 27 28 28 - const { Org } = models; 29 - 30 - ({ apiKey } = await createOrg( 31 - { Org }, 32 - ModerationConfigService, 33 - ApiKeyService, 29 + ({ apiKey, cleanup: orgCleanup } = await createOrg( 30 + { KyselyPg, ModerationConfigService, ApiKeyService }, 34 31 orgId, 35 32 )); 36 33 ··· 54 51 }); 55 52 56 53 afterAll(async () => { 57 - const { Org } = models; 58 - await Org.destroy({ where: { id: orgId } }); 54 + await orgCleanup(); 59 55 await shutdown(); 60 56 }); 61 57
+9 -8
server/routes/items/ItemRoutes.test.ts
··· 15 15 let request: Awaited<ReturnType<typeof makeMockedServer>>['request'], 16 16 shutdown: Awaited<ReturnType<typeof makeMockedServer>>['shutdown'], 17 17 apiKey: Awaited<ReturnType<typeof createOrg>>['apiKey'], 18 + orgCleanup: Awaited<ReturnType<typeof createOrg>>['cleanup'], 18 19 models: Dependencies['Sequelize'], 19 20 ModerationConfigService: Dependencies['ModerationConfigService'], 20 21 ApiKeyService: Dependencies['ApiKeyService'], 21 - analytics: Dependencies['DataWarehouseAnalytics']; 22 + analytics: Dependencies['DataWarehouseAnalytics'], 23 + KyselyPg: Dependencies['KyselyPg']; 22 24 23 25 beforeAll(async () => { 24 26 ({ ··· 29 31 DataWarehouseAnalytics: analytics, 30 32 ModerationConfigService, 31 33 ApiKeyService, 34 + KyselyPg, 32 35 }, 33 36 } = await makeMockedServer()); 34 37 35 - const { User, Org } = models; 38 + const { User } = models; 36 39 37 - ({ apiKey } = await createOrg( 38 - { Org }, 39 - ModerationConfigService, 40 - ApiKeyService, 40 + ({ apiKey, cleanup: orgCleanup } = await createOrg( 41 + { KyselyPg, ModerationConfigService, ApiKeyService }, 41 42 orgId, 42 43 )); 43 44 ··· 73 74 }); 74 75 75 76 afterAll(async () => { 76 - const { Org, User } = models; 77 - await Org.destroy({ where: { id: orgId } }); 77 + const { User } = models; 78 + await orgCleanup(); 78 79 await ModerationConfigService.deleteItemType({ 79 80 orgId, 80 81 itemTypeId: contentType.id,
+13 -10
server/routes/policies/PoliciesRoutes.test.ts
··· 13 13 let request: Awaited<ReturnType<typeof makeMockedServer>>['request'], 14 14 shutdown: Awaited<ReturnType<typeof makeMockedServer>>['shutdown'], 15 15 apiKey: Awaited<ReturnType<typeof createOrg>>['apiKey'], 16 + orgCleanup: Awaited<ReturnType<typeof createOrg>>['cleanup'], 16 17 models: Dependencies['Sequelize'], 17 18 ModerationConfigService: Dependencies['ModerationConfigService'], 18 - ApiKeyService: Dependencies['ApiKeyService']; 19 + ApiKeyService: Dependencies['ApiKeyService'], 20 + KyselyPg: Dependencies['KyselyPg']; 19 21 20 22 beforeAll(async () => { 21 23 ({ 22 24 request, 23 25 shutdown, 24 - deps: { Sequelize: models, ModerationConfigService, ApiKeyService }, 26 + deps: { 27 + Sequelize: models, 28 + ModerationConfigService, 29 + ApiKeyService, 30 + KyselyPg, 31 + }, 25 32 } = await makeMockedServer()); 26 33 27 - const { Org } = models; 28 - 29 - ({ apiKey } = await createOrg( 30 - { Org }, 31 - ModerationConfigService, 32 - ApiKeyService, 34 + ({ apiKey, cleanup: orgCleanup } = await createOrg( 35 + { KyselyPg, ModerationConfigService, ApiKeyService }, 33 36 orgId, 34 37 )); 35 38 }); 36 39 37 40 afterAll(async () => { 38 - const { Org, Policy } = models; 41 + const { Policy } = models; 39 42 await Policy.destroy({ where: { id: policyId1 } }); 40 43 await Policy.destroy({ where: { id: policyId2 } }); 41 - await Org.destroy({ where: { id: orgId } }); 44 + await orgCleanup(); 42 45 await shutdown(); 43 46 }); 44 47
+10 -7
server/routes/reporting/ReportingRoutes.test.ts
··· 17 17 deps: Awaited<ReturnType<typeof makeMockedServer>>['deps'], 18 18 request: Awaited<ReturnType<typeof makeMockedServer>>['request'], 19 19 shutdown: Awaited<ReturnType<typeof makeMockedServer>>['shutdown'], 20 - apiKey: Awaited<ReturnType<typeof createOrg>>['apiKey']; 20 + apiKey: Awaited<ReturnType<typeof createOrg>>['apiKey'], 21 + orgCleanup: Awaited<ReturnType<typeof createOrg>>['cleanup']; 21 22 22 23 const getBulkWriteMock = () => 23 24 deps.DataWarehouseAnalytics.bulkWrite as jest.MockedFunction< ··· 29 30 30 31 models = deps.Sequelize; 31 32 32 - ({ apiKey } = await createOrg( 33 - models, 34 - deps.ModerationConfigService, 35 - deps.ApiKeyService, 33 + ({ apiKey, cleanup: orgCleanup } = await createOrg( 34 + { 35 + KyselyPg: deps.KyselyPg, 36 + ModerationConfigService: deps.ModerationConfigService, 37 + ApiKeyService: deps.ApiKeyService, 38 + }, 36 39 orgId, 37 40 )); 38 41 const userType = await deps.ModerationConfigService.createUserType(orgId, { ··· 119 122 }); 120 123 121 124 afterAll(async () => { 122 - const { Org, User, ItemType } = models; 123 - await Org.destroy({ where: { id: orgId } }); 125 + const { User, ItemType } = models; 126 + await orgCleanup(); 124 127 await ItemType.destroy({ where: { id: contentTypeId } }); 125 128 await ItemType.destroy({ where: { id: userTypeId } }); 126 129 await ItemType.destroy({ where: { id: threadTypeId } });
+2 -4
server/routes/user_scores/UserScoresRoutes.test.ts
··· 11 11 const { 12 12 request, 13 13 shutdown, 14 - deps: { Sequelize: models, ModerationConfigService, ApiKeyService }, 14 + deps: { ModerationConfigService, ApiKeyService, KyselyPg }, 15 15 } = await makeMockedServer(); 16 16 17 17 const { org, apiKey, cleanup: orgCleanup } = await createOrg( 18 - models, 19 - ModerationConfigService, 20 - ApiKeyService, 18 + { KyselyPg, ModerationConfigService, ApiKeyService }, 21 19 uid(), 22 20 ); 23 21 const { itemTypes, cleanup } = await createUserItemTypes({
+10 -6
server/services/itemInvestigationService/itemInvestigationService.test.ts
··· 42 42 const dummyOrgId = uid(); 43 43 44 44 await createOrg( 45 - { Org: container.Sequelize.Org }, 46 - container.ModerationConfigService, 47 - container.ApiKeyService, 45 + { 46 + KyselyPg: container.KyselyPg, 47 + ModerationConfigService: container.ModerationConfigService, 48 + ApiKeyService: container.ApiKeyService, 49 + }, 48 50 dummyOrgId, 49 51 ); 50 52 ··· 123 125 const dummyOrgId = uid(); 124 126 125 127 await createOrg( 126 - { Org: container.Sequelize.Org }, 127 - container.ModerationConfigService, 128 - container.ApiKeyService, 128 + { 129 + KyselyPg: container.KyselyPg, 130 + ModerationConfigService: container.ModerationConfigService, 131 + ApiKeyService: container.ApiKeyService, 132 + }, 129 133 dummyOrgId, 130 134 ); 131 135
+5 -3
server/services/manualReviewToolService/modules/CommentOperations.test.ts
··· 16 16 // Create test org 17 17 const orgId = uuidv1(); 18 18 const { cleanup: orgCleanup } = await createOrg( 19 - { Org: container.Sequelize.Org }, 20 - container.ModerationConfigService, 21 - container.ApiKeyService, 19 + { 20 + KyselyPg: container.KyselyPg, 21 + ModerationConfigService: container.ModerationConfigService, 22 + ApiKeyService: container.ApiKeyService, 23 + }, 22 24 orgId, 23 25 ); 24 26
+5 -4
server/services/manualReviewToolService/modules/JobRouting.test.ts
··· 21 21 describe('JobRouting tests', () => { 22 22 const jobRoutingTestWithFixtures = makeTestWithFixture(async () => { 23 23 const { container } = await getBottle(); 24 - const { Org } = container.Sequelize; 25 24 const manualReviewToolService = container.ManualReviewToolService; 26 25 const { org, cleanup: orgCleanup } = await createOrg( 27 - { Org }, 28 - container.ModerationConfigService, 29 - container.ApiKeyService, 26 + { 27 + KyselyPg: container.KyselyPg, 28 + ModerationConfigService: container.ModerationConfigService, 29 + ApiKeyService: container.ApiKeyService, 30 + }, 30 31 uid(), 31 32 ); 32 33 const userId = uid();
+5 -3
server/services/manualReviewToolService/modules/QueueOperations.test.ts
··· 36 36 const container = (await getBottle()).container; 37 37 38 38 const { org, cleanup: orgCleanup } = await createOrg( 39 - { Org: container.Sequelize.Org }, 40 - container.ModerationConfigService, 41 - container.ApiKeyService, 39 + { 40 + KyselyPg: container.KyselyPg, 41 + ModerationConfigService: container.ModerationConfigService, 42 + ApiKeyService: container.ApiKeyService, 43 + }, 42 44 uid(), 43 45 ); 44 46
+43 -25
server/services/moderationConfigService/moderationConfigService.test.ts
··· 75 75 ] satisfies Policy[]; 76 76 77 77 const dummyOrgId = uid(); 78 + let dummyOrgCleanup: () => Promise<void>; 78 79 const dummySchema = [ 79 80 { name: 'fakeField', type: 'STRING', required: false, container: null }, 80 81 ] as const; ··· 118 119 ); 119 120 120 121 const createOrgResult = await createOrg( 121 - { Org: container.Sequelize.Org }, 122 - container.ModerationConfigService, 123 - container.ApiKeyService, 122 + { 123 + KyselyPg: container.KyselyPg, 124 + ModerationConfigService: container.ModerationConfigService, 125 + ApiKeyService: container.ApiKeyService, 126 + }, 124 127 dummyOrgId, 125 128 ); 126 129 127 130 defaultUserItemType = createOrgResult.defaultUserItemType; 131 + dummyOrgCleanup = createOrgResult.cleanup; 128 132 allCreatedItemTypes = [...allCreatedItemTypes, defaultUserItemType]; 129 133 }); 130 134 131 135 afterAll(async () => { 132 136 const { Sequelize: models } = (await getBottle()).container; 133 - await models.Org.destroy({ where: { id: dummyOrgId } }); 137 + await dummyOrgCleanup(); 134 138 135 139 await Promise.all([ 136 140 container.KyselyPg.destroy(), ··· 193 197 describe('#getRuleByIdAndOrg', () => { 194 198 const testWithRuleRow = makeTestWithFixture(async () => { 195 199 const { org, cleanup: orgCleanup } = await createOrg( 196 - { Org: container.Sequelize.Org }, 197 - container.ModerationConfigService, 198 - container.ApiKeyService, 200 + { 201 + KyselyPg: container.KyselyPg, 202 + ModerationConfigService: container.ModerationConfigService, 203 + ApiKeyService: container.ApiKeyService, 204 + }, 199 205 uid(), 200 206 ); 201 207 const { user, cleanup: userCleanup } = await createUser( ··· 248 254 'returns null when the org id does not match (IDOR guard)', 249 255 async ({ ruleId }) => { 250 256 const { org: otherOrg, cleanup: otherOrgCleanup } = await createOrg( 251 - { Org: container.Sequelize.Org }, 252 - container.ModerationConfigService, 253 - container.ApiKeyService, 257 + { 258 + KyselyPg: container.KyselyPg, 259 + ModerationConfigService: container.ModerationConfigService, 260 + ApiKeyService: container.ApiKeyService, 261 + }, 254 262 uid(), 255 263 ); 256 264 try { ··· 702 710 'should throw NotFound when called with the wrong org', 703 711 async ({ action }) => { 704 712 const otherOrg = await createOrg( 705 - { Org: container.Sequelize.Org }, 706 - container.ModerationConfigService, 707 - container.ApiKeyService, 713 + { 714 + KyselyPg: container.KyselyPg, 715 + ModerationConfigService: container.ModerationConfigService, 716 + ApiKeyService: container.ApiKeyService, 717 + }, 708 718 uid(), 709 719 ); 710 720 try { ··· 894 904 'should return false when called with the wrong org and leave the row intact', 895 905 async ({ action }) => { 896 906 const otherOrg = await createOrg( 897 - { Org: container.Sequelize.Org }, 898 - container.ModerationConfigService, 899 - container.ApiKeyService, 907 + { 908 + KyselyPg: container.KyselyPg, 909 + ModerationConfigService: container.ModerationConfigService, 910 + ApiKeyService: container.ApiKeyService, 911 + }, 900 912 uid(), 901 913 ); 902 914 try { ··· 1078 1090 // applies-to-all rows (they'd otherwise leak across orgs since the 1079 1091 // ANY(...) predicate alone has no tenant scope). 1080 1092 const otherOrg = await createOrg( 1081 - { Org: container.Sequelize.Org }, 1082 - container.ModerationConfigService, 1083 - container.ApiKeyService, 1093 + { 1094 + KyselyPg: container.KyselyPg, 1095 + ModerationConfigService: container.ModerationConfigService, 1096 + ApiKeyService: container.ApiKeyService, 1097 + }, 1084 1098 uid(), 1085 1099 ); 1086 1100 try { ··· 1141 1155 'should not return actions when called with a different org', 1142 1156 async ({ rule }) => { 1143 1157 const otherOrg = await createOrg( 1144 - { Org: container.Sequelize.Org }, 1145 - container.ModerationConfigService, 1146 - container.ApiKeyService, 1158 + { 1159 + KyselyPg: container.KyselyPg, 1160 + ModerationConfigService: container.ModerationConfigService, 1161 + ApiKeyService: container.ApiKeyService, 1162 + }, 1147 1163 uid(), 1148 1164 ); 1149 1165 try { ··· 1177 1193 describe('Mutations', () => { 1178 1194 const testWithUserAndOrg = makeTestWithFixture(async () => { 1179 1195 const { org, cleanup: orgCleanup } = await createOrg( 1180 - { Org: container.Sequelize.Org }, 1181 - container.ModerationConfigService, 1182 - container.ApiKeyService, 1196 + { 1197 + KyselyPg: container.KyselyPg, 1198 + ModerationConfigService: container.ModerationConfigService, 1199 + ApiKeyService: container.ApiKeyService, 1200 + }, 1183 1201 uid(), 1184 1202 ); 1185 1203
+7
server/services/moderationConfigService/moderationConfigService.ts
··· 332 332 return this.ruleReadOps.getRuleByIdAndOrg(ruleId, orgId, opts); 333 333 } 334 334 335 + async getRulesForOrg( 336 + orgId: string, 337 + opts?: { readFromReplica?: boolean }, 338 + ): Promise<readonly PlainRuleWithLatestVersion[]> { 339 + return this.ruleReadOps.getRulesForOrg(orgId, opts); 340 + } 341 + 335 342 async findEnabledUserRules(): Promise<PlainRuleWithLatestVersion[]> { 336 343 return this.ruleReadOps.findEnabledUserRules(); 337 344 }
+21
server/services/moderationConfigService/modules/RuleReadOperations.ts
··· 146 146 return rowToPlainRuleWithLatest(row); 147 147 } 148 148 149 + /** 150 + * All rules for an org (latest version string), for GraphQL org.rules and 151 + * similar list surfaces. Not filtered by enabled status. 152 + */ 153 + async getRulesForOrg( 154 + orgId: string, 155 + opts?: { readFromReplica?: boolean }, 156 + ): Promise<PlainRuleWithLatestVersion[]> { 157 + const readFromReplica = opts?.readFromReplica ?? true; 158 + const pg = readFromReplica ? this.pgQueryReplica : this.pgQuery; 159 + const rows = (await pg 160 + .selectFrom('public.rules as r') 161 + .leftJoin('public.rules_latest_versions as rlv', 'rlv.rule_id', 'r.id') 162 + .select(ruleSelect) 163 + .where('r.org_id', '=', orgId) 164 + .orderBy('r.name', 'asc') 165 + .execute()) as RuleRow[]; 166 + 167 + return rows.map(rowToPlainRuleWithLatest); 168 + } 169 + 149 170 async findEnabledUserRules() { 150 171 const today = String(getUtcDateOnlyString()); 151 172 const rows = (await this.pgQueryReplica
+12 -8
server/services/ruleAnomalyDetectionService/detectRulePassRateAnomaliesJob.test.ts
··· 90 90 Sequelize: models, 91 91 ModerationConfigService, 92 92 ApiKeyService, 93 + KyselyPg, 93 94 } = (await getBottle()).container; 94 95 95 96 // make some fake rules (w/ stable ids so we can match them in a snapshot) 96 97 // in different initial alarm statuses, to test all 9 combinations [i.e., 97 98 // starting and ending at one of (OK, ALARM, or INSUFFICENT_DATA), where 98 99 // the start and end states can be the same]. 99 - const { org } = await createOrg( 100 - models, 100 + const { org, cleanup: orgCleanup } = await createOrg({ 101 + KyselyPg, 101 102 ModerationConfigService, 102 103 ApiKeyService, 103 - ); 104 - const { org: org2 } = await createOrg( 105 - models, 106 - ModerationConfigService, 107 - ApiKeyService, 104 + }); 105 + const { org: org2, cleanup: org2Cleanup } = await createOrg( 106 + { 107 + KyselyPg, 108 + ModerationConfigService, 109 + ApiKeyService, 110 + }, 108 111 undefined, 109 112 { onCallAlertEmail: 'test@gmail.com' }, 110 113 ); ··· 208 211 deleteMockData = async () => { 209 212 await Promise.all(fakeRules.map(async (it) => it.destroy())); 210 213 await Promise.all([ruleOwner.destroy(), ruleOwner2.destroy()]); 211 - await Promise.all([org.destroy(), org2.destroy()]); 214 + await orgCleanup(); 215 + await org2Cleanup(); 212 216 await models.sequelize.close(); 213 217 }; 214 218 /* eslint-enable functional/immutable-data */
+2 -3
server/services/signalsService/signals/aggregation/AggregationSignal.test.ts
··· 27 27 RuleAPIDataSource, 28 28 ActionAPIDataSource, 29 29 ApiKeyService, 30 + KyselyPg, 30 31 } = deps; 31 32 32 33 const { org, cleanup: orgCleanup } = await createOrg( 33 - models, 34 - ModerationConfigService, 35 - ApiKeyService, 34 + { KyselyPg, ModerationConfigService, ApiKeyService }, 36 35 uid(), 37 36 ); 38 37
+23 -13
server/test/fixtureHelpers/createOrg.ts
··· 1 1 import { faker } from '@faker-js/faker'; 2 2 import { uid } from 'uid'; 3 3 4 + import { 5 + kyselyOrgDeleteById, 6 + kyselyOrgInsert, 7 + } from '../../graphql/datasources/orgKyselyPersistence.js'; 4 8 import { type Dependencies } from '../../iocContainer/index.js'; 5 9 import { logErrorAndThrow } from '../utils.js'; 6 10 7 - export default async function ( 8 - models: Pick<Dependencies['Sequelize'], 'Org'>, 9 - moderationConfigService: Dependencies['ModerationConfigService'], 10 - apiKeyService: Dependencies['ApiKeyService'], 11 + export default async function createOrg( 12 + deps: Pick< 13 + Dependencies, 14 + 'KyselyPg' | 'ModerationConfigService' | 'ApiKeyService' 15 + >, 11 16 id?: string, 12 17 extra: { 13 18 onCallAlertEmail?: string; 14 19 } = {}, 15 20 ) { 16 21 const orgId = id ?? uid(); 17 - const org = await models.Org.create({ 22 + 23 + const org = await kyselyOrgInsert({ 24 + db: deps.KyselyPg, 18 25 id: orgId, 19 26 name: `Dummy_Company_Name_${orgId}`, 20 27 email: faker.internet.email(), 21 28 websiteUrl: faker.internet.url(), 22 - onCallAlertEmail: extra.onCallAlertEmail ?? undefined, 29 + onCallAlertEmail: extra.onCallAlertEmail ?? null, 23 30 }).catch(logErrorAndThrow); 24 31 25 - const { apiKey } = await apiKeyService 26 - .createApiKey(orgId, `Dummy_Company_Name_${orgId}_Key`, null, null) 27 - .catch(logErrorAndThrow); 32 + const { apiKey } = await deps.ApiKeyService.createApiKey( 33 + orgId, 34 + `Dummy_Company_Name_${orgId}_Key`, 35 + null, 36 + null, 37 + ).catch(logErrorAndThrow); 28 38 29 - const defaultUserItemType = await moderationConfigService 30 - .createDefaultUserType(orgId) 31 - .catch(logErrorAndThrow); 39 + const defaultUserItemType = await deps.ModerationConfigService.createDefaultUserType( 40 + orgId, 41 + ).catch(logErrorAndThrow); 32 42 33 43 return { 34 44 org, 35 45 apiKey, 36 46 defaultUserItemType, 37 47 async cleanup() { 38 - await org.destroy(); 48 + await kyselyOrgDeleteById(deps.KyselyPg, orgId); 39 49 }, 40 50 }; 41 51 }