Skip to content

Commit 080da1e

Browse files
committed
fix: cursor review
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
1 parent 6d258e9 commit 080da1e

2 files changed

Lines changed: 44 additions & 22 deletions

File tree

backend/src/api/public/v1/dev-stats/getAffiliations.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const DEFAULT_PAGE_SIZE = 50
1616

1717
const bodySchema = z.object({
1818
githubHandles: z
19-
.array(z.string().min(1))
19+
.array(z.string().trim().min(1).toLowerCase())
2020
.min(1)
2121
.max(MAX_HANDLES, `Maximum ${MAX_HANDLES} handles per request`),
2222
})
@@ -31,24 +31,35 @@ export async function getAffiliations(req: Request, res: Response): Promise<void
3131
const { page, pageSize } = validateOrThrow(querySchema, req.query)
3232
const qx = optionsQx(req)
3333

34-
const lowercasedHandles = githubHandles.map((h) => h.toLowerCase())
34+
const offset = (page - 1) * pageSize
35+
if (offset >= githubHandles.length) {
36+
ok(res, {
37+
total: githubHandles.length,
38+
totalFound: 0,
39+
page,
40+
pageSize,
41+
contributorsInPage: 0,
42+
contributors: [],
43+
notFound: githubHandles,
44+
})
45+
return
46+
}
3547

3648
// Step 1: find all verified members across all handles
37-
const allMemberRows = await findMembersByGithubHandles(qx, lowercasedHandles)
49+
const allMemberRows = await findMembersByGithubHandles(qx, githubHandles)
3850

3951
const foundHandles = new Set(allMemberRows.map((r) => r.githubHandle.toLowerCase()))
40-
const notFound = lowercasedHandles.filter((h) => !foundHandles.has(h))
52+
const notFound = githubHandles.filter((h) => !foundHandles.has(h))
4153

42-
const offset = (page - 1) * pageSize
4354
const pageMemberRows = allMemberRows.slice(offset, offset + pageSize)
4455

