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
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
-- Backup settings before any modifications
CREATE TABLE integration.integrations_settings_backup_02_28_2026 AS
SELECT id, settings FROM integrations;

-- Strip repos from orgs in settings for github and github-nango integrations
-- Repos now live in public.repositories table and are populated into API responses
-- via the compatibility layer in integrationRepository._populateRelations
UPDATE integrations
SET settings = jsonb_set(
settings,
'{orgs}',
(
SELECT coalesce(jsonb_agg(
org - 'repos'
), '[]'::jsonb)
FROM jsonb_array_elements(settings->'orgs') org
)
)
WHERE platform IN ('github', 'github-nango')
AND settings->'orgs' IS NOT NULL
AND "deletedAt" IS NULL
AND status != 'mapping';

-- Also clean up top-level repos/unavailableRepos if present
UPDATE integrations
SET settings = settings - 'repos' - 'unavailableRepos'
WHERE platform IN ('github', 'github-nango')
AND (settings ? 'repos' OR settings ? 'unavailableRepos')
AND "deletedAt" IS NULL
AND status != 'mapping';
65 changes: 60 additions & 5 deletions backend/src/database/repositories/integrationRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -585,26 +585,81 @@ class IntegrationRepository {

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

// For github-nango integrations, populate settings.nangoMapping from the dedicated table
// so the API contract remains unchanged for frontend consumers
// For github-nango integrations, populate settings.nangoMapping from dedicated table
if (output.platform === PlatformType.GITHUB_NANGO) {
const rows = await record.sequelize.query(
const nangoRows = await record.sequelize.query(
`SELECT "connectionId", owner, "repoName" FROM integration.nango_mapping WHERE "integrationId" = :integrationId`,
{
replacements: { integrationId: output.id },
type: QueryTypes.SELECT,
},
)

if (rows.length > 0) {
if (nangoRows.length > 0) {
const nangoMapping: Record<string, { owner: string; repoName: string }> = {}
for (const row of rows as { connectionId: string; owner: string; repoName: string }[]) {
for (const row of nangoRows as {
connectionId: string
owner: string
repoName: string
}[]) {
nangoMapping[row.connectionId] = { owner: row.owner, repoName: row.repoName }
}
output.settings = { ...output.settings, nangoMapping }
}
}

// For both github and github-nango, populate orgs[].repos from repositories table
if (
(output.platform === PlatformType.GITHUB || output.platform === PlatformType.GITHUB_NANGO) &&
output.settings?.orgs?.length > 0
) {
const repoRows = (await record.sequelize.query(
`SELECT url, split_part(url, '/', -1) as name, split_part(url, '/', -2) as owner, "forkedFrom", "updatedAt"
FROM public.repositories
WHERE "sourceIntegrationId" = :integrationId AND "deletedAt" IS NULL
ORDER BY url`,
{
replacements: { integrationId: output.id },
type: QueryTypes.SELECT,
},
)) as {
url: string
name: string
owner: string
forkedFrom: string | null
updatedAt: string
}[]

// Only overwrite orgs[].repos from the repositories table if there are rows.
// During the 'mapping' phase (legacy github connect), repos live in settings
// before being written to the repositories table via mapGithubRepos.
if (repoRows.length > 0) {
const reposByOwner: Record<string, typeof repoRows> = {}
for (const repo of repoRows) {
if (!reposByOwner[repo.owner]) reposByOwner[repo.owner] = []
reposByOwner[repo.owner].push(repo)
}

output.settings = {
...output.settings,
orgs: output.settings.orgs.map((org) => ({
...org,
repos: (reposByOwner[org.name] || []).map((r) => ({
url: r.url,
name: r.name,
owner: r.owner,
forkedFrom: r.forkedFrom,
updatedAt: r.updatedAt,
})),
})),
}
}

// Strip legacy top-level keys that may still exist in the DB column
delete output.settings.repos
delete output.settings.unavailableRepos
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicated repo-population logic across two layers

Low Severity

_populateRelations in integrationRepository.ts and populateGithubSettingsWithRepos in repositories/index.ts contain nearly identical logic: query repos from public.repositories, group by owner, and merge into settings.orgs[].repos. They differ only in ORM (Sequelize vs pg-promise) and in whether legacy keys are stripped. This duplication risks future divergence — a bug fix or schema change applied to one but not the other.

Additional Locations (1)

Fix in Cursor Fix in Web


return output
}
}
Expand Down
30 changes: 11 additions & 19 deletions backend/src/services/collectionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
} from '@crowd/data-access-layer/src/collections'
import { fetchIntegrationsForSegment } from '@crowd/data-access-layer/src/integrations'
import { QueryFilter } from '@crowd/data-access-layer/src/query'
import { getReposForGithubIntegration } from '@crowd/data-access-layer/src/repositories'
import {
ICreateRepositoryGroup,
IRepositoryGroup,
Expand Down Expand Up @@ -533,13 +534,8 @@ export class CollectionService extends LoggerBase {
return listRepositoryGroups(qx, { insightsProjectId })
}

static isSingleRepoOrg(orgs: GithubIntegrationSettings['orgs']): boolean {
return (
Array.isArray(orgs) &&
orgs.length === 1 &&
Array.isArray(orgs[0]?.repos) &&
orgs[0].repos.length === 1
)
static isSingleRepoOrg(orgs: GithubIntegrationSettings['orgs'], repoCount: number): boolean {
return Array.isArray(orgs) && orgs.length === 1 && repoCount === 1
}

/**
Expand Down Expand Up @@ -613,13 +609,12 @@ export class CollectionService extends LoggerBase {
const settings = githubIntegration.settings as GithubIntegrationSettings

// The orgs must have at least one repo
if (
!settings?.orgs ||
!Array.isArray(settings.orgs) ||
settings.orgs.length === 0 ||
!Array.isArray(settings.orgs[0].repos) ||
settings.orgs[0].repos.length === 0
) {
if (!settings?.orgs || !Array.isArray(settings.orgs) || settings.orgs.length === 0) {
return null
}

const repos = await getReposForGithubIntegration(qx, githubIntegration.id)
if (repos.length === 0) {
return null
}

Expand All @@ -633,11 +628,8 @@ export class CollectionService extends LoggerBase {
return null
}

const details = CollectionService.isSingleRepoOrg(settings.orgs)
? await GithubIntegrationService.findRepoDetails(
mainOrg.name,
settings.orgs[0].repos[0].name,
)
const details = CollectionService.isSingleRepoOrg(settings.orgs, repos.length)
? await GithubIntegrationService.findRepoDetails(mainOrg.name, repos[0].name)
: {
...(await GithubIntegrationService.findOrgDetails(mainOrg.name)),
topics: mainOrg.topics,
Expand Down
Loading