You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
80 lines
2.6 KiB
TypeScript
80 lines
2.6 KiB
TypeScript
import type { Database } from 'bun:sqlite';
|
|
import { ensureUserExists } from '../../db';
|
|
import { toIsoSqlUTC } from '../../utils/datetime';
|
|
|
|
/**
|
|
* Reconciliación idempotente de membresías de un grupo.
|
|
*/
|
|
export function reconcileGroupMembers(
|
|
db: Database,
|
|
groupId: string,
|
|
snapshot: Array<{ userId: string; isAdmin: boolean }>,
|
|
nowIso?: string
|
|
): { added: number; updated: number; deactivated: number } {
|
|
if (!groupId || !Array.isArray(snapshot)) {
|
|
throw new Error('Invalid arguments for reconcileGroupMembers');
|
|
}
|
|
const now = nowIso || toIsoSqlUTC(new Date());
|
|
let added = 0, updated = 0, deactivated = 0;
|
|
|
|
const incoming = new Map<string, { isAdmin: boolean }>();
|
|
for (const m of snapshot) {
|
|
if (!m?.userId) continue;
|
|
incoming.set(m.userId, { isAdmin: !!m.isAdmin });
|
|
}
|
|
|
|
db.transaction(() => {
|
|
const existingRows = db.prepare(`
|
|
SELECT user_id, is_admin, is_active
|
|
FROM group_members
|
|
WHERE group_id = ?
|
|
`).all(groupId) as Array<{ user_id: string; is_admin: number; is_active: number }>;
|
|
const existing = new Map(existingRows.map(r => [r.user_id, { isAdmin: !!r.is_admin, isActive: !!r.is_active }]));
|
|
|
|
for (const [userId, { isAdmin }] of incoming.entries()) {
|
|
ensureUserExists(userId, db);
|
|
const row = existing.get(userId);
|
|
if (!row) {
|
|
db.prepare(`
|
|
INSERT INTO group_members (group_id, user_id, is_admin, is_active, first_seen_at, last_seen_at)
|
|
VALUES (?, ?, ?, 1, ?, ?)
|
|
`).run(groupId, userId, isAdmin ? 1 : 0, now, now);
|
|
added++;
|
|
} else {
|
|
const roleChanged = row.isAdmin !== isAdmin;
|
|
if (!row.isActive || roleChanged) {
|
|
db.prepare(`
|
|
UPDATE group_members
|
|
SET is_active = 1,
|
|
is_admin = ?,
|
|
last_seen_at = ?,
|
|
last_role_change_at = CASE WHEN ? THEN ? ELSE last_role_change_at END
|
|
WHERE group_id = ? AND user_id = ?
|
|
`).run(isAdmin ? 1 : 0, now, roleChanged ? 1 : 0, roleChanged ? now : null, groupId, userId);
|
|
updated++;
|
|
} else {
|
|
db.prepare(`
|
|
UPDATE group_members
|
|
SET last_seen_at = ?
|
|
WHERE group_id = ? AND user_id = ?
|
|
`).run(now, groupId, userId);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const [userId, state] of existing.entries()) {
|
|
if (!incoming.has(userId) && state.isActive) {
|
|
db.prepare(`
|
|
UPDATE group_members
|
|
SET is_active = 0,
|
|
last_seen_at = ?
|
|
WHERE group_id = ? AND user_id = ?
|
|
`).run(now, groupId, userId);
|
|
deactivated++;
|
|
}
|
|
}
|
|
})();
|
|
|
|
return { added, updated, deactivated };
|
|
}
|