Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions backend/src/services/organizationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@ import {
organizationMergeAction,
organizationUnmergeAction,
} from '@crowd/audit-logs'
import { Error400, Error404, Error409, mergeObjects, normalizeHostname } from '@crowd/common'
import {
Error400,
Error404,
Error409,
generateOrganizationNameVariants,
mergeObjects,
normalizeHostname,
} from '@crowd/common'
import { unmergeRoles } from '@crowd/common_services'
import {
addMemberRole,
Expand All @@ -31,7 +38,7 @@ import {
} from '@crowd/data-access-layer/src/organizations'
import {
decrementOrganizationMergeSuggestionCounts,
findLfSegmentByName,
findManyLfSegmentsByNames,
getOrganizationsCommonProjectGroupSegmentIds,
} from '@crowd/data-access-layer/src/segments'
import { LoggerBase } from '@crowd/logging'
Expand Down Expand Up @@ -927,8 +934,11 @@ export default class OrganizationService extends LoggerBase {
if (data.displayName) {
// Block organization affiliation if a LF segment (project, subproject, or project group)
// has the same name as the organization when creating one.
const lfSegment = await findLfSegmentByName(qx, data.displayName)
if (lfSegment) {
const lfSegments = await findManyLfSegmentsByNames(
qx,
generateOrganizationNameVariants(data.displayName),
)
if (lfSegments.length > 0) {
this.log.info(
{ displayName: data.displayName },
'Found segment with the same name as the organization, blocking affiliation!',
Expand Down
9 changes: 6 additions & 3 deletions backend/src/services/segmentService.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Transaction } from 'sequelize'

import { Error400, validateNonLfSlug } from '@crowd/common'
import { Error400, generateOrganizationNameVariants, validateNonLfSlug } from '@crowd/common'
import {
QueryExecutor,
findOrganizationsByName,
findManyOrganizationsByNames,
updateOrganization,
} from '@crowd/data-access-layer'
import { ICreateInsightsProject, findBySlug } from '@crowd/data-access-layer/src/collections'
Expand Down Expand Up @@ -727,7 +727,10 @@ export default class SegmentService extends LoggerBase {
})

// Check if there is an existing organization with segment name
const organizations = await findOrganizationsByName(qx, segmentName)
const organizations = await findManyOrganizationsByNames(
qx,
generateOrganizationNameVariants(segmentName),
)

if (organizations.length === 0) {
return []
Expand Down
1 change: 1 addition & 0 deletions services/libs/common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export * from './rawQueryParser'
export * from './byteLength'
export * from './domain'
export * from './displayName'
export * from './organization'
export * from './country'
export * from './jira'
export * from './email'
Expand Down
59 changes: 59 additions & 0 deletions services/libs/common/src/organization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
export function generateOrganizationNameVariants(name: string): string[] {
const exact = name.trim().toLowerCase().replace(/\s+/g, ' ')
if (!exact) {
return []
}

const variants = new Set<string>([exact])
const add = (value: string) => {
const normalized = value.trim().toLowerCase().replace(/\s+/g, ' ')
if (normalized) {
variants.add(normalized)
}
}

let withoutParens = exact
if (exact.endsWith(')')) {
const open = exact.lastIndexOf('(')
if (open !== -1 && !exact.slice(open + 1, -1).includes(')')) {
withoutParens = exact.slice(0, open).trimEnd()
}
}
if (withoutParens !== exact && withoutParens.length >= 8) {
add(withoutParens)
}

for (const value of [...variants]) {
if (value.startsWith('the ') && value.slice(4).length >= 8) {
add(value.slice(4))
}
}

for (const value of [...variants]) {
for (const suffix of ['project', 'foundation', 'initiative']) {
const token = ` ${suffix}`
if (value.endsWith(token)) {
const base = value.slice(0, -token.length).trim()
if (base.length >= 6) {
add(base)
}
} else if (value.length >= 4 && !value.includes('(')) {
add(`${value}${token}`)
}
}
}
Comment on lines +32 to +44

for (const value of [...variants]) {
if (value.includes('-')) {
add(value.replace(/-/g, ' '))
}
if (value.includes(' ')) {
add(value.replace(/ /g, '-'))
}
if (value.includes('.')) {
add(value.replace(/\./g, ''))
}
}

return [...variants]
}
33 changes: 18 additions & 15 deletions services/libs/data-access-layer/src/organizations/base.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
DEFAULT_TENANT_ID,
UnrepeatableError,
generateOrganizationNameVariants,
generateUUIDv1,
normalizeHostname,
} from '@crowd/common'
Expand All @@ -18,7 +19,7 @@ import {
} from '@crowd/types'

import { QueryExecutor } from '../queryExecutor'
import { findLfSegmentByName } from '../segments'
import { findManyLfSegmentsByNames } from '../segments'
import { QueryOptions, QueryResult, prepareBulkInsert, queryTable, queryTableById } from '../utils'
import { prepareSelectColumns } from '../utils'

Expand Down Expand Up @@ -131,24 +132,23 @@ export async function findOrgsByIds(
return results
}

export async function findOrganizationsByName(
export async function findManyOrganizationsByNames(
qx: QueryExecutor,
name: string,
options: { limit?: number } = {},
names: string[],
): Promise<IDbOrganization[]> {
const { limit } = options
const normalized = names.map((name) => name.trim().toLowerCase()).filter(Boolean)
if (normalized.length === 0) {
return []
}

return qx.select(
`
select ${prepareSelectColumns(ORG_SELECT_COLUMNS, 'o')}
from organizations o
where lower(trim(o."displayName")) = lower(trim($(name)))
${limit !== undefined ? 'limit $(limit)' : ''}
where o."deletedAt" is null
and trim(lower(o."displayName")) in ($(names:csv))
`,
{
name,
limit,
},
{ names: normalized },
)
}

Expand Down Expand Up @@ -584,9 +584,9 @@ export async function findOrCreateOrganization(

if (!existing) {
const organizations = await logExecutionTimeV2(
async () => findOrganizationsByName(qe, data.displayName, { limit: 1 }),
async () => findManyOrganizationsByNames(qe, [data.displayName]),
log,
'organizationService -> findOrCreateOrganization -> findOrganizationsByName',
'organizationService -> findOrCreateOrganization -> findManyOrganizationsByNames',
)

if (organizations.length > 0) {
Expand Down Expand Up @@ -673,8 +673,11 @@ export async function findOrCreateOrganization(

// Block organization affiliation if a segment (project, subproject, or project group)
// has the same name as the organization when creating one.
const lfSegment = await findLfSegmentByName(qe, displayName)
if (lfSegment) {
const lfSegments = await findManyLfSegmentsByNames(
qe,
generateOrganizationNameVariants(displayName),
)
if (lfSegments.length > 0) {
payload.isAffiliationBlocked = true
}

Expand Down
22 changes: 15 additions & 7 deletions services/libs/data-access-layer/src/segments/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,27 @@ export async function findProjectGroupByName(
)
}

export async function findLfSegmentByName(
export async function findManyLfSegmentsByNames(
qx: QueryExecutor,
name: string,
): Promise<SegmentData | null> {
return qx.selectOneOrNone(
names: string[],
): Promise<SegmentData[]> {
const normalized = names.map((name) => name.trim().toLowerCase()).filter(Boolean)
if (normalized.length === 0) {
return []
}

return qx.select(
`
SELECT *
FROM segments
WHERE "isLF" = true
AND trim(lower(name)) = trim(lower($(name)))
LIMIT 1;
AND (
trim(lower(name)) IN ($(names:csv))
OR trim(both FROM regexp_replace(trim(lower(name)), '\\s*\\([^)]*\\)\\s*$', ''))
IN ($(names:csv))
)
`,
{ name },
{ names: normalized },
)
}

Expand Down
Loading