import { DataSource } from "typeorm"; /** * `iam.users.name` is a localized object ({ en, am, … }), not a string — a * plain `String(name)` there yields "[object Object]" in an audit trail. */ export interface IamUserRow { name?: Record | string | null; username?: string | null; email?: string | null; } /** Best display name for a user row: English label → any locale → login → email. */ export function pickUserName(user: IamUserRow): string | null { const { name } = user; if (typeof name === "string" && name.trim()) return name.trim(); if (name && typeof name === "object") { const localized = name.en ?? Object.values(name).find((v) => typeof v === "string" && v.trim()); if (localized?.trim()) return localized.trim(); } return user.username?.trim() || user.email?.trim() || null; } /** * Display names for a set of IAM user ids — one query for the whole set. * `iam.users` is owned by the auth system and has no entity here, so it is read * directly. A miss is not an error: the caller still holds the id and can fall * back to it. */ export async function resolveIamUserNames( dataSource: DataSource, userIds: (string | null | undefined)[], ): Promise> { const resolved = new Map(); const ids = [...new Set(userIds.filter((id): id is string => Boolean(id)))]; if (ids.length === 0) return resolved; const rows = (await dataSource.query( `SELECT id, name, username, email FROM iam.users WHERE id = ANY($1::uuid[])`, [ids], )) as Array; for (const row of rows) { const name = pickUserName(row); if (name) resolved.set(row.id, name); } return resolved; }