Skip to content

Commit 029345a

Browse files
authored
feat: adding username and displayName normalisation (CM-1235) (#4244)
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
1 parent a9fa630 commit 029345a

6 files changed

Lines changed: 93 additions & 34 deletions

File tree

backend/src/api/public/v1/packages/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ export interface OpenVulns {
2424

2525
export interface Steward {
2626
userId: string
27-
name: string
27+
username: string | null
28+
displayName: string | null
2829
role: 'lead' | 'co_steward'
2930
assignedAt: string
3031
}

backend/src/api/public/v1/stewardships/assignSteward.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,32 @@ const paramsSchema = z.object({
1212
id: z.coerce.number().int().positive(),
1313
})
1414

15-
const bodySchema = z.object({
16-
userId: z.string().trim().min(1),
17-
role: z.enum(['lead', 'co_steward']),
18-
note: z.string().trim().min(1).optional(),
19-
moveToAssessing: z.boolean().optional().default(false),
20-
})
15+
const bodySchema = z
16+
.object({
17+
userId: z.string().trim().min(1),
18+
username: z.string().trim().min(1).optional().nullable(),
19+
displayName: z.string().trim().min(1).optional().nullable(),
20+
role: z.enum(['lead', 'co_steward']),
21+
note: z.string().trim().min(1).optional(),
22+
moveToAssessing: z.boolean().optional().default(false),
23+
})
24+
.refine((d) => (d.username == null) === (d.displayName == null), {
25+
message: 'username and displayName must both be provided or both be absent',
26+
path: ['displayName'],
27+
})
2128

2229
export async function assignStewardHandler(req: Request, res: Response): Promise<void> {
2330
const { id } = validateOrThrow(paramsSchema, req.params)
24-
const { userId, role, note, moveToAssessing } = validateOrThrow(bodySchema, req.body)
31+
const { userId, username, displayName, role, note, moveToAssessing } = validateOrThrow(
32+
bodySchema,
33+
req.body,
34+
)
2535

2636
const qx = await getPackagesQx()
2737
const result = await assignSteward(qx, id, {
2838
userId,
39+
username,
40+
displayName,
2941
role,
3042
note,
3143
assignedBy: req.actor.id,

backend/src/api/public/v1/stewardships/openapi.yaml

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,12 +126,18 @@ components:
126126
type: string
127127
description: Auth0 sub of the assigned steward.
128128
example: abc123
129-
name:
129+
username:
130130
type:
131131
- string
132132
- 'null'
133-
description: Display name of the steward. Null if not available.
134-
example: Jonathan R.
133+
description: LFX username of the steward. Null if not yet stored.
134+
example: joanagmaia
135+
displayName:
136+
type:
137+
- string
138+
- 'null'
139+
description: Full display name of the steward. Null if not yet stored.
140+
example: Joana Maia
135141
role:
136142
type: string
137143
enum: [lead, co_steward]
@@ -264,6 +270,14 @@ paths:
264270
type: string
265271
description: Auth0 sub of the user to assign as steward.
266272
example: abc123
273+
username:
274+
type: string
275+
description: LFX username of the steward. When provided together with displayName, stored for display purposes.
276+
example: joanagmaia
277+
displayName:
278+
type: string
279+
description: Full display name of the steward. When provided together with username, stored for display purposes.
280+
example: Joana Maia
267281
role:
268282
type: string
269283
enum: [lead, co_steward]
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
CREATE TABLE stewards (
2+
user_id TEXT PRIMARY KEY,
3+
username TEXT NOT NULL,
4+
display_name TEXT NOT NULL,
5+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
6+
);

services/libs/data-access-layer/src/osspckgs/api.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ export async function getOsspreyMetrics(qx: QueryExecutor): Promise<OsspreyMetri
112112

113113
export interface StewardEntry {
114114
userId: string
115+
username: string | null
116+
displayName: string | null
115117
role: string
116118
assignedAt: string
117119
}
@@ -460,12 +462,19 @@ export async function listPackagesForApi(
460462
LEFT JOIN LATERAL (
461463
SELECT COALESCE(
462464
json_agg(
463-
json_build_object('userId', ss.user_id, 'role', ss.role, 'assignedAt', ss.assigned_at)
465+
json_build_object(
466+
'userId', ss.user_id,
467+
'username', st.username,
468+
'displayName', st.display_name,
469+
'role', ss.role,
470+
'assignedAt', ss.assigned_at
471+
)
464472
ORDER BY ss.assigned_at ASC
465473
) FILTER (WHERE ss.id IS NOT NULL),
466474
'[]'::json
467475
) AS stewards
468476
FROM stewardship_stewards ss
477+
LEFT JOIN stewards st ON st.user_id = ss.user_id
469478
WHERE ss.stewardship_id = s.id
470479
AND ss.deleted_at IS NULL
471480
) ss_agg ON true`

services/libs/data-access-layer/src/osspckgs/stewardships.ts

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ export interface StewardshipStewardRecord {
1919
id: string
2020
stewardshipId: string
2121
userId: string
22-
name: string | null
22+
username: string | null
23+
displayName: string | null
2324
role: string
2425
assignedAt: string
2526
assignedBy: string | null
@@ -87,15 +88,33 @@ function toIso(v: unknown): string {
8788
return v instanceof Date ? v.toISOString() : String(v)
8889
}
8990

91+
async function fetchActiveStewards(
92+
qx: QueryExecutor,
93+
stewardshipId: number,
94+
): Promise<StewardshipStewardRecord[]> {
95+
const rows: Array<Record<string, unknown>> = await qx.select(
96+
`SELECT ss.id, ss.stewardship_id, ss.user_id, ss.role, ss.assigned_at, ss.assigned_by,
97+
st.username, st.display_name
98+
FROM stewardship_stewards ss
99+
LEFT JOIN stewards st ON st.user_id = ss.user_id
100+
WHERE ss.stewardship_id = $(stewardshipId)
101+
AND ss.deleted_at IS NULL
102+
ORDER BY ss.assigned_at ASC`,
103+
{ stewardshipId },
104+
)
105+
return rows.map(mapStewardStewardRow)
106+
}
107+
90108
function mapStewardStewardRow(row: Record<string, unknown>): StewardshipStewardRecord {
91109
return {
92110
id: String(row.id),
93111
stewardshipId: String(row.stewardship_id),
94112
userId: String(row.user_id),
95-
name: null,
113+
username: (row.username as string) ?? null,
114+
displayName: (row.display_name as string) ?? null,
96115
role: String(row.role),
97116
assignedAt: toIso(row.assigned_at),
98-
assignedBy: row.assigned_by ? String(row.assigned_by) : null,
117+
assignedBy: (row.assigned_by as string) ?? null,
99118
}
100119
}
101120

@@ -190,6 +209,8 @@ export async function assignSteward(
190209
stewardshipId: number,
191210
data: {
192211
userId: string
212+
username?: string | null
213+
displayName?: string | null
193214
role: 'lead' | 'co_steward'
194215
assignedBy: string
195216
note?: string
@@ -200,6 +221,18 @@ export async function assignSteward(
200221
const stewardship = await getStewardshipById(tx, stewardshipId)
201222
if (!stewardship) return null
202223

224+
if (data.username != null && data.displayName != null) {
225+
await tx.result(
226+
`INSERT INTO stewards (user_id, username, display_name, updated_at)
227+
VALUES ($(userId), $(username), $(displayName), NOW())
228+
ON CONFLICT (user_id) DO UPDATE
229+
SET username = EXCLUDED.username,
230+
display_name = EXCLUDED.display_name,
231+
updated_at = NOW()`,
232+
{ userId: data.userId, username: data.username, displayName: data.displayName },
233+
)
234+
}
235+
203236
// Soft-delete existing active entry for this user (handles role changes).
204237
await tx.result(
205238
`UPDATE stewardship_stewards
@@ -266,18 +299,9 @@ export async function assignSteward(
266299
if (updated) finalStewardship = mapStewardshipRow(updated)
267300
}
268301

269-
const stewards: Array<Record<string, unknown>> = await tx.select(
270-
`SELECT id, stewardship_id, user_id, role, assigned_at, assigned_by
271-
FROM stewardship_stewards
272-
WHERE stewardship_id = $(stewardshipId)
273-
AND deleted_at IS NULL
274-
ORDER BY assigned_at ASC`,
275-
{ stewardshipId },
276-
)
277-
278302
return {
279303
stewardship: finalStewardship,
280-
stewards: stewards.map(mapStewardStewardRow),
304+
stewards: await fetchActiveStewards(tx, stewardshipId),
281305
}
282306
})
283307
}
@@ -292,14 +316,7 @@ export async function getStewardshipSummary(
292316
stewardshipId: number,
293317
): Promise<StewardshipSummary> {
294318
const [stewards, activityRow] = await Promise.all([
295-
qx.select(
296-
`SELECT id, stewardship_id, user_id, role, assigned_at, assigned_by
297-
FROM stewardship_stewards
298-
WHERE stewardship_id = $(stewardshipId)
299-
AND deleted_at IS NULL
300-
ORDER BY assigned_at ASC`,
301-
{ stewardshipId },
302-
) as Promise<Array<Record<string, unknown>>>,
319+
fetchActiveStewards(qx, stewardshipId),
303320
qx.selectOneOrNone(
304321
`SELECT MAX(created_at) AS last_activity_at
305322
FROM stewardship_activity
@@ -309,7 +326,7 @@ export async function getStewardshipSummary(
309326
])
310327

311328
return {
312-
stewards: stewards.map(mapStewardStewardRow),
329+
stewards,
313330
lastActivityAt: activityRow?.last_activity_at ? toIso(activityRow.last_activity_at) : null,
314331
}
315332
}

0 commit comments

Comments
 (0)