Skip to content

Commit 2529144

Browse files
authored
feat: improve prevent duplication [CM-560] (#3317)
1 parent ec9dff1 commit 2529144

9 files changed

Lines changed: 2333 additions & 187 deletions

File tree

backend/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,9 @@
140140
},
141141
"private": true,
142142
"devDependencies": {
143-
"@babel/core": "^7.21.8",
144-
"@babel/preset-env": "^7.21.5",
145-
"@babel/preset-typescript": "^7.21.5",
143+
"@babel/core": "^7.24.4",
144+
"@babel/preset-env": "^7.24.4",
145+
"@babel/preset-typescript": "^7.24.1",
146146
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
147147
"@types/bunyan": "^1.8.8",
148148
"@types/bunyan-format": "^0.2.5",

backend/src/database/migrations/U1755867534__add_unique_constraint_on_repository.sql

Whitespace-only changes.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS segmentRepositories_repository_uq_idx ON "segmentRepositories" ("repository");

backend/src/database/repositories/segmentRepository.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -976,6 +976,66 @@ class SegmentRepository extends RepositoryBase<
976976

977977
return result
978978
}
979+
980+
async getGithubRepoUrlsMappedToOtherSegments(urls: string[], segmentId: string) {
981+
if (!urls || urls.length === 0) {
982+
return []
983+
}
984+
985+
const transaction = SequelizeRepository.getTransaction(this.options)
986+
const tenantId = this.options.currentTenant.id
987+
988+
const rows = await this.options.database.sequelize.query(
989+
`
990+
select distinct
991+
r."url" as "url"
992+
from
993+
"githubRepos" r
994+
where
995+
r."tenantId" = :tenantId
996+
and r."url" in (:urls)
997+
and r."deletedAt" is null
998+
and r."segmentId" <> :segmentId
999+
`,
1000+
{
1001+
replacements: { tenantId, urls, segmentId },
1002+
type: QueryTypes.SELECT,
1003+
transaction,
1004+
},
1005+
)
1006+
1007+
return rows.map((r) => r.url)
1008+
}
1009+
1010+
async getGitlabRepoUrlsMappedToOtherSegments(urls: string[], segmentId: string) {
1011+
if (!urls || urls.length === 0) {
1012+
return []
1013+
}
1014+
1015+
const transaction = SequelizeRepository.getTransaction(this.options)
1016+
const tenantId = this.options.currentTenant.id
1017+
1018+
const rows = await this.options.database.sequelize.query(
1019+
`
1020+
select distinct
1021+
r."url" as "url"
1022+
from
1023+
"gitlabRepos" r
1024+
where
1025+
r."tenantId" = :tenantId
1026+
and r."url" in (:urls)
1027+
and r."deletedAt" is null
1028+
and r."segmentId" <> :segmentId
1029+
`,
1030+
{
1031+
replacements: { tenantId, urls, segmentId },
1032+
type: QueryTypes.SELECT,
1033+
transaction,
1034+
},
1035+
)
1036+
1037+
return rows.map((r) => r.url)
1038+
}
9791039
}
9801040

9811041
export default SegmentRepository

backend/src/services/collectionService.ts

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ import {
2323
updateCollection,
2424
updateInsightsProject,
2525
} from '@crowd/data-access-layer/src/collections'
26-
import { fetchIntegrationsForSegment } from '@crowd/data-access-layer/src/integrations'
26+
import {
27+
fetchIntegrationById,
28+
fetchIntegrationsForSegment,
29+
removePlainGitHubRepoMapping,
30+
} from '@crowd/data-access-layer/src/integrations'
2731
import { OrganizationField, findOrgById, queryOrgs } from '@crowd/data-access-layer/src/orgs'
2832
import { QueryFilter } from '@crowd/data-access-layer/src/query'
2933
import { findSegmentById } from '@crowd/data-access-layer/src/segments'
@@ -426,7 +430,7 @@ export class CollectionService extends LoggerBase {
426430
}
427431

428432
// Add mapped repositories to GitHub platform
429-
const segmentRepository = new SegmentRepository(this.options)
433+
const segmentRepository = new SegmentRepository({ ...this.options, transaction: tx })
430434
const githubMappedRepos = await segmentRepository.getGithubMappedRepos(segmentId)
431435
const gitlabMappedRepos = await segmentRepository.getGitlabMappedRepos(segmentId)
432436

@@ -634,4 +638,74 @@ export class CollectionService extends LoggerBase {
634638

635639
return result
636640
}
641+
642+
static extractGithubRepoSlug(url: string): any {
643+
const parsedUrl = new URL(url)
644+
const pathname = parsedUrl.pathname
645+
const parts = pathname.split('/').filter(Boolean)
646+
647+
if (parts.length >= 2) {
648+
return `${parts[0]}/${parts[1]}`
649+
}
650+
651+
throw new Error('Invalid GitHub URL format')
652+
}
653+
654+
async findNangoRepositoriesToBeRemoved(integrationId: string): Promise<string[]> {
655+
return SequelizeRepository.withTx(this.options, async (tx) => {
656+
const qx = SequelizeRepository.getQueryExecutor({ ...this.options, transaction: tx })
657+
const integration = await fetchIntegrationById(qx, integrationId)
658+
659+
if (!integration || integration.platform !== PlatformType.GITHUB_NANGO) {
660+
return []
661+
}
662+
663+
const repoSlugs = new Set<string>()
664+
const settings = integration.settings as any
665+
const reposToBeRemoved = []
666+
667+
if (!settings.nangoMapping) {
668+
return []
669+
}
670+
671+
if (settings.orgs) {
672+
for (const org of settings.orgs) {
673+
for (const repo of org.repos ?? []) {
674+
repoSlugs.add(CollectionService.extractGithubRepoSlug(repo.url))
675+
}
676+
}
677+
}
678+
679+
if (settings.repos) {
680+
for (const repo of settings.repos) {
681+
repoSlugs.add(CollectionService.extractGithubRepoSlug(repo.url))
682+
}
683+
}
684+
// determine which connections to delete if needed
685+
for (const mappedRepo of Object.values(settings.nangoMapping) as {
686+
owner: string
687+
repoName: string
688+
}[]) {
689+
if (!repoSlugs.has(`${mappedRepo.owner}/${mappedRepo.repoName}`)) {
690+
reposToBeRemoved.push(`https://github.com/${mappedRepo.owner}/${mappedRepo.repoName}`)
691+
}
692+
}
693+
694+
return reposToBeRemoved
695+
})
696+
}
697+
698+
async unmapGithubRepo(integrationId: string, repo: string): Promise<void> {
699+
return SequelizeRepository.withTx(this.options, async (tx) => {
700+
const qx = SequelizeRepository.getQueryExecutor({ ...this.options, transaction: tx })
701+
await removePlainGitHubRepoMapping(qx, this.options.redis, integrationId, repo)
702+
})
703+
}
704+
705+
async unmapGitlabRepo(integrationId: string, repo: string): Promise<void> {
706+
return SequelizeRepository.withTx(this.options, async (tx) => {
707+
const qx = SequelizeRepository.getQueryExecutor({ ...this.options, transaction: tx })
708+
await removePlainGitHubRepoMapping(qx, this.options.redis, integrationId, repo)
709+
})
710+
}
637711
}

0 commit comments

Comments
 (0)