From ef2db93209e49a4c30fccd51507c4263b06138ab Mon Sep 17 00:00:00 2001 From: Yeganathan S <63534555+skwowet@users.noreply.github.com> Date: Mon, 26 Jan 2026 19:37:48 +0530 Subject: [PATCH 01/11] refactor: block organization affiliation when segment name matches --- backend/src/services/organizationService.ts | 8 ++++ backend/src/services/segmentService.ts | 47 ++++++++++++++++++- .../src/organizations/attributesConfig.ts | 1 + .../src/organizations/base.ts | 38 ++++++++++----- .../src/organizations/types.ts | 2 + .../data-access-layer/src/segments/index.ts | 15 ++++++ 6 files changed, 96 insertions(+), 15 deletions(-) diff --git a/backend/src/services/organizationService.ts b/backend/src/services/organizationService.ts index e42dbabfd8..9be50cbbff 100644 --- a/backend/src/services/organizationService.ts +++ b/backend/src/services/organizationService.ts @@ -27,6 +27,7 @@ import { findOrgById, upsertOrgIdentities, } from '@crowd/data-access-layer/src/organizations' +import { findSegmentByName } from '@crowd/data-access-layer/src/segments' import { LoggerBase } from '@crowd/logging' import { WorkflowIdReusePolicy } from '@crowd/temporal' import { @@ -940,6 +941,13 @@ export default class OrganizationService extends LoggerBase { } } + // Block organization affiliation if the name matches a segment name + const segment = await findSegmentByName(qx, record.displayName) + + if (segment && !record.isAffiliationBlocked) { + record = await OrganizationRepository.update(record.id, { isAffiliationBlocked: true }, txOptions) + } + const result = await OrganizationRepository.findById(record.id, txOptions) await SequelizeRepository.commitTransaction(transaction) diff --git a/backend/src/services/segmentService.ts b/backend/src/services/segmentService.ts index c4678a8638..6cdac05410 100644 --- a/backend/src/services/segmentService.ts +++ b/backend/src/services/segmentService.ts @@ -1,7 +1,7 @@ import { Transaction } from 'sequelize' import { Error400, validateNonLfSlug } from '@crowd/common' -import { QueryExecutor } from '@crowd/data-access-layer' +import { findOrganizationsByName, QueryExecutor } from '@crowd/data-access-layer' import { ICreateInsightsProject, findBySlug } from '@crowd/data-access-layer/src/collections' import { buildSegmentActivityTypes, @@ -25,6 +25,7 @@ import SequelizeRepository from '../database/repositories/sequelizeRepository' import { IServiceOptions } from './IServiceOptions' import { CollectionService } from './collectionService' +import OrganizationService from './organizationService' interface UnnestedActivityTypes { [key: string]: any @@ -131,6 +132,10 @@ export default class SegmentService extends LoggerBase { transaction, ) + // Block organization affiliation when the project group name + // matches an existing organization name + await this.blockOrganizationAffiliationIfSegmentNameMatches(data.name, transaction) + await SequelizeRepository.commitTransaction(transaction) return await this.findById(projectGroup.id) @@ -192,6 +197,10 @@ export default class SegmentService extends LoggerBase { transaction, ) + // Block organization affiliation when the project name + // matches an existing organization name + await this.blockOrganizationAffiliationIfSegmentNameMatches(data.name, transaction) + await SequelizeRepository.commitTransaction(transaction) return await this.findById(project.id) @@ -204,7 +213,13 @@ export default class SegmentService extends LoggerBase { async createSubproject(data: SegmentData): Promise { return SequelizeRepository.withTx(this.options, async (tx) => { const qx = SequelizeRepository.getQueryExecutor({ ...this.options, transaction: tx }) - return this.createSubprojectInternal(data, qx, tx) + const subproject = await this.createSubprojectInternal(data, qx, tx) + + // Block organization affiliation when the subproject name + // matches an existing organization name + await this.blockOrganizationAffiliationIfSegmentNameMatches(subproject.name, tx) + + return subproject }) } @@ -638,6 +653,34 @@ export default class SegmentService extends LoggerBase { } } + private async blockOrganizationAffiliationIfSegmentNameMatches( + segmentName: string, + transaction: Transaction, + ): Promise { + const qx = SequelizeRepository.getQueryExecutor({ + ...this.options, + transaction, + }) + + // Check if there is an existing organization with segment name + const organizations = await findOrganizationsByName(qx, segmentName) + + if (organizations.length === 0) { + return + } + + const organizationService = new OrganizationService({ + ...this.options, + transaction, + }) + + for (const o of organizations) { + if (!o.isAffiliationBlocked) { + await organizationService.update(o.id, { isAffiliationBlocked: true }) + } + } + } + /** * Throws an appropriate error message when a segment conflict is detected. * This method dynamically generates error messages based on the existing conflicting segment, diff --git a/services/libs/data-access-layer/src/organizations/attributesConfig.ts b/services/libs/data-access-layer/src/organizations/attributesConfig.ts index 8010ceaeb7..bc771252dd 100644 --- a/services/libs/data-access-layer/src/organizations/attributesConfig.ts +++ b/services/libs/data-access-layer/src/organizations/attributesConfig.ts @@ -17,6 +17,7 @@ export const ORG_DB_FIELDS = [ 'importHash', 'location', 'isTeamOrganization', + 'isAffiliationBlocked', 'type', 'size', 'headline', diff --git a/services/libs/data-access-layer/src/organizations/base.ts b/services/libs/data-access-layer/src/organizations/base.ts index b451244e50..f9a8937f1b 100644 --- a/services/libs/data-access-layer/src/organizations/base.ts +++ b/services/libs/data-access-layer/src/organizations/base.ts @@ -15,6 +15,7 @@ import { } from '@crowd/types' import { QueryExecutor } from '../queryExecutor' +import { findSegmentByName } from '../segments' import { QueryOptions, QueryResult, queryTable, queryTableById } from '../utils' import { prepareSelectColumns } from '../utils' @@ -36,6 +37,7 @@ const ORG_SELECT_COLUMNS = [ 'importHash', 'location', 'isTeamOrganization', + 'isAffiliationBlocked', 'type', 'size', 'headline', @@ -125,17 +127,16 @@ export async function findOrgsByIds( return results } -export async function findOrgByName( +export async function findOrganizationsByName( qx: QueryExecutor, name: string, -): Promise { - const result = await qx.selectOneOrNone( +): Promise { + const result = await qx.select( ` - select ${prepareSelectColumns(ORG_SELECT_COLUMNS, 'o')} - from organizations o - where trim(lower(o."displayName")) = trim(lower($(name))) - limit 1; - `, + select ${prepareSelectColumns(ORG_SELECT_COLUMNS, 'o')} + from organizations o + where trim(lower(o."displayName")) = trim(lower($(name))) + `, { name, }, @@ -382,7 +383,7 @@ export async function insertOrganization( export async function updateOrganization( qe: QueryExecutor, organizationId: string, - data: IDbOrganizationInput, + data: Partial, ): Promise { const columns = Object.keys(data) if (columns.length === 0) { @@ -499,11 +500,15 @@ export async function findOrCreateOrganization( } if (!existing) { - existing = await logExecutionTimeV2( - async () => findOrgByName(qe, data.displayName), + const organizations = await logExecutionTimeV2( + async () => findOrganizationsByName(qe, data.displayName), log, - 'organizationService -> findOrCreateOrganization -> findOrgByName', + 'organizationService -> findOrCreateOrganization -> findOrganizationsByName', ) + + if (organizations.length > 0) { + existing = organizations[0] + } } let id @@ -561,7 +566,7 @@ export async function findOrCreateOrganization( log.trace(`Organization wasn't found via website or identities.`) const displayName = data.displayName ? data.displayName : verifiedIdentities[0].value - const payload = { + const payload: Partial = { displayName, description: data.description, logo: data.logo, @@ -575,6 +580,13 @@ export async function findOrCreateOrganization( founded: data.founded, } + // Block organization affiliation if a segment (project, subproject, or project group) + // has the same name as the organization. + const segment = await findSegmentByName(qe, displayName) + if (segment) { + payload.isAffiliationBlocked = true + } + log.trace({ data, payload }, `Preparing payload to create a new organization!`) const processed = prepareOrganizationData(payload, source) diff --git a/services/libs/data-access-layer/src/organizations/types.ts b/services/libs/data-access-layer/src/organizations/types.ts index 743e40bdc3..b988d71034 100644 --- a/services/libs/data-access-layer/src/organizations/types.ts +++ b/services/libs/data-access-layer/src/organizations/types.ts @@ -16,6 +16,7 @@ export interface IDbOrganization { importHash?: string location?: string isTeamOrganization: boolean + isAffiliationBlocked: boolean type?: string size?: string headline?: string @@ -36,6 +37,7 @@ export interface IDbOrganizationInput { importHash?: string location?: string isTeamOrganization: boolean + isAffiliationBlocked?: boolean type?: string size?: string headline?: string diff --git a/services/libs/data-access-layer/src/segments/index.ts b/services/libs/data-access-layer/src/segments/index.ts index 9934fa622f..d56f02b367 100644 --- a/services/libs/data-access-layer/src/segments/index.ts +++ b/services/libs/data-access-layer/src/segments/index.ts @@ -22,6 +22,21 @@ export async function findProjectGroupByName( ) } +export async function findSegmentByName( + qx: QueryExecutor, + name: string, +): Promise { + return qx.selectOneOrNone( + ` + SELECT * + FROM segments + WHERE trim(lower(name)) = trim(lower($(name))) + LIMIT 1; + `, + { name }, + ) +} + export async function fetchManySegments( qx, segmentIds: string[], From 579f1ccdbeed1cf113365aa8de857ac2577459fa Mon Sep 17 00:00:00 2001 From: Yeganathan S <63534555+skwowet@users.noreply.github.com> Date: Mon, 26 Jan 2026 19:40:25 +0530 Subject: [PATCH 02/11] fix: linter and prettier --- backend/src/services/organizationService.ts | 6 +++++- backend/src/services/segmentService.ts | 10 +++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/backend/src/services/organizationService.ts b/backend/src/services/organizationService.ts index 9be50cbbff..05cfaaa5b6 100644 --- a/backend/src/services/organizationService.ts +++ b/backend/src/services/organizationService.ts @@ -945,7 +945,11 @@ export default class OrganizationService extends LoggerBase { const segment = await findSegmentByName(qx, record.displayName) if (segment && !record.isAffiliationBlocked) { - record = await OrganizationRepository.update(record.id, { isAffiliationBlocked: true }, txOptions) + record = await OrganizationRepository.update( + record.id, + { isAffiliationBlocked: true }, + txOptions, + ) } const result = await OrganizationRepository.findById(record.id, txOptions) diff --git a/backend/src/services/segmentService.ts b/backend/src/services/segmentService.ts index 6cdac05410..57a91e927f 100644 --- a/backend/src/services/segmentService.ts +++ b/backend/src/services/segmentService.ts @@ -1,7 +1,7 @@ import { Transaction } from 'sequelize' import { Error400, validateNonLfSlug } from '@crowd/common' -import { findOrganizationsByName, QueryExecutor } from '@crowd/data-access-layer' +import { QueryExecutor, findOrganizationsByName } from '@crowd/data-access-layer' import { ICreateInsightsProject, findBySlug } from '@crowd/data-access-layer/src/collections' import { buildSegmentActivityTypes, @@ -661,19 +661,19 @@ export default class SegmentService extends LoggerBase { ...this.options, transaction, }) - + // Check if there is an existing organization with segment name const organizations = await findOrganizationsByName(qx, segmentName) - + if (organizations.length === 0) { return } - + const organizationService = new OrganizationService({ ...this.options, transaction, }) - + for (const o of organizations) { if (!o.isAffiliationBlocked) { await organizationService.update(o.id, { isAffiliationBlocked: true }) From 00d1fa8686ad33ac8b53810a3ce124c45456eee0 Mon Sep 17 00:00:00 2001 From: Yeganathan S <63534555+skwowet@users.noreply.github.com> Date: Mon, 26 Jan 2026 20:52:17 +0530 Subject: [PATCH 03/11] fix: trnx and other bugs --- .../repositories/organizationRepository.ts | 1 + backend/src/services/organizationService.ts | 47 ++++---- backend/src/services/segmentService.ts | 103 +++++++++++++----- .../src/organizations/base.ts | 13 ++- .../src/organizations/types.ts | 2 +- 5 files changed, 116 insertions(+), 50 deletions(-) diff --git a/backend/src/database/repositories/organizationRepository.ts b/backend/src/database/repositories/organizationRepository.ts index d1cd51a443..af57c1c9c4 100644 --- a/backend/src/database/repositories/organizationRepository.ts +++ b/backend/src/database/repositories/organizationRepository.ts @@ -1155,6 +1155,7 @@ class OrganizationRepository { OrganizationField.INDUSTRY, OrganizationField.FOUNDED, OrganizationField.IS_TEAM_ORGANIZATION, + OrganizationField.IS_AFFILIATION_BLOCKED, OrganizationField.MANUALLY_CREATED, ]) diff --git a/backend/src/services/organizationService.ts b/backend/src/services/organizationService.ts index 05cfaaa5b6..751e1dea31 100644 --- a/backend/src/services/organizationService.ts +++ b/backend/src/services/organizationService.ts @@ -1081,25 +1081,9 @@ export default class OrganizationService extends LoggerBase { await SequelizeRepository.commitTransaction(tx) - await this.options.temporal.workflow.start('organizationUpdate', { - taskQueue: 'profiles', - workflowId: `${TemporalWorkflowId.ORGANIZATION_UPDATE}/${id}`, - workflowIdReusePolicy: WorkflowIdReusePolicy.WORKFLOW_ID_REUSE_POLICY_TERMINATE_IF_RUNNING, - retry: { - maximumAttempts: 10, - }, - args: [ - { - organization: { - id: record.id, - }, - recalculateAffiliations, - syncOptions: { - doSync: syncToOpensearch, - withAggs: false, - }, - }, - ], + await this.startOrganizationUpdateWorkflow(record.id, { + recalculateAffiliations, + syncToOpensearch, }) return record @@ -1244,4 +1228,29 @@ export default class OrganizationService extends LoggerBase { throw error } } + + async startOrganizationUpdateWorkflow( + organizationId: string, + { syncToOpensearch = false, recalculateAffiliations = false }, + ) { + await this.options.temporal.workflow.start('organizationUpdate', { + taskQueue: 'profiles', + workflowId: `${TemporalWorkflowId.ORGANIZATION_UPDATE}/${organizationId}`, + workflowIdReusePolicy: WorkflowIdReusePolicy.WORKFLOW_ID_REUSE_POLICY_TERMINATE_IF_RUNNING, + retry: { + maximumAttempts: 10, + }, + args: [ + { + organization: { + id: organizationId, + }, + recalculateAffiliations, + syncOptions: { + doSync: syncToOpensearch, + }, + }, + ], + }) + } } diff --git a/backend/src/services/segmentService.ts b/backend/src/services/segmentService.ts index 57a91e927f..5f8ff3f266 100644 --- a/backend/src/services/segmentService.ts +++ b/backend/src/services/segmentService.ts @@ -1,7 +1,7 @@ import { Transaction } from 'sequelize' import { Error400, validateNonLfSlug } from '@crowd/common' -import { QueryExecutor, findOrganizationsByName } from '@crowd/data-access-layer' +import { QueryExecutor, findOrganizationsByName, updateOrganization } from '@crowd/data-access-layer' import { ICreateInsightsProject, findBySlug } from '@crowd/data-access-layer/src/collections' import { buildSegmentActivityTypes, @@ -18,6 +18,7 @@ import { SegmentUpdateData, } from '@crowd/types' +import { applyOrganizationAffiliationPolicyToMembers } from '@crowd/data-access-layer/src/member_organization_affiliation_overrides' import { IRepositoryOptions } from '../database/repositories/IRepositoryOptions' import MemberRepository from '../database/repositories/memberRepository' import SegmentRepository from '../database/repositories/segmentRepository' @@ -132,13 +133,24 @@ export default class SegmentService extends LoggerBase { transaction, ) - // Block organization affiliation when the project group name - // matches an existing organization name - await this.blockOrganizationAffiliationIfSegmentNameMatches(data.name, transaction) + // Block org affiliation if project group name matches an organization name + const orgIds = + await this.blockOrganizationAffiliationIfSegmentNameMatches(data.name, transaction) + await SequelizeRepository.commitTransaction(transaction) - return await this.findById(projectGroup.id) + const organizationService = new OrganizationService(this.options) + + for (const orgId of orgIds) { + // Trigger org update workflow to recalculate affiliations + await organizationService.startOrganizationUpdateWorkflow(orgId, { + syncToOpensearch: true, + recalculateAffiliations: true, + }) + } + + return projectGroup } catch (error) { await SequelizeRepository.rollbackTransaction(transaction) throw error @@ -197,13 +209,23 @@ export default class SegmentService extends LoggerBase { transaction, ) - // Block organization affiliation when the project name - // matches an existing organization name - await this.blockOrganizationAffiliationIfSegmentNameMatches(data.name, transaction) + const organizationService = new OrganizationService(this.options) + // Block org affiliation if project name matches an organization name + const orgIds = + await this.blockOrganizationAffiliationIfSegmentNameMatches(data.name, transaction) + await SequelizeRepository.commitTransaction(transaction) + + for (const orgId of orgIds) { + // Trigger org update workflow to recalculate affiliations + await organizationService.startOrganizationUpdateWorkflow(orgId, { + syncToOpensearch: true, + recalculateAffiliations: true, + }) + } - return await this.findById(project.id) + return project } catch (error) { await SequelizeRepository.rollbackTransaction(transaction) throw error @@ -211,18 +233,42 @@ export default class SegmentService extends LoggerBase { } async createSubproject(data: SegmentData): Promise { - return SequelizeRepository.withTx(this.options, async (tx) => { - const qx = SequelizeRepository.getQueryExecutor({ ...this.options, transaction: tx }) - const subproject = await this.createSubprojectInternal(data, qx, tx) - - // Block organization affiliation when the subproject name - // matches an existing organization name - await this.blockOrganizationAffiliationIfSegmentNameMatches(subproject.name, tx) - - return subproject - }) + const { subproject, orgIds } = await SequelizeRepository.withTx( + this.options, + async (tx) => { + const qx = SequelizeRepository.getQueryExecutor({ + ...this.options, + transaction: tx, + }) + + const subproject = await this.createSubprojectInternal(data, qx, tx) + + // Block org affiliation if subproject name matches an organization name + const orgIds = + await this.blockOrganizationAffiliationIfSegmentNameMatches( + data.name, + tx, + ) + + return { subproject, orgIds } + }, + ) + + if (orgIds?.length) { + const organizationService = new OrganizationService(this.options) + + for (const orgId of orgIds) { + // Trigger org update workflow to recalculate affiliations + await organizationService.startOrganizationUpdateWorkflow(orgId, { + syncToOpensearch: true, + recalculateAffiliations: true, + }) + } + } + + return subproject } - + async createSubprojectInternal( data: SegmentData, qx: QueryExecutor, @@ -656,7 +702,7 @@ export default class SegmentService extends LoggerBase { private async blockOrganizationAffiliationIfSegmentNameMatches( segmentName: string, transaction: Transaction, - ): Promise { + ): Promise { const qx = SequelizeRepository.getQueryExecutor({ ...this.options, transaction, @@ -666,19 +712,22 @@ export default class SegmentService extends LoggerBase { const organizations = await findOrganizationsByName(qx, segmentName) if (organizations.length === 0) { - return + return [] } - const organizationService = new OrganizationService({ - ...this.options, - transaction, - }) + const result: string[] = [] for (const o of organizations) { if (!o.isAffiliationBlocked) { - await organizationService.update(o.id, { isAffiliationBlocked: true }) + const updatedOrgId = await updateOrganization(qx, o.id, { isAffiliationBlocked: true }) + if (updatedOrgId) { + await applyOrganizationAffiliationPolicyToMembers(qx, updatedOrgId, false) + result.push(updatedOrgId) + } } } + + return result } /** diff --git a/services/libs/data-access-layer/src/organizations/base.ts b/services/libs/data-access-layer/src/organizations/base.ts index f9a8937f1b..ebd8b07a50 100644 --- a/services/libs/data-access-layer/src/organizations/base.ts +++ b/services/libs/data-access-layer/src/organizations/base.ts @@ -384,10 +384,10 @@ export async function updateOrganization( qe: QueryExecutor, organizationId: string, data: Partial, -): Promise { +): Promise { const columns = Object.keys(data) if (columns.length === 0) { - return + return null } const updatedAt = new Date() @@ -398,14 +398,21 @@ export async function updateOrganization( update organizations set ${columns.map((c) => `"${c}" = $(${c})`).join(',\n')} where id = $(organizationId) and "updatedAt" <= $(oneMinuteAgo) + returning id; ` - await qe.selectNone(query, { + const result = await qe.selectOneOrNone(query, { ...data, organizationId, updatedAt, oneMinuteAgo, }) + + if (!result) { + return null + } + + return result.id } export async function getTimeseriesOfNewOrganizations( diff --git a/services/libs/data-access-layer/src/organizations/types.ts b/services/libs/data-access-layer/src/organizations/types.ts index b988d71034..9c948f02bb 100644 --- a/services/libs/data-access-layer/src/organizations/types.ts +++ b/services/libs/data-access-layer/src/organizations/types.ts @@ -16,7 +16,7 @@ export interface IDbOrganization { importHash?: string location?: string isTeamOrganization: boolean - isAffiliationBlocked: boolean + isAffiliationBlocked?: boolean type?: string size?: string headline?: string From aa4f0ab1e94ba4a14e98b6ab022893bde0ec33d4 Mon Sep 17 00:00:00 2001 From: Yeganathan S <63534555+skwowet@users.noreply.github.com> Date: Mon, 26 Jan 2026 21:00:34 +0530 Subject: [PATCH 04/11] refactor: fix update organization query --- backend/src/services/segmentService.ts | 70 +++++++++---------- .../src/organizations/base.ts | 4 +- 2 files changed, 36 insertions(+), 38 deletions(-) diff --git a/backend/src/services/segmentService.ts b/backend/src/services/segmentService.ts index 5f8ff3f266..c11833e00f 100644 --- a/backend/src/services/segmentService.ts +++ b/backend/src/services/segmentService.ts @@ -1,8 +1,13 @@ import { Transaction } from 'sequelize' import { Error400, validateNonLfSlug } from '@crowd/common' -import { QueryExecutor, findOrganizationsByName, updateOrganization } from '@crowd/data-access-layer' +import { + QueryExecutor, + findOrganizationsByName, + updateOrganization, +} from '@crowd/data-access-layer' import { ICreateInsightsProject, findBySlug } from '@crowd/data-access-layer/src/collections' +import { applyOrganizationAffiliationPolicyToMembers } from '@crowd/data-access-layer/src/member_organization_affiliation_overrides' import { buildSegmentActivityTypes, isSegmentSubproject, @@ -18,7 +23,6 @@ import { SegmentUpdateData, } from '@crowd/types' -import { applyOrganizationAffiliationPolicyToMembers } from '@crowd/data-access-layer/src/member_organization_affiliation_overrides' import { IRepositoryOptions } from '../database/repositories/IRepositoryOptions' import MemberRepository from '../database/repositories/memberRepository' import SegmentRepository from '../database/repositories/segmentRepository' @@ -133,15 +137,16 @@ export default class SegmentService extends LoggerBase { transaction, ) - // Block org affiliation if project group name matches an organization name - const orgIds = - await this.blockOrganizationAffiliationIfSegmentNameMatches(data.name, transaction) - + const orgIds = await this.blockOrganizationAffiliationIfSegmentNameMatches( + data.name, + transaction, + ) + await SequelizeRepository.commitTransaction(transaction) const organizationService = new OrganizationService(this.options) - + for (const orgId of orgIds) { // Trigger org update workflow to recalculate affiliations await organizationService.startOrganizationUpdateWorkflow(orgId, { @@ -212,11 +217,13 @@ export default class SegmentService extends LoggerBase { const organizationService = new OrganizationService(this.options) // Block org affiliation if project name matches an organization name - const orgIds = - await this.blockOrganizationAffiliationIfSegmentNameMatches(data.name, transaction) - + const orgIds = await this.blockOrganizationAffiliationIfSegmentNameMatches( + data.name, + transaction, + ) + await SequelizeRepository.commitTransaction(transaction) - + for (const orgId of orgIds) { // Trigger org update workflow to recalculate affiliations await organizationService.startOrganizationUpdateWorkflow(orgId, { @@ -233,30 +240,23 @@ export default class SegmentService extends LoggerBase { } async createSubproject(data: SegmentData): Promise { - const { subproject, orgIds } = await SequelizeRepository.withTx( - this.options, - async (tx) => { - const qx = SequelizeRepository.getQueryExecutor({ - ...this.options, - transaction: tx, - }) - - const subproject = await this.createSubprojectInternal(data, qx, tx) - - // Block org affiliation if subproject name matches an organization name - const orgIds = - await this.blockOrganizationAffiliationIfSegmentNameMatches( - data.name, - tx, - ) - - return { subproject, orgIds } - }, - ) - + const { subproject, orgIds } = await SequelizeRepository.withTx(this.options, async (tx) => { + const qx = SequelizeRepository.getQueryExecutor({ + ...this.options, + transaction: tx, + }) + + const subproject = await this.createSubprojectInternal(data, qx, tx) + + // Block org affiliation if subproject name matches an organization name + const orgIds = await this.blockOrganizationAffiliationIfSegmentNameMatches(data.name, tx) + + return { subproject, orgIds } + }) + if (orgIds?.length) { const organizationService = new OrganizationService(this.options) - + for (const orgId of orgIds) { // Trigger org update workflow to recalculate affiliations await organizationService.startOrganizationUpdateWorkflow(orgId, { @@ -265,10 +265,10 @@ export default class SegmentService extends LoggerBase { }) } } - + return subproject } - + async createSubprojectInternal( data: SegmentData, qx: QueryExecutor, diff --git a/services/libs/data-access-layer/src/organizations/base.ts b/services/libs/data-access-layer/src/organizations/base.ts index ebd8b07a50..257cf99f77 100644 --- a/services/libs/data-access-layer/src/organizations/base.ts +++ b/services/libs/data-access-layer/src/organizations/base.ts @@ -391,13 +391,12 @@ export async function updateOrganization( } const updatedAt = new Date() - const oneMinuteAgo = new Date(updatedAt.getTime() - 60 * 1000) columns.push('updatedAt') const query = ` update organizations set ${columns.map((c) => `"${c}" = $(${c})`).join(',\n')} - where id = $(organizationId) and "updatedAt" <= $(oneMinuteAgo) + where id = $(organizationId) returning id; ` @@ -405,7 +404,6 @@ export async function updateOrganization( ...data, organizationId, updatedAt, - oneMinuteAgo, }) if (!result) { From b89128855effb395517071cb724019f274d6a0a3 Mon Sep 17 00:00:00 2001 From: Yeganathan S <63534555+skwowet@users.noreply.github.com> Date: Mon, 26 Jan 2026 21:20:33 +0530 Subject: [PATCH 05/11] refactor: streamline subproject creation and return the created subproject by ID --- backend/src/services/segmentService.ts | 61 ++++++++++++++------------ 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/backend/src/services/segmentService.ts b/backend/src/services/segmentService.ts index c11833e00f..95a8e3e5e1 100644 --- a/backend/src/services/segmentService.ts +++ b/backend/src/services/segmentService.ts @@ -155,7 +155,7 @@ export default class SegmentService extends LoggerBase { }) } - return projectGroup + return await this.findById(projectGroup.id) } catch (error) { await SequelizeRepository.rollbackTransaction(transaction) throw error @@ -232,7 +232,7 @@ export default class SegmentService extends LoggerBase { }) } - return project + return await this.findById(project.id) } catch (error) { await SequelizeRepository.rollbackTransaction(transaction) throw error @@ -240,35 +240,42 @@ export default class SegmentService extends LoggerBase { } async createSubproject(data: SegmentData): Promise { - const { subproject, orgIds } = await SequelizeRepository.withTx(this.options, async (tx) => { - const qx = SequelizeRepository.getQueryExecutor({ - ...this.options, - transaction: tx, - }) - - const subproject = await this.createSubprojectInternal(data, qx, tx) - - // Block org affiliation if subproject name matches an organization name - const orgIds = await this.blockOrganizationAffiliationIfSegmentNameMatches(data.name, tx) - - return { subproject, orgIds } - }) - - if (orgIds?.length) { - const organizationService = new OrganizationService(this.options) + const transaction = await SequelizeRepository.createTransaction(this.options) + const qx = SequelizeRepository.getQueryExecutor({ ...this.options, transaction }) - for (const orgId of orgIds) { - // Trigger org update workflow to recalculate affiliations - await organizationService.startOrganizationUpdateWorkflow(orgId, { - syncToOpensearch: true, - recalculateAffiliations: true, - }) + try { + const subproject = await this.createSubprojectInternal( + data, + qx, + transaction, + ) + + const orgIds = + await this.blockOrganizationAffiliationIfSegmentNameMatches( + data.name, + transaction, + ) + + await SequelizeRepository.commitTransaction(transaction) + + if (orgIds?.length) { + const organizationService = new OrganizationService(this.options) + + for (const orgId of orgIds) { + await organizationService.startOrganizationUpdateWorkflow(orgId, { + syncToOpensearch: true, + recalculateAffiliations: true, + }) + } } + + return await this.findById(subproject.id) + } catch (error) { + await SequelizeRepository.rollbackTransaction(transaction) + throw error } - - return subproject } - + async createSubprojectInternal( data: SegmentData, qx: QueryExecutor, From 44ff87758017b190cbf5e4d54fad4f9cdeb0bb47 Mon Sep 17 00:00:00 2001 From: Yeganathan S <63534555+skwowet@users.noreply.github.com> Date: Tue, 27 Jan 2026 13:23:48 +0530 Subject: [PATCH 06/11] refactor: improve organization update workflow to include affiliation recalculation and sync --- backend/src/services/organizationService.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/backend/src/services/organizationService.ts b/backend/src/services/organizationService.ts index 751e1dea31..7febb0c9b2 100644 --- a/backend/src/services/organizationService.ts +++ b/backend/src/services/organizationService.ts @@ -941,6 +941,8 @@ export default class OrganizationService extends LoggerBase { } } + let recalculateAffiliations = false + // Block organization affiliation if the name matches a segment name const segment = await findSegmentByName(qx, record.displayName) @@ -950,16 +952,18 @@ export default class OrganizationService extends LoggerBase { { isAffiliationBlocked: true }, txOptions, ) + + recalculateAffiliations = true } const result = await OrganizationRepository.findById(record.id, txOptions) await SequelizeRepository.commitTransaction(transaction) - if (syncOptions.doSync) { - const searchSyncService = new SearchSyncService(this.options, syncOptions.mode) - await searchSyncService.triggerOrganizationSync(record.id) - } + await this.startOrganizationUpdateWorkflow(record.id, { + recalculateAffiliations, + syncToOpensearch: syncOptions.doSync, + }) return result } catch (error) { From a094b5b87839a87a320dbd858f37ec5066acae1f Mon Sep 17 00:00:00 2001 From: Yeganathan S <63534555+skwowet@users.noreply.github.com> Date: Tue, 27 Jan 2026 13:36:06 +0530 Subject: [PATCH 07/11] refactor: clarify comment on organization affiliation blocking during creation --- services/libs/data-access-layer/src/organizations/base.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/libs/data-access-layer/src/organizations/base.ts b/services/libs/data-access-layer/src/organizations/base.ts index 257cf99f77..45de529216 100644 --- a/services/libs/data-access-layer/src/organizations/base.ts +++ b/services/libs/data-access-layer/src/organizations/base.ts @@ -586,7 +586,7 @@ export async function findOrCreateOrganization( } // Block organization affiliation if a segment (project, subproject, or project group) - // has the same name as the organization. + // has the same name as the organization when creating one. const segment = await findSegmentByName(qe, displayName) if (segment) { payload.isAffiliationBlocked = true From 0f27fcdde61297643daf4b5100160dcefd8a94f9 Mon Sep 17 00:00:00 2001 From: Yeganathan S <63534555+skwowet@users.noreply.github.com> Date: Tue, 27 Jan 2026 13:36:51 +0530 Subject: [PATCH 08/11] fix: linter and prettier --- backend/src/services/segmentService.ts | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/backend/src/services/segmentService.ts b/backend/src/services/segmentService.ts index 95a8e3e5e1..878dc3ba90 100644 --- a/backend/src/services/segmentService.ts +++ b/backend/src/services/segmentService.ts @@ -244,23 +244,18 @@ export default class SegmentService extends LoggerBase { const qx = SequelizeRepository.getQueryExecutor({ ...this.options, transaction }) try { - const subproject = await this.createSubprojectInternal( - data, - qx, + const subproject = await this.createSubprojectInternal(data, qx, transaction) + + const orgIds = await this.blockOrganizationAffiliationIfSegmentNameMatches( + data.name, transaction, ) - - const orgIds = - await this.blockOrganizationAffiliationIfSegmentNameMatches( - data.name, - transaction, - ) - + await SequelizeRepository.commitTransaction(transaction) - + if (orgIds?.length) { const organizationService = new OrganizationService(this.options) - + for (const orgId of orgIds) { await organizationService.startOrganizationUpdateWorkflow(orgId, { syncToOpensearch: true, @@ -268,14 +263,14 @@ export default class SegmentService extends LoggerBase { }) } } - + return await this.findById(subproject.id) } catch (error) { await SequelizeRepository.rollbackTransaction(transaction) throw error } } - + async createSubprojectInternal( data: SegmentData, qx: QueryExecutor, From fb5686b819a1ae51807e2dc83fd258bfd64bf677 Mon Sep 17 00:00:00 2001 From: Yeganathan S <63534555+skwowet@users.noreply.github.com> Date: Tue, 27 Jan 2026 13:41:11 +0530 Subject: [PATCH 09/11] refactor: organization creation logic to block affiliation if segment name matches --- backend/src/services/organizationService.ts | 24 +++++++-------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/backend/src/services/organizationService.ts b/backend/src/services/organizationService.ts index 7febb0c9b2..f54cf1159d 100644 --- a/backend/src/services/organizationService.ts +++ b/backend/src/services/organizationService.ts @@ -914,6 +914,14 @@ export default class OrganizationService extends LoggerBase { await upsertOrgIdentities(qx, record.id, data.identities) } else { + + // Block organization affiliation if a segment (project, subproject, or project group) + // has the same name as the organization when creating one. + const segment = await findSegmentByName(qx, data.displayName) + if (segment) { + data.isAffiliationBlocked = true + } + record = await OrganizationRepository.create(data, txOptions) telemetryTrack( 'Organization created', @@ -941,27 +949,11 @@ export default class OrganizationService extends LoggerBase { } } - let recalculateAffiliations = false - - // Block organization affiliation if the name matches a segment name - const segment = await findSegmentByName(qx, record.displayName) - - if (segment && !record.isAffiliationBlocked) { - record = await OrganizationRepository.update( - record.id, - { isAffiliationBlocked: true }, - txOptions, - ) - - recalculateAffiliations = true - } - const result = await OrganizationRepository.findById(record.id, txOptions) await SequelizeRepository.commitTransaction(transaction) await this.startOrganizationUpdateWorkflow(record.id, { - recalculateAffiliations, syncToOpensearch: syncOptions.doSync, }) From 7bd268ae57f7b0bb8a28705faa31c8e331f1aa19 Mon Sep 17 00:00:00 2001 From: Yeganathan S <63534555+skwowet@users.noreply.github.com> Date: Tue, 27 Jan 2026 13:52:41 +0530 Subject: [PATCH 10/11] fix: edge case in api org create --- backend/src/services/organizationService.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/backend/src/services/organizationService.ts b/backend/src/services/organizationService.ts index f54cf1159d..afdcb6ea5e 100644 --- a/backend/src/services/organizationService.ts +++ b/backend/src/services/organizationService.ts @@ -914,12 +914,18 @@ export default class OrganizationService extends LoggerBase { await upsertOrgIdentities(qx, record.id, data.identities) } else { + if (data.displayName) { + // Block organization affiliation if a segment (project, subproject, or project group) + // has the same name as the organization when creating one. + const segment = await findSegmentByName(qx, data.displayName) + if (segment) { + this.log.info( + { displayName: data.displayName }, + 'Found segment with the same name as the organization, blocking affiliation!', + ) - // Block organization affiliation if a segment (project, subproject, or project group) - // has the same name as the organization when creating one. - const segment = await findSegmentByName(qx, data.displayName) - if (segment) { - data.isAffiliationBlocked = true + data.isAffiliationBlocked = true + } } record = await OrganizationRepository.create(data, txOptions) From 2d15b012843f93998af7725ec784984499ae1bc9 Mon Sep 17 00:00:00 2001 From: Yeganathan S <63534555+skwowet@users.noreply.github.com> Date: Tue, 27 Jan 2026 14:05:52 +0530 Subject: [PATCH 11/11] fix: add optional limit param to findOrganizationsByName for improved query perf --- .../src/organizations/base.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/services/libs/data-access-layer/src/organizations/base.ts b/services/libs/data-access-layer/src/organizations/base.ts index 45de529216..6b4c8f90d0 100644 --- a/services/libs/data-access-layer/src/organizations/base.ts +++ b/services/libs/data-access-layer/src/organizations/base.ts @@ -130,19 +130,22 @@ export async function findOrgsByIds( export async function findOrganizationsByName( qx: QueryExecutor, name: string, + options: { limit?: number } = {}, ): Promise { - const result = await qx.select( + const { limit } = options + + return qx.select( ` - select ${prepareSelectColumns(ORG_SELECT_COLUMNS, 'o')} - from organizations o - where trim(lower(o."displayName")) = trim(lower($(name))) - `, + select ${prepareSelectColumns(ORG_SELECT_COLUMNS, 'o')} + from organizations o + where lower(trim(o."displayName")) = lower(trim($(name))) + ${limit !== undefined ? 'limit $(limit)' : ''} + `, { name, + limit, }, ) - - return result } export async function findOrgByVerifiedDomain( @@ -506,7 +509,7 @@ export async function findOrCreateOrganization( if (!existing) { const organizations = await logExecutionTimeV2( - async () => findOrganizationsByName(qe, data.displayName), + async () => findOrganizationsByName(qe, data.displayName, { limit: 1 }), log, 'organizationService -> findOrCreateOrganization -> findOrganizationsByName', )