Skip to content

Commit 9f6194f

Browse files
authored
chore: remove repos from integrations.settings column for github and github-nango integrations (CM-1005) (#3882)
Signed-off-by: Uroš Marolt <uros@marolt.me>
1 parent 14d3f71 commit 9f6194f

20 files changed

Lines changed: 513 additions & 292 deletions

File tree

backend/src/database/migrations/U1772043927__removeReposFromGithubSettings.sql

Whitespace-only changes.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
-- Backup settings before any modifications
2+
CREATE TABLE integration.integrations_settings_backup_02_28_2026 AS
3+
SELECT id, settings FROM integrations;
4+
5+
-- Strip repos from orgs in settings for github and github-nango integrations
6+
-- Repos now live in public.repositories table and are populated into API responses
7+
-- via the compatibility layer in integrationRepository._populateRelations
8+
UPDATE integrations
9+
SET settings = jsonb_set(
10+
settings,
11+
'{orgs}',
12+
(
13+
SELECT coalesce(jsonb_agg(
14+
org - 'repos'
15+
), '[]'::jsonb)
16+
FROM jsonb_array_elements(settings->'orgs') org
17+
)
18+
)
19+
WHERE platform IN ('github', 'github-nango')
20+
AND settings->'orgs' IS NOT NULL
21+
AND "deletedAt" IS NULL
22+
AND status != 'mapping';
23+
24+
-- Also clean up top-level repos/unavailableRepos if present
25+
UPDATE integrations
26+
SET settings = settings - 'repos' - 'unavailableRepos'
27+
WHERE platform IN ('github', 'github-nango')
28+
AND (settings ? 'repos' OR settings ? 'unavailableRepos')
29+
AND "deletedAt" IS NULL
30+
AND status != 'mapping';

backend/src/database/repositories/integrationRepository.ts

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import {
99
fetchGlobalIntegrationsStatusCount,
1010
fetchGlobalNotConnectedIntegrations,
1111
fetchGlobalNotConnectedIntegrationsCount,
12+
getNangoMappingsForIntegration,
1213
} from '@crowd/data-access-layer/src/integrations'
14+
import { SequelizeQueryExecutor } from '@crowd/data-access-layer/src/queryExecutor'
15+
import { getReposGroupedByOrg } from '@crowd/data-access-layer/src/repositories'
1316
import { IntegrationRunState, PlatformType } from '@crowd/types'
1417

1518
import SequelizeFilterUtils from '../utils/sequelizeFilterUtils'
@@ -585,24 +588,45 @@ class IntegrationRepository {
585588

586589
const output = record.get({ plain: true })
587590

588-
// For github-nango integrations, populate settings.nangoMapping from the dedicated table
589-
// so the API contract remains unchanged for frontend consumers
591+
const qx = new SequelizeQueryExecutor(record.sequelize)
592+
593+
// For github-nango integrations, populate settings.nangoMapping from dedicated table
590594
if (output.platform === PlatformType.GITHUB_NANGO) {
591-
const rows = await record.sequelize.query(
592-
`SELECT "connectionId", owner, "repoName" FROM integration.nango_mapping WHERE "integrationId" = :integrationId`,
593-
{
594-
replacements: { integrationId: output.id },
595-
type: QueryTypes.SELECT,
596-
},
597-
)
595+
const nangoMapping = await getNangoMappingsForIntegration(qx, output.id)
596+
if (Object.keys(nangoMapping).length > 0) {
597+
output.settings = { ...output.settings, nangoMapping }
598+
}
599+
}
598600

599-
if (rows.length > 0) {
600-
const nangoMapping: Record<string, { owner: string; repoName: string }> = {}
601-
for (const row of rows as { connectionId: string; owner: string; repoName: string }[]) {
602-
nangoMapping[row.connectionId] = { owner: row.owner, repoName: row.repoName }
601+
// For both github and github-nango, populate orgs[].repos from repositories table
602+
if (
603+
(output.platform === PlatformType.GITHUB || output.platform === PlatformType.GITHUB_NANGO) &&
604+
output.settings?.orgs?.length > 0
605+
) {
606+
const reposByOrg = await getReposGroupedByOrg(qx, output.id)
607+
608+
// Only overwrite orgs[].repos from the repositories table if there are rows.
609+
// During the 'mapping' phase (legacy github connect), repos live in settings
610+
// before being written to the repositories table via mapGithubRepos.
611+
if (Object.keys(reposByOrg).length > 0) {
612+
output.settings = {
613+
...output.settings,
614+
orgs: output.settings.orgs.map((org) => ({
615+
...org,
616+
repos: (reposByOrg[org.name] || []).map((r) => ({
617+
url: r.url,
618+
name: r.name,
619+
owner: r.owner,
620+
forkedFrom: r.forkedFrom,
621+
updatedAt: r.updatedAt,
622+
})),
623+
})),
603624
}
604-
output.settings = { ...output.settings, nangoMapping }
605625
}
626+
627+
// Strip legacy top-level keys that may still exist in the DB column
628+
delete output.settings.repos
629+
delete output.settings.unavailableRepos
606630
}
607631

608632
return output

backend/src/services/collectionService.ts

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
} from '@crowd/data-access-layer/src/collections'
2828
import { fetchIntegrationsForSegment } from '@crowd/data-access-layer/src/integrations'
2929
import { QueryFilter } from '@crowd/data-access-layer/src/query'
30+
import { getReposForGithubIntegration } from '@crowd/data-access-layer/src/repositories'
3031
import {
3132
ICreateRepositoryGroup,
3233
IRepositoryGroup,
@@ -533,13 +534,8 @@ export class CollectionService extends LoggerBase {
533534
return listRepositoryGroups(qx, { insightsProjectId })
534535
}
535536

536-
static isSingleRepoOrg(orgs: GithubIntegrationSettings['orgs']): boolean {
537-
return (
538-
Array.isArray(orgs) &&
539-
orgs.length === 1 &&
540-
Array.isArray(orgs[0]?.repos) &&
541-
orgs[0].repos.length === 1
542-
)
537+
static isSingleRepoOrg(orgs: GithubIntegrationSettings['orgs'], repoCount: number): boolean {
538+
return Array.isArray(orgs) && orgs.length === 1 && repoCount === 1
543539
}
544540

545541
/**
@@ -613,13 +609,12 @@ export class CollectionService extends LoggerBase {
613609
const settings = githubIntegration.settings as GithubIntegrationSettings
614610

615611
// The orgs must have at least one repo
616-
if (
617-
!settings?.orgs ||
618-
!Array.isArray(settings.orgs) ||
619-
settings.orgs.length === 0 ||
620-
!Array.isArray(settings.orgs[0].repos) ||
621-
settings.orgs[0].repos.length === 0
622-
) {
612+
if (!settings?.orgs || !Array.isArray(settings.orgs) || settings.orgs.length === 0) {
613+
return null
614+
}
615+
616+
const repos = await getReposForGithubIntegration(qx, githubIntegration.id)
617+
if (repos.length === 0) {
623618
return null
624619
}
625620

@@ -633,11 +628,8 @@ export class CollectionService extends LoggerBase {
633628
return null
634629
}
635630

636-
const details = CollectionService.isSingleRepoOrg(settings.orgs)
637-
? await GithubIntegrationService.findRepoDetails(
638-
mainOrg.name,
639-
settings.orgs[0].repos[0].name,
640-
)
631+
const details = CollectionService.isSingleRepoOrg(settings.orgs, repos.length)
632+
? await GithubIntegrationService.findRepoDetails(mainOrg.name, repos[0].name)
641633
: {
642634
...(await GithubIntegrationService.findOrgDetails(mainOrg.name)),
643635
topics: mainOrg.topics,

0 commit comments

Comments
 (0)