diff --git a/backend/src/api/member/memberMerge.ts b/backend/src/api/member/memberMerge.ts index 38b7b43562..5dfd654696 100644 --- a/backend/src/api/member/memberMerge.ts +++ b/backend/src/api/member/memberMerge.ts @@ -1,5 +1,6 @@ import { CommonMemberService, invalidateMemberQueryCache } from '@crowd/common_services' -import { optionsQx } from '@crowd/data-access-layer' + +import { optionsQx } from '@/database/sequelizeQueryExecutor' import Permissions from '../../security/permissions' import track from '../../segment/track' diff --git a/backend/src/api/public/middlewares/staticApiKeyMiddleware.ts b/backend/src/api/public/middlewares/staticApiKeyMiddleware.ts index 76d928f8a8..ac6aa4b207 100644 --- a/backend/src/api/public/middlewares/staticApiKeyMiddleware.ts +++ b/backend/src/api/public/middlewares/staticApiKeyMiddleware.ts @@ -2,7 +2,9 @@ import crypto from 'crypto' import type { NextFunction, Request, RequestHandler, Response } from 'express' import { UnauthorizedError } from '@crowd/common' -import { findApiKeyByHash, optionsQx, touchApiKeyLastUsed } from '@crowd/data-access-layer' +import { findApiKeyByHash, touchApiKeyLastUsed } from '@crowd/data-access-layer' + +import { optionsQx } from '@/database/sequelizeQueryExecutor' export function staticApiKeyMiddleware(): RequestHandler { return async (req: Request, _res: Response, next: NextFunction): Promise => { diff --git a/backend/src/api/public/v1/affiliations/getAffiliationByHandle.ts b/backend/src/api/public/v1/affiliations/getAffiliationByHandle.ts index 3273149efb..f7773a17b3 100644 --- a/backend/src/api/public/v1/affiliations/getAffiliationByHandle.ts +++ b/backend/src/api/public/v1/affiliations/getAffiliationByHandle.ts @@ -4,10 +4,10 @@ import { NotFoundError } from '@crowd/common' import { findMembersByGithubHandles, findVerifiedEmailsByMemberIds, - optionsQx, resolveAffiliationsByMemberIds, } from '@crowd/data-access-layer' +import { optionsQx } from '@/database/sequelizeQueryExecutor' import { ok } from '@/utils/api' export async function getAffiliationByHandle(req: Request, res: Response): Promise { diff --git a/backend/src/api/public/v1/affiliations/getAffiliations.ts b/backend/src/api/public/v1/affiliations/getAffiliations.ts index 9179081a13..c1d1ed0e45 100644 --- a/backend/src/api/public/v1/affiliations/getAffiliations.ts +++ b/backend/src/api/public/v1/affiliations/getAffiliations.ts @@ -4,10 +4,10 @@ import { z } from 'zod' import { findMembersByGithubHandles, findVerifiedEmailsByMemberIds, - optionsQx, resolveAffiliationsByMemberIds, } from '@crowd/data-access-layer' +import { optionsQx } from '@/database/sequelizeQueryExecutor' import { ok } from '@/utils/api' import { validateOrThrow } from '@/utils/validation' diff --git a/backend/src/api/public/v1/members/createMember.ts b/backend/src/api/public/v1/members/createMember.ts index 075be732ee..d112143cc1 100644 --- a/backend/src/api/public/v1/members/createMember.ts +++ b/backend/src/api/public/v1/members/createMember.ts @@ -3,13 +3,10 @@ import { z } from 'zod' import { captureApiChange, memberCreateAction, memberEditIdentitiesAction } from '@crowd/audit-logs' import { getProperDisplayName } from '@crowd/common' -import { - insertManyMemberIdentities, - createMember as insertMember, - optionsQx, -} from '@crowd/data-access-layer' +import { insertManyMemberIdentities, createMember as insertMember } from '@crowd/data-access-layer' import { MemberIdentityType } from '@crowd/types' +import { optionsQx } from '@/database/sequelizeQueryExecutor' import { created } from '@/utils/api' import { rethrowDbConflict } from '@/utils/err' import { validateOrThrow } from '@/utils/validation' diff --git a/backend/src/api/public/v1/members/identities/createMemberIdentity.ts b/backend/src/api/public/v1/members/identities/createMemberIdentity.ts index 5d20ee7398..bef6c773c6 100644 --- a/backend/src/api/public/v1/members/identities/createMemberIdentity.ts +++ b/backend/src/api/public/v1/members/identities/createMemberIdentity.ts @@ -8,12 +8,12 @@ import { findMemberById, findMemberIdentitiesByValue, createMemberIdentity as insertMemberIdentity, - optionsQx, touchMemberUpdatedAt, updateMemberIdentity, } from '@crowd/data-access-layer' import { IMemberIdentity, MemberIdentityType } from '@crowd/types' +import { optionsQx } from '@/database/sequelizeQueryExecutor' import { created, ok } from '@/utils/api' import { rethrowDbConflict } from '@/utils/err' import { validateOrThrow } from '@/utils/validation' diff --git a/backend/src/api/public/v1/members/identities/getMemberIdentities.ts b/backend/src/api/public/v1/members/identities/getMemberIdentities.ts index aa94fb7204..3f8925876c 100644 --- a/backend/src/api/public/v1/members/identities/getMemberIdentities.ts +++ b/backend/src/api/public/v1/members/identities/getMemberIdentities.ts @@ -2,13 +2,9 @@ import type { Request, Response } from 'express' import { z } from 'zod' import { NotFoundError } from '@crowd/common' -import { - MemberField, - fetchMemberIdentities, - findMemberById, - optionsQx, -} from '@crowd/data-access-layer' +import { MemberField, fetchMemberIdentities, findMemberById } from '@crowd/data-access-layer' +import { optionsQx } from '@/database/sequelizeQueryExecutor' import { ok } from '@/utils/api' import { validateOrThrow } from '@/utils/validation' diff --git a/backend/src/api/public/v1/members/identities/verifyMemberIdentity.ts b/backend/src/api/public/v1/members/identities/verifyMemberIdentity.ts index a7f940b7c6..3229f44f48 100644 --- a/backend/src/api/public/v1/members/identities/verifyMemberIdentity.ts +++ b/backend/src/api/public/v1/members/identities/verifyMemberIdentity.ts @@ -18,7 +18,6 @@ import { deleteMemberIdentity, findMemberById, findMemberIdentityById, - optionsQx, queryActivityRelations, updateMemberIdentity, } from '@crowd/data-access-layer' @@ -30,6 +29,7 @@ import { MemberUnmergeResult, } from '@crowd/types' +import { optionsQx } from '@/database/sequelizeQueryExecutor' import { noContent, ok } from '@/utils/api' import { rethrowDbConflict } from '@/utils/err' import { validateOrThrow } from '@/utils/validation' diff --git a/backend/src/api/public/v1/members/maintainer-roles/getMemberMaintainerRoles.ts b/backend/src/api/public/v1/members/maintainer-roles/getMemberMaintainerRoles.ts index b24dfec61c..095e0c0170 100644 --- a/backend/src/api/public/v1/members/maintainer-roles/getMemberMaintainerRoles.ts +++ b/backend/src/api/public/v1/members/maintainer-roles/getMemberMaintainerRoles.ts @@ -2,13 +2,9 @@ import type { Request, Response } from 'express' import { z } from 'zod' import { NotFoundError } from '@crowd/common' -import { - MemberField, - findMaintainerRoles, - findMemberById, - optionsQx, -} from '@crowd/data-access-layer' +import { MemberField, findMaintainerRoles, findMemberById } from '@crowd/data-access-layer' +import { optionsQx } from '@/database/sequelizeQueryExecutor' import { ok } from '@/utils/api' import { validateOrThrow } from '@/utils/validation' diff --git a/backend/src/api/public/v1/members/project-affiliations/getProjectAffiliations.ts b/backend/src/api/public/v1/members/project-affiliations/getProjectAffiliations.ts index db3e18de43..85d9f01312 100644 --- a/backend/src/api/public/v1/members/project-affiliations/getProjectAffiliations.ts +++ b/backend/src/api/public/v1/members/project-affiliations/getProjectAffiliations.ts @@ -9,9 +9,9 @@ import { fetchMemberWorkExperienceAffiliations, findMaintainerRoles, findMemberById, - optionsQx, } from '@crowd/data-access-layer' +import { optionsQx } from '@/database/sequelizeQueryExecutor' import { ok } from '@/utils/api' import { validateOrThrow } from '@/utils/validation' diff --git a/backend/src/api/public/v1/members/project-affiliations/patchProjectAffiliation.ts b/backend/src/api/public/v1/members/project-affiliations/patchProjectAffiliation.ts index fe516a38e9..1623d4ea0c 100644 --- a/backend/src/api/public/v1/members/project-affiliations/patchProjectAffiliation.ts +++ b/backend/src/api/public/v1/members/project-affiliations/patchProjectAffiliation.ts @@ -11,11 +11,11 @@ import { findMaintainerRoles, findMemberById, insertMemberSegmentAffiliations, - optionsQx, } from '@crowd/data-access-layer' import type { ISegmentAffiliationWithOrg } from '@crowd/data-access-layer' import { deleteMemberSegmentAffiliations } from '@crowd/data-access-layer/src/member_segment_affiliations' +import { optionsQx } from '@/database/sequelizeQueryExecutor' import { ok } from '@/utils/api' import { validateOrThrow } from '@/utils/validation' diff --git a/backend/src/api/public/v1/members/resolveMember.ts b/backend/src/api/public/v1/members/resolveMember.ts index 0d9f654f4e..0db66b33ae 100644 --- a/backend/src/api/public/v1/members/resolveMember.ts +++ b/backend/src/api/public/v1/members/resolveMember.ts @@ -2,9 +2,10 @@ import type { Request, Response } from 'express' import { z } from 'zod' import { ConflictError, NotFoundError } from '@crowd/common' -import { findMemberIdsByIdentities, optionsQx } from '@crowd/data-access-layer' +import { findMemberIdsByIdentities } from '@crowd/data-access-layer' import { IMemberIdentity, MemberIdentityType, PlatformType } from '@crowd/types' +import { optionsQx } from '@/database/sequelizeQueryExecutor' import { ok } from '@/utils/api' import { validateOrThrow } from '@/utils/validation' diff --git a/backend/src/api/public/v1/members/work-experiences/createMemberWorkExperience.ts b/backend/src/api/public/v1/members/work-experiences/createMemberWorkExperience.ts index f84e7d259c..2d515eaac1 100644 --- a/backend/src/api/public/v1/members/work-experiences/createMemberWorkExperience.ts +++ b/backend/src/api/public/v1/members/work-experiences/createMemberWorkExperience.ts @@ -17,7 +17,6 @@ import { fetchManyMemberOrgsWithOrgData, fetchManyOrganizationAffiliationPolicies, findMemberById, - optionsQx, } from '@crowd/data-access-layer' import { deleteMemberSegmentAffiliations } from '@crowd/data-access-layer/src/member_segment_affiliations' import type { @@ -26,6 +25,7 @@ import type { MemberOrganizationDateRange, } from '@crowd/types' +import { optionsQx } from '@/database/sequelizeQueryExecutor' import { created } from '@/utils/api' import { toMemberWorkExperience } from '@/utils/mapper' import { validateOrThrow } from '@/utils/validation' diff --git a/backend/src/api/public/v1/members/work-experiences/deleteMemberWorkExperience.ts b/backend/src/api/public/v1/members/work-experiences/deleteMemberWorkExperience.ts index c0ea12faf9..66b3771331 100644 --- a/backend/src/api/public/v1/members/work-experiences/deleteMemberWorkExperience.ts +++ b/backend/src/api/public/v1/members/work-experiences/deleteMemberWorkExperience.ts @@ -9,9 +9,9 @@ import { deleteMemberOrganizations, fetchMemberOrganizations, findMemberById, - optionsQx, } from '@crowd/data-access-layer' +import { optionsQx } from '@/database/sequelizeQueryExecutor' import { noContent } from '@/utils/api' import { getOverlappingGroupedMemberOrganizations } from '@/utils/mapper' import { validateOrThrow } from '@/utils/validation' diff --git a/backend/src/api/public/v1/members/work-experiences/getMemberWorkExperiences.ts b/backend/src/api/public/v1/members/work-experiences/getMemberWorkExperiences.ts index ed000b68a4..fed76f36d8 100644 --- a/backend/src/api/public/v1/members/work-experiences/getMemberWorkExperiences.ts +++ b/backend/src/api/public/v1/members/work-experiences/getMemberWorkExperiences.ts @@ -6,9 +6,9 @@ import { MemberField, fetchManyMemberOrgsWithOrgData, findMemberById, - optionsQx, } from '@crowd/data-access-layer' +import { optionsQx } from '@/database/sequelizeQueryExecutor' import { ok } from '@/utils/api' import { groupMemberOrganizations, toMemberWorkExperience } from '@/utils/mapper' import { validateOrThrow } from '@/utils/validation' diff --git a/backend/src/api/public/v1/members/work-experiences/updateMemberWorkExperience.ts b/backend/src/api/public/v1/members/work-experiences/updateMemberWorkExperience.ts index f0ee3462e5..77f55977f6 100644 --- a/backend/src/api/public/v1/members/work-experiences/updateMemberWorkExperience.ts +++ b/backend/src/api/public/v1/members/work-experiences/updateMemberWorkExperience.ts @@ -16,7 +16,6 @@ import { fetchManyMemberOrgsWithOrgData, fetchMemberOrganizations, findMemberById, - optionsQx, updateMemberOrganization, } from '@crowd/data-access-layer' import type { @@ -25,6 +24,7 @@ import type { MemberOrganizationUpdate, } from '@crowd/types' +import { optionsQx } from '@/database/sequelizeQueryExecutor' import { ok } from '@/utils/api' import { getOverlappingGroupedMemberOrganizations, diff --git a/backend/src/api/public/v1/members/work-experiences/verifyMemberWorkExperience.ts b/backend/src/api/public/v1/members/work-experiences/verifyMemberWorkExperience.ts index be85eca166..0491bf7079 100644 --- a/backend/src/api/public/v1/members/work-experiences/verifyMemberWorkExperience.ts +++ b/backend/src/api/public/v1/members/work-experiences/verifyMemberWorkExperience.ts @@ -10,11 +10,11 @@ import { fetchManyMemberOrgsWithOrgData, fetchMemberOrganizations, findMemberById, - optionsQx, updateMemberOrganization, } from '@crowd/data-access-layer' import { IMemberOrganization, IMemberRoleWithOrganization } from '@crowd/types' +import { optionsQx } from '@/database/sequelizeQueryExecutor' import { ok } from '@/utils/api' import { getOverlappingGroupedMemberOrganizations, diff --git a/backend/src/api/public/v1/organizations/createOrganization.ts b/backend/src/api/public/v1/organizations/createOrganization.ts index 2ae7b3abae..0d7865b684 100644 --- a/backend/src/api/public/v1/organizations/createOrganization.ts +++ b/backend/src/api/public/v1/organizations/createOrganization.ts @@ -3,9 +3,10 @@ import { z } from 'zod' import { captureApiChange, organizationCreateAction } from '@crowd/audit-logs' import { BadRequestError, InternalError, normalizeHostname } from '@crowd/common' -import { findOrCreateOrganization, optionsQx } from '@crowd/data-access-layer' +import { findOrCreateOrganization } from '@crowd/data-access-layer' import { OrganizationAttributeSource, OrganizationIdentityType } from '@crowd/types' +import { optionsQx } from '@/database/sequelizeQueryExecutor' import { created } from '@/utils/api' import { validateOrThrow } from '@/utils/validation' diff --git a/backend/src/api/public/v1/organizations/getOrganization.ts b/backend/src/api/public/v1/organizations/getOrganization.ts index 0926f30471..03e37e57ad 100644 --- a/backend/src/api/public/v1/organizations/getOrganization.ts +++ b/backend/src/api/public/v1/organizations/getOrganization.ts @@ -2,8 +2,9 @@ import type { Request, Response } from 'express' import { z } from 'zod' import { BadRequestError, NotFoundError, normalizeHostname } from '@crowd/common' -import { findOrganizationByNameOrDomain, optionsQx } from '@crowd/data-access-layer' +import { findOrganizationByNameOrDomain } from '@crowd/data-access-layer' +import { optionsQx } from '@/database/sequelizeQueryExecutor' import { ok } from '@/utils/api' import { validateOrThrow } from '@/utils/validation' diff --git a/backend/src/bin/scripts/merge-members.ts b/backend/src/bin/scripts/merge-members.ts index 51da187b70..3cd3bf6ba2 100644 --- a/backend/src/bin/scripts/merge-members.ts +++ b/backend/src/bin/scripts/merge-members.ts @@ -5,10 +5,11 @@ import path from 'path' import { generateUUIDv1 } from '@crowd/common' import { CommonMemberService } from '@crowd/common_services' -import { optionsQx } from '@crowd/data-access-layer' import { MemberField, findMemberById } from '@crowd/data-access-layer/src/members' import { getServiceLogger } from '@crowd/logging' +import { optionsQx } from '@/database/sequelizeQueryExecutor' + import SequelizeRepository from '../../database/repositories/sequelizeRepository' /* eslint-disable no-console */ diff --git a/backend/src/bin/scripts/merge-similar-organizations.ts b/backend/src/bin/scripts/merge-similar-organizations.ts index 6be6f7b54e..7f02286e27 100644 --- a/backend/src/bin/scripts/merge-similar-organizations.ts +++ b/backend/src/bin/scripts/merge-similar-organizations.ts @@ -4,11 +4,11 @@ import * as fs from 'fs' import path from 'path' import { QueryTypes } from 'sequelize' -import { optionsQx } from '@crowd/data-access-layer' import { addMergeAction, setMergeAction } from '@crowd/data-access-layer/src/mergeActions/repo' import { MergeActionState, MergeActionType } from '@crowd/types' import { IRepositoryOptions } from '@/database/repositories/IRepositoryOptions' +import { optionsQx } from '@/database/sequelizeQueryExecutor' import getUserContext from '@/database/utils/getUserContext' import OrganizationService from '@/services/organizationService' import TenantService from '@/services/tenantService' diff --git a/backend/src/database/repositories/memberRepository.ts b/backend/src/database/repositories/memberRepository.ts index 9fa32b2b41..3cc917b0e0 100644 --- a/backend/src/database/repositories/memberRepository.ts +++ b/backend/src/database/repositories/memberRepository.ts @@ -50,7 +50,6 @@ import { includeMemberToSegments, } from '@crowd/data-access-layer/src/members/segments' import { IDbMemberData } from '@crowd/data-access-layer/src/members/types' -import { optionsBgQx, optionsQx } from '@crowd/data-access-layer/src/queryExecutor' import { fetchManySegments, getSegmentMergeSuggestionCounts, @@ -76,6 +75,7 @@ import { import { KUBE_MODE, SERVICE } from '@/conf' import { ServiceType } from '@/conf/configTypes' +import { optionsBgQx, optionsQx } from '@/database/sequelizeQueryExecutor' import { IFetchMemberMergeSuggestionArgs, SimilarityScoreRange } from '@/types/mergeSuggestionTypes' import { PlatformIdentities } from '../../serverless/integrations/types/messageTypes' diff --git a/backend/src/database/repositories/organizationRepository.ts b/backend/src/database/repositories/organizationRepository.ts index 9f6a50b28c..086c3edb50 100644 --- a/backend/src/database/repositories/organizationRepository.ts +++ b/backend/src/database/repositories/organizationRepository.ts @@ -33,7 +33,6 @@ import { upsertOrgAttributes, } from '@crowd/data-access-layer/src/organizations' import { findAttribute } from '@crowd/data-access-layer/src/organizations/attributesConfig' -import { optionsQx } from '@crowd/data-access-layer/src/queryExecutor' import { findSegmentById, getSegmentMergeSuggestionCounts, @@ -49,6 +48,7 @@ import { SegmentData, } from '@crowd/types' +import { optionsQx } from '@/database/sequelizeQueryExecutor' import { IFetchOrganizationMergeSuggestionArgs, SimilarityScoreRange, diff --git a/backend/src/database/repositories/repositoryBase.ts b/backend/src/database/repositories/repositoryBase.ts index 0b5649fe09..103bf6cc73 100644 --- a/backend/src/database/repositories/repositoryBase.ts +++ b/backend/src/database/repositories/repositoryBase.ts @@ -1,14 +1,11 @@ /* eslint-disable class-methods-use-this,@typescript-eslint/no-unused-vars */ import { Sequelize } from 'sequelize' -import { - QueryExecutor, - SequelizeQueryExecutor, - TransactionalSequelizeQueryExecutor, - optionsQx, -} from '@crowd/data-access-layer/src/queryExecutor' +import { QueryExecutor } from '@crowd/data-access-layer/src/queryExecutor' import { PageData, SearchCriteria } from '@crowd/types' +import { optionsQx } from '../sequelizeQueryExecutor' + import { IRepositoryOptions } from './IRepositoryOptions' import SequelizeRepository from './sequelizeRepository' diff --git a/backend/src/database/repositories/sequelizeRepository.ts b/backend/src/database/repositories/sequelizeRepository.ts index 7c8b09dcf0..245d5e5b4a 100644 --- a/backend/src/database/repositories/sequelizeRepository.ts +++ b/backend/src/database/repositories/sequelizeRepository.ts @@ -3,11 +3,7 @@ import { Sequelize, Transaction, UniqueConstraintError } from 'sequelize' import { Error400 } from '@crowd/common' import { DbConnection, getDbConnection } from '@crowd/data-access-layer/src/database' -import { - QueryExecutor, - SequelizeQueryExecutor, - TransactionalSequelizeQueryExecutor, -} from '@crowd/data-access-layer/src/queryExecutor' +import { QueryExecutor } from '@crowd/data-access-layer/src/queryExecutor' import { getServiceLogger } from '@crowd/logging' import { getOpensearchClient } from '@crowd/opensearch' import { getRedisClient } from '@crowd/redis' @@ -23,6 +19,10 @@ import { } from '../../conf' import { IServiceOptions } from '../../services/IServiceOptions' import { databaseInit } from '../databaseConnection' +import { + SequelizeQueryExecutor, + TransactionalSequelizeQueryExecutor, +} from '../sequelizeQueryExecutor' import { IRepositoryOptions } from './IRepositoryOptions' diff --git a/backend/src/database/sequelizeQueryExecutor.ts b/backend/src/database/sequelizeQueryExecutor.ts new file mode 100644 index 0000000000..3361c38e47 --- /dev/null +++ b/backend/src/database/sequelizeQueryExecutor.ts @@ -0,0 +1,131 @@ +/* eslint-disable max-classes-per-file */ +import { QueryTypes, Sequelize, Transaction } from 'sequelize' + +import { type QueryExecutor, formatQuery } from '@crowd/data-access-layer/src/queryExecutor' + +/** Sequelize-backed QueryExecutor for legacy backend repositories. */ +export class SequelizeQueryExecutor implements QueryExecutor { + constructor( + private readonly sequelize: Sequelize, + private readonly noTransaction = false, + ) {} + + protected prepareOptions(options: any): any { + // When noTransaction=true, explicitly opt out of any CLS or implicit + // transaction binding — used for background/fire-and-forget work that + // must not inherit a parent request's transaction. + return this.noTransaction ? { ...options, transaction: null } : options + } + + select(query: string, params?: object): Promise { + return this.sequelize.query( + formatQuery(query, params), + this.prepareOptions({ + type: QueryTypes.SELECT, + }), + ) + } + + async selectNone(query: string, params?: object): Promise { + const result = await this.sequelize.query( + formatQuery(query, params), + this.prepareOptions({ + type: QueryTypes.SELECT, + }), + ) + if (result.length > 0) { + throw new Error('Expected no rows') + } + } + + async selectOneOrNone(query: string, params?: object): Promise { + const result = await this.sequelize.query( + formatQuery(query, params), + this.prepareOptions({ + type: QueryTypes.SELECT, + }), + ) + if (result.length > 1) { + throw new Error('Expected at most one row') + } + + return result[0] + } + + async selectOne(query: string, params?: object): Promise { + const result: any = await this.sequelize.query( + formatQuery(query, params), + this.prepareOptions({ + type: QueryTypes.SELECT, + }), + ) + if (result.length !== 1) { + throw new Error('Expected exactly one row') + } + + return result[0] + } + + async result(query: string, params?: object): Promise { + const [, result] = await this.sequelize.query( + formatQuery(query, params), + this.prepareOptions({}), + ) + if (typeof result === 'number') { + return result + } + + if (typeof result === 'object' && 'rowCount' in result) { + return (result as any).rowCount + } + + return result + } + + async tx(fn: (tx: QueryExecutor) => Promise): Promise { + const transaction = await this.sequelize.transaction() + + try { + const res = await fn(new TransactionalSequelizeQueryExecutor(this.sequelize, transaction)) + await transaction.commit() + return res + } catch (err) { + await transaction.rollback() + throw err + } + } +} + +export class TransactionalSequelizeQueryExecutor extends SequelizeQueryExecutor { + constructor( + sequelize: Sequelize, + private readonly transaction: Transaction, + ) { + super(sequelize) + } + + protected prepareOptions(options: any): any { + return { + ...super.prepareOptions(options), + transaction: this.transaction, + } + } +} + +export function optionsQx(options: any): QueryExecutor { + const seq = options.database.sequelize + if (options.transaction) { + return new TransactionalSequelizeQueryExecutor(seq, options.transaction) + } + + return new SequelizeQueryExecutor(seq) +} + +/** + * Creates a QueryExecutor for fire-and-forget background work. + * Always runs outside any transaction — safe to use after the caller's + * request transaction has been committed. + */ +export function optionsBgQx(options: any): QueryExecutor { + return new SequelizeQueryExecutor(options.database.sequelize, true) +} diff --git a/backend/src/services/member/memberIdentityService.ts b/backend/src/services/member/memberIdentityService.ts index a039a43a25..48569bf93b 100644 --- a/backend/src/services/member/memberIdentityService.ts +++ b/backend/src/services/member/memberIdentityService.ts @@ -3,7 +3,7 @@ import lodash from 'lodash' import { captureApiChange, memberEditIdentitiesAction } from '@crowd/audit-logs' import { Error404, Error409 } from '@crowd/common' -import { createMemberIdentity, findIdentitiesForMembers, optionsQx } from '@crowd/data-access-layer' +import { createMemberIdentity, findIdentitiesForMembers } from '@crowd/data-access-layer' import { deleteMemberIdentity, fetchMemberIdentities, @@ -17,6 +17,7 @@ import { IMemberIdentity, NewMemberIdentity } from '@crowd/types' import { IRepositoryOptions } from '@/database/repositories/IRepositoryOptions' import SequelizeRepository from '@/database/repositories/sequelizeRepository' +import { optionsQx } from '@/database/sequelizeQueryExecutor' import { IServiceOptions } from '../IServiceOptions' diff --git a/backend/src/services/memberService.ts b/backend/src/services/memberService.ts index 6e8f82bcee..052e4f3b33 100644 --- a/backend/src/services/memberService.ts +++ b/backend/src/services/memberService.ts @@ -21,7 +21,7 @@ import { insertMemberSegmentAggregates, queryMembersAdvanced, } from '@crowd/data-access-layer/src/members' -import { QueryExecutor, optionsBgQx, optionsQx } from '@crowd/data-access-layer/src/queryExecutor' +import { QueryExecutor } from '@crowd/data-access-layer/src/queryExecutor' import { decrementMemberMergeSuggestionCounts, fetchManySegments, @@ -41,6 +41,8 @@ import { SyncMode, } from '@crowd/types' +import { optionsBgQx, optionsQx } from '@/database/sequelizeQueryExecutor' + import MemberAttributeSettingsRepository from '../database/repositories/memberAttributeSettingsRepository' import MemberRepository from '../database/repositories/memberRepository' import { MergeActionsRepository } from '../database/repositories/mergeActionsRepository' diff --git a/backend/src/services/organizationService.ts b/backend/src/services/organizationService.ts index 3fe7e38cf7..cd794629bc 100644 --- a/backend/src/services/organizationService.ts +++ b/backend/src/services/organizationService.ts @@ -11,7 +11,6 @@ import { unmergeRoles } from '@crowd/common_services' import { addMemberRole, moveMembersBetweenOrganizations, - optionsQx, removeMemberRole, } from '@crowd/data-access-layer' import { hasLfxMembership } from '@crowd/data-access-layer/src/lfx_memberships' @@ -54,6 +53,7 @@ import { import { IRepositoryOptions } from '@/database/repositories/IRepositoryOptions' import MemberOrganizationRepository from '@/database/repositories/memberOrganizationRepository' +import { optionsQx } from '@/database/sequelizeQueryExecutor' import getObjectWithoutKey from '@/utils/getObjectWithoutKey' import { MergeActionsRepository } from '../database/repositories/mergeActionsRepository' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e6565a5487..9b2994d141 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2268,9 +2268,6 @@ importers: '@types/node': specifier: ^20.8.2 version: 20.12.7 - sequelize: - specifier: 6.37.8 - version: 6.37.8(pg@8.11.5) typescript: specifier: ^5.6.3 version: 5.6.3 @@ -10609,8 +10606,8 @@ snapshots: dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sso-oidc': 3.572.0(@aws-sdk/client-sts@3.572.0) - '@aws-sdk/client-sts': 3.572.0 + '@aws-sdk/client-sso-oidc': 3.572.0 + '@aws-sdk/client-sts': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0) '@aws-sdk/core': 3.572.0 '@aws-sdk/credential-provider-node': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0)(@aws-sdk/client-sts@3.572.0) '@aws-sdk/middleware-host-header': 3.567.0 @@ -10804,11 +10801,11 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sso-oidc@3.572.0(@aws-sdk/client-sts@3.572.0)': + '@aws-sdk/client-sso-oidc@3.572.0': dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sts': 3.572.0 + '@aws-sdk/client-sts': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0) '@aws-sdk/core': 3.572.0 '@aws-sdk/credential-provider-node': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0)(@aws-sdk/client-sts@3.572.0) '@aws-sdk/middleware-host-header': 3.567.0 @@ -10847,7 +10844,6 @@ snapshots: '@smithy/util-utf8': 2.3.0 tslib: 2.6.2 transitivePeerDependencies: - - '@aws-sdk/client-sts' - aws-crt '@aws-sdk/client-sso@3.556.0': @@ -11023,11 +11019,11 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sts@3.572.0': + '@aws-sdk/client-sts@3.572.0(@aws-sdk/client-sso-oidc@3.572.0)': dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sso-oidc': 3.572.0(@aws-sdk/client-sts@3.572.0) + '@aws-sdk/client-sso-oidc': 3.572.0 '@aws-sdk/core': 3.572.0 '@aws-sdk/credential-provider-node': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0)(@aws-sdk/client-sts@3.572.0) '@aws-sdk/middleware-host-header': 3.567.0 @@ -11066,6 +11062,7 @@ snapshots: '@smithy/util-utf8': 2.3.0 tslib: 2.6.2 transitivePeerDependencies: + - '@aws-sdk/client-sso-oidc' - aws-crt '@aws-sdk/client-sts@3.985.0': @@ -11231,7 +11228,7 @@ snapshots: '@aws-sdk/credential-provider-ini@3.572.0(@aws-sdk/client-sso-oidc@3.572.0)(@aws-sdk/client-sts@3.572.0)': dependencies: - '@aws-sdk/client-sts': 3.572.0 + '@aws-sdk/client-sts': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0) '@aws-sdk/credential-provider-env': 3.568.0 '@aws-sdk/credential-provider-process': 3.572.0 '@aws-sdk/credential-provider-sso': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0) @@ -11408,7 +11405,7 @@ snapshots: '@aws-sdk/credential-provider-web-identity@3.568.0(@aws-sdk/client-sts@3.572.0)': dependencies: - '@aws-sdk/client-sts': 3.572.0 + '@aws-sdk/client-sts': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0) '@aws-sdk/types': 3.567.0 '@smithy/property-provider': 2.2.0 '@smithy/types': 2.12.0 @@ -11720,7 +11717,7 @@ snapshots: '@aws-sdk/token-providers@3.572.0(@aws-sdk/client-sso-oidc@3.572.0)': dependencies: - '@aws-sdk/client-sso-oidc': 3.572.0(@aws-sdk/client-sts@3.572.0) + '@aws-sdk/client-sso-oidc': 3.572.0 '@aws-sdk/types': 3.567.0 '@smithy/property-provider': 2.2.0 '@smithy/shared-ini-file-loader': 2.4.0 diff --git a/services/libs/data-access-layer/package.json b/services/libs/data-access-layer/package.json index bb5d64a311..16579c5b0b 100644 --- a/services/libs/data-access-layer/package.json +++ b/services/libs/data-access-layer/package.json @@ -34,7 +34,6 @@ }, "devDependencies": { "@types/node": "^20.8.2", - "sequelize": "6.37.8", "typescript": "^5.6.3" } } diff --git a/services/libs/data-access-layer/src/queryExecutor.ts b/services/libs/data-access-layer/src/queryExecutor.ts index a08261dea0..ed943973c4 100644 --- a/services/libs/data-access-layer/src/queryExecutor.ts +++ b/services/libs/data-access-layer/src/queryExecutor.ts @@ -1,131 +1,2 @@ -import { QueryTypes, Sequelize, Transaction } from 'sequelize' - -import { type QueryExecutor, formatQuery } from '@crowd/database' - export { PgPromiseQueryExecutor, dbStoreQx, formatQuery, pgpQx, repoQx } from '@crowd/database' export type { QueryExecutor } from '@crowd/database' - -/* eslint-disable @typescript-eslint/no-explicit-any */ - -/** Sequelize-backed QueryExecutor for legacy backend repositories. */ -export class SequelizeQueryExecutor implements QueryExecutor { - constructor( - private readonly sequelize: Sequelize, - private readonly noTransaction = false, - ) {} - - protected prepareOptions(options: any): any { - // When noTransaction=true, explicitly opt out of any CLS or implicit - // transaction binding — used for background/fire-and-forget work that - // must not inherit a parent request's transaction. - return this.noTransaction ? { ...options, transaction: null } : options - } - - select(query: string, params?: object): Promise { - return this.sequelize.query( - formatQuery(query, params), - this.prepareOptions({ - type: QueryTypes.SELECT, - }), - ) - } - async selectNone(query: string, params?: object): Promise { - const result = await this.sequelize.query( - formatQuery(query, params), - this.prepareOptions({ - type: QueryTypes.SELECT, - }), - ) - if (result.length > 0) { - throw new Error('Expected no rows') - } - } - async selectOneOrNone(query: string, params?: object): Promise { - const result = await this.sequelize.query( - formatQuery(query, params), - this.prepareOptions({ - type: QueryTypes.SELECT, - }), - ) - if (result.length > 1) { - throw new Error('Expected at most one row') - } - - return result[0] - } - async selectOne(query: string, params?: object): Promise { - const result: any = await this.sequelize.query( - formatQuery(query, params), - this.prepareOptions({ - type: QueryTypes.SELECT, - }), - ) - if (result.length !== 1) { - throw new Error('Expected exactly one row') - } - - return result[0] - } - async result(query: string, params?: object): Promise { - const [, result] = await this.sequelize.query( - formatQuery(query, params), - this.prepareOptions({}), - ) - if (typeof result === 'number') { - return result - } - - if (typeof result === 'object' && 'rowCount' in result) { - return (result as any).rowCount - } - - return result - } - - async tx(fn: (tx: QueryExecutor) => Promise): Promise { - const transaction = await this.sequelize.transaction() - - try { - const res = await fn(new TransactionalSequelizeQueryExecutor(this.sequelize, transaction)) - await transaction.commit() - return res - } catch (err) { - await transaction.rollback() - throw err - } - } -} - -export class TransactionalSequelizeQueryExecutor extends SequelizeQueryExecutor { - constructor( - sequelize: Sequelize, - private readonly transaction: Transaction, - ) { - super(sequelize) - } - - protected prepareOptions(options: any): any { - return { - ...super.prepareOptions(options), - transaction: this.transaction, - } - } -} - -export function optionsQx(options: any): QueryExecutor { - const seq = options.database.sequelize - if (options.transaction) { - return new TransactionalSequelizeQueryExecutor(seq, options.transaction) - } - - return new SequelizeQueryExecutor(seq) -} - -/** - * Creates a QueryExecutor for fire-and-forget background work. - * Always runs outside any transaction — safe to use after the caller's - * request transaction has been committed. - */ -export function optionsBgQx(options: any): QueryExecutor { - return new SequelizeQueryExecutor(options.database.sequelize, true) -}