delete if necc.

This commit is contained in:
2026-05-27 17:24:09 -05:00
parent a63045a685
commit 730c03370d

View File

@@ -5,12 +5,18 @@ import { DmsService } from './dms.js';
export class SyncService {
constructor(private dms = new DmsService()) {}
async syncFromDms(): Promise<{ domains: number; mailboxes: number }> {
async syncFromDms(): Promise<{
domains: number;
mailboxes: number;
removed_mailboxes: number;
removed_domains: number;
}> {
const accounts = await this.dms.listAccounts();
const domains = [...new Set(accounts.map((a) => a.domain))].sort();
await pool.query('BEGIN');
try {
// 1. Upsert every domain that currently exists in DMS as 'active'.
for (const domain of domains) {
await pool.query(
`INSERT INTO domains(domain, current_node, status, last_seen_at, last_synced_at)
@@ -25,6 +31,7 @@ export class SyncService {
);
}
// 2. Upsert every mailbox that currently exists in DMS as 'active'.
const localEmails = accounts.map((a) => a.email);
for (const account of accounts) {
await pool.query(
@@ -41,25 +48,72 @@ export class SyncService {
);
}
await pool.query(
`UPDATE domains
SET status='missing_on_node', last_synced_at=now(), updated_at=now()
WHERE current_node=$1 AND NOT (domain = ANY($2::text[]))`,
[config.nodeName, domains],
// 3. Find mailboxes that this node previously owned but that are
// no longer in DMS (or never were). They get hard-deleted.
// We collect (email, domain) first so we can emit billing events
// for any of them that don't already have a 'deleted' event.
const toRemoveResult = await pool.query<{ email_address: string; domain: string; previous_status: string }>(
`SELECT email_address, domain, status AS previous_status
FROM mailboxes
WHERE node_name=$1 AND NOT (email_address = ANY($2::text[]))`,
[config.nodeName, localEmails],
);
const toRemove = toRemoveResult.rows;
await pool.query(
`UPDATE mailboxes
SET status='missing_on_node', updated_at=now()
WHERE node_name=$1 AND status='active' AND NOT (email_address = ANY($2::text[]))`,
// 3a. Emit a synthetic 'deleted' billing event for any mailbox
// that was previously active and doesn't already have one.
// This keeps billing history correct even though we hard-delete
// the mailbox row itself in the next step.
for (const row of toRemove) {
if (row.previous_status === 'active' || row.previous_status === 'missing_on_node') {
await pool.query(
`INSERT INTO mailbox_billing_events (domain, email, action, actor_email, notes)
SELECT $1, $2, 'deleted', NULL, 'removed via DMS resync (was ' || $3 || ')'
WHERE NOT EXISTS (
SELECT 1 FROM mailbox_billing_events b
WHERE b.email = $2
AND b.action = 'deleted'
AND b.occurred_at >= (
SELECT COALESCE(MAX(occurred_at), '1970-01-01'::timestamptz)
FROM mailbox_billing_events
WHERE email = $2 AND action = 'created'
)
)`,
[row.domain, row.email_address, row.previous_status],
);
}
}
// 3b. Hard-delete the mailbox rows themselves.
const removedMailboxes = await pool.query(
`DELETE FROM mailboxes
WHERE node_name=$1 AND NOT (email_address = ANY($2::text[]))`,
[config.nodeName, localEmails],
);
// 4. Hard-delete domains that no longer have any mailbox on this node.
// We restrict to current_node so we don't touch domains owned by
// other nodes if that ever becomes a thing.
const removedDomains = await pool.query(
`DELETE FROM domains
WHERE current_node=$1
AND NOT EXISTS (
SELECT 1 FROM mailboxes m
WHERE m.domain = domains.domain AND m.node_name = $1
)`,
[config.nodeName],
);
await pool.query('COMMIT');
return { domains: domains.length, mailboxes: accounts.length };
return {
domains: domains.length,
mailboxes: accounts.length,
removed_mailboxes: removedMailboxes.rowCount ?? 0,
removed_domains: removedDomains.rowCount ?? 0,
};
} catch (err) {
await pool.query('ROLLBACK');
throw err;
}
}
}
}