Merge branch 'freight/nati-2' into freight/feat/element-chat

This commit is contained in:
Nathnael
2026-08-17 12:53:39 +00:00
63 changed files with 2408 additions and 1126 deletions

View File

@@ -0,0 +1,49 @@
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, string> | 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<Map<string, string>> {
const resolved = new Map<string, string>();
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<IamUserRow & { id: string }>;
for (const row of rows) {
const name = pickUserName(row);
if (name) resolved.set(row.id, name);
}
return resolved;
}