4556
if (pageMemberRows.length === 0) {
4657
ok(res, {
4758
total: githubHandles.length,
48-
total_found: allMemberRows.length,
59+
totalFound: allMemberRows.length,
4960
page,
5061
pageSize,
51-
contributors_in_page: 0,
62+
contributorsInPage: 0,
5263
contributors: [],
5364
notFound,
5465
})
@@ -80,10 +91,10 @@ export async function getAffiliations(req: Request, res: Response): Promise<void
8091

8192
ok(res, {
8293
total: githubHandles.length,
83-
total_found: allMemberRows.length,
94+
totalFound: allMemberRows.length,
8495
page,
8596
pageSize,
86-
contributors_in_page: contributors.length,
97+
contributorsInPage: contributors.length,
8798
contributors,
8899
notFound,
89100
})

services/libs/data-access-layer/src/affiliations/index.ts

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ function collectBoundaries(datedRows: IWorkRow[]): Date[] {
189189

190190
if (row.dateEnd) {
191191
const afterEnd = startOfDay(row.dateEnd)
192-
afterEnd.setDate(afterEnd.getDate() + 1)
192+
afterEnd.setUTCDate(afterEnd.getUTCDate() + 1)
193193
if (afterEnd <= today) ms.add(afterEnd.getTime())
194194
}
195195
}
@@ -199,9 +199,11 @@ function collectBoundaries(datedRows: IWorkRow[]): Date[] {
199199
.map((t) => new Date(t))
200200
}
201201

202-
function orgsActiveAt(datedRows: IWorkRow[], boundaryDate: Date): IWorkRow[] {
203-
return datedRows.filter((role) => {
204-
const roleStart = startOfDay(role.dateStart ?? '')
202+
function orgsActiveAt(rows: IWorkRow[], boundaryDate: Date): IWorkRow[] {
203+
return rows.filter((role) => {
204+
if (!role.dateStart) return true // undated: active at every boundary
205+
206+
const roleStart = startOfDay(role.dateStart)
205207
const roleEnd = role.dateEnd ? startOfDay(role.dateEnd) : null
206208

207209
// org is active if the boundary date falls within its employment period
@@ -211,13 +213,13 @@ function orgsActiveAt(datedRows: IWorkRow[], boundaryDate: Date): IWorkRow[] {
211213

212214
function startOfDay(date: Date | string): Date {
213215
const d = new Date(date)
214-
d.setHours(0, 0, 0, 0)
216+
d.setUTCHours(0, 0, 0, 0)
215217
return d
216218
}
217219

218220
function dayBefore(date: Date): Date {
219221
const d = new Date(date)
220-
d.setDate(d.getDate() - 1)
222+
d.setUTCDate(d.getUTCDate() - 1)
221223
return d
222224
}
223225

@@ -247,7 +249,7 @@ function closeAffiliationWindow(
247249
/** Iterates boundary intervals and builds non-overlapping affiliation windows. */
248250
function buildTimeline(
249251
memberId: string,
250-
datedRows: IWorkRow[],
252+
allRows: IWorkRow[],
251253
fallbackOrg: IWorkRow | null,
252254
boundaries: Date[],
253255
): IAffiliationPeriod[] {
@@ -256,9 +258,9 @@ function buildTimeline(
256258
let currentWindowStart: Date = null
257259
let uncoveredPeriodStart: Date = null
258260

259-
for (let i = 0; i < boundaries.length - 1; i++) {
261+
for (let i = 0; i < boundaries.length; i++) {
260262
const boundaryDate = boundaries[i]
261-
const activeOrgsAtBoundary = orgsActiveAt(datedRows, boundaryDate)
263+
const activeOrgsAtBoundary = orgsActiveAt(allRows, boundaryDate)
262264

263265
log.info(
264266
{
@@ -420,10 +422,11 @@ function resolveAffiliationsForMember(memberId: string, rows: IWorkRow[]): IAffi
420422
'resolving affiliations',
421423
)
422424

423-
// If one undated org is marked primary, drop all other undated orgs to avoid infinite conflicts
425+
// If one undated work-experience org is marked primary, drop other undated work-experience orgs
426+
// to avoid infinite conflicts. Manual affiliations (segmentId !== null) are never dropped.
424427
const primaryUndated = rows.find((r) => r.isPrimaryWorkExperience && !r.dateStart && !r.dateEnd)
425428
const cleaned = primaryUndated
426-
? rows.filter((r) => r.dateStart || r.id === primaryUndated.id)
429+
? rows.filter((r) => r.segmentId !== null || r.dateStart || r.id === primaryUndated.id)
427430
: rows
428431

429432
if (cleaned.length < rows.length) {
@@ -456,7 +459,14 @@ function resolveAffiliationsForMember(memberId: string, rows: IWorkRow[]): IAffi
456459
)
457460

458461
if (datedRows.length === 0) {
459-
log.debug({ memberId }, 'no dated rows — returning empty affiliations')
462+
if (fallbackOrg) {
463+
log.debug(
464+
{ memberId, fallbackOrg: fallbackOrg.organizationName },
465+
'no dated rows — returning fallback as undated affiliation',
466+
)
467+
return [{ organization: fallbackOrg.organizationName, startDate: null, endDate: null }]
468+
}
469+
log.debug({ memberId }, 'no dated rows and no fallback — returning empty affiliations')
460470
return []
461471
}
462472

@@ -470,7 +480,8 @@ function resolveAffiliationsForMember(memberId: string, rows: IWorkRow[]): IAffi
470480
'collected boundaries',
471481
)
472482

473-
const timeline = buildTimeline(memberId, datedRows, fallbackOrg, boundaries)
483+
// Pass all cleaned rows (not just dated) so undated orgs compete at every boundary (bug 2 fix)
484+
const timeline = buildTimeline(memberId, cleaned, fallbackOrg, boundaries)
474485

475486
log.debug(
476487
{

0 commit comments

Comments
 (0)