delete if necc.
This commit is contained in:
@@ -5,12 +5,18 @@ import { DmsService } from './dms.js';
|
|||||||
export class SyncService {
|
export class SyncService {
|
||||||
constructor(private dms = new DmsService()) {}
|
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 accounts = await this.dms.listAccounts();
|
||||||
const domains = [...new Set(accounts.map((a) => a.domain))].sort();
|
const domains = [...new Set(accounts.map((a) => a.domain))].sort();
|
||||||
|
|
||||||
await pool.query('BEGIN');
|
await pool.query('BEGIN');
|
||||||
try {
|
try {
|
||||||
|
// 1. Upsert every domain that currently exists in DMS as 'active'.
|
||||||
for (const domain of domains) {
|
for (const domain of domains) {
|
||||||
await pool.query(
|
await pool.query(
|
||||||
`INSERT INTO domains(domain, current_node, status, last_seen_at, last_synced_at)
|
`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);
|
const localEmails = accounts.map((a) => a.email);
|
||||||
for (const account of accounts) {
|
for (const account of accounts) {
|
||||||
await pool.query(
|
await pool.query(
|
||||||
@@ -41,25 +48,72 @@ export class SyncService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await pool.query(
|
// 3. Find mailboxes that this node previously owned but that are
|
||||||
`UPDATE domains
|
// no longer in DMS (or never were). They get hard-deleted.
|
||||||
SET status='missing_on_node', last_synced_at=now(), updated_at=now()
|
// We collect (email, domain) first so we can emit billing events
|
||||||
WHERE current_node=$1 AND NOT (domain = ANY($2::text[]))`,
|
// for any of them that don't already have a 'deleted' event.
|
||||||
[config.nodeName, domains],
|
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(
|
// 3a. Emit a synthetic 'deleted' billing event for any mailbox
|
||||||
`UPDATE mailboxes
|
// that was previously active and doesn't already have one.
|
||||||
SET status='missing_on_node', updated_at=now()
|
// This keeps billing history correct even though we hard-delete
|
||||||
WHERE node_name=$1 AND status='active' AND NOT (email_address = ANY($2::text[]))`,
|
// 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],
|
[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');
|
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) {
|
} catch (err) {
|
||||||
await pool.query('ROLLBACK');
|
await pool.query('ROLLBACK');
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user