mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:43:06 +00:00
200 lines
6 KiB
TypeScript
200 lines
6 KiB
TypeScript
import 'server-only'
|
|
|
|
import { and, eq, type AnyColumn } from '@payloadcms/db-postgres/drizzle'
|
|
import type { Payload } from 'payload'
|
|
|
|
import type {
|
|
CandidateVoteRow,
|
|
ElectionCandidateRow,
|
|
ElectionOffice,
|
|
ElectionTallyRow,
|
|
ElectionTurn,
|
|
TseDetalheApuracaoRow,
|
|
} from '@/lib/electionResults'
|
|
import { computeWinnersByScope, mergeTallyWithWinners } from '@/lib/electionResults'
|
|
import { groupByScope } from '@/lib/electionResultsParse'
|
|
import { chunk, getDrizzleTables, INSERT_CHUNK_SIZE, requireTable } from '@/utilities/drizzleBulk'
|
|
|
|
const VOTE_TABLE = 'election_candidate_vote'
|
|
const TALLY_TABLE = 'election_tally'
|
|
const CANDIDATE_TABLE = 'election_candidate'
|
|
|
|
type ElectionImportScope = {
|
|
year: number
|
|
office: ElectionOffice
|
|
turn: ElectionTurn
|
|
}
|
|
|
|
export type ElectionImportBundle = {
|
|
scope: ElectionImportScope
|
|
votes: CandidateVoteRow[]
|
|
tallies: TseDetalheApuracaoRow[]
|
|
candidates: ElectionCandidateRow[]
|
|
}
|
|
|
|
export type ElectionImportCounts = {
|
|
scope: ElectionImportScope
|
|
votesDeleted: number
|
|
votesInserted: number
|
|
talliesDeleted: number
|
|
talliesInserted: number
|
|
candidatesDeleted: number
|
|
candidatesInserted: number
|
|
}
|
|
|
|
type DrizzleDeleteResult = { rowCount?: number | null }
|
|
|
|
type DrizzleTx = {
|
|
delete: (table: unknown) => {
|
|
where: (condition: unknown) => Promise<DrizzleDeleteResult>
|
|
}
|
|
insert: (table: unknown) => {
|
|
values: (rows: Record<string, unknown>[]) => Promise<unknown>
|
|
}
|
|
}
|
|
|
|
type PayloadDbDrizzle = {
|
|
drizzle: {
|
|
transaction: <T>(fn: (tx: DrizzleTx) => Promise<T>) => Promise<T>
|
|
}
|
|
}
|
|
|
|
/** Payload's generated Drizzle tables type columns as `unknown` — narrow with a runtime check. */
|
|
const scopeColumn = (
|
|
table: Record<string, unknown>,
|
|
name: 'year' | 'office' | 'turn',
|
|
): AnyColumn => {
|
|
const column = table[name]
|
|
if (column == null) throw new Error(`Election table is missing the "${name}" column.`)
|
|
return column as AnyColumn
|
|
}
|
|
|
|
const scopeWhere = (table: Record<string, unknown>, scope: ElectionImportScope) =>
|
|
and(
|
|
eq(scopeColumn(table, 'year'), scope.year),
|
|
eq(scopeColumn(table, 'office'), scope.office),
|
|
eq(scopeColumn(table, 'turn'), scope.turn),
|
|
)
|
|
|
|
const deleteScope = async (
|
|
tx: DrizzleTx,
|
|
table: Record<string, unknown>,
|
|
scope: ElectionImportScope,
|
|
): Promise<number> => {
|
|
// Avoid DELETE … RETURNING id (materializes every deleted row in memory on re-seed).
|
|
const result = await tx.delete(table).where(scopeWhere(table, scope))
|
|
return result.rowCount ?? 0
|
|
}
|
|
|
|
const nowIso = () => new Date().toISOString()
|
|
|
|
const withTimestamps = <T extends Record<string, unknown>>(row: T, now: string) => ({
|
|
...row,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
})
|
|
|
|
/**
|
|
* Replace all election data for a single (year, office, turn) scope in one transaction:
|
|
* delete existing rows, then bulk-insert the provided bundle.
|
|
*/
|
|
const importElectionScope = async (
|
|
payload: Payload,
|
|
bundle: ElectionImportBundle,
|
|
): Promise<ElectionImportCounts> => {
|
|
const tables = getDrizzleTables(payload)
|
|
const voteTable = requireTable(tables, VOTE_TABLE)
|
|
const tallyTable = requireTable(tables, TALLY_TABLE)
|
|
const candidateTable = requireTable(tables, CANDIDATE_TABLE)
|
|
const dbAdapter = payload.db as unknown as PayloadDbDrizzle
|
|
|
|
const winners = computeWinnersByScope(bundle.votes)
|
|
const talliesWithWinners = mergeTallyWithWinners(bundle.tallies, winners)
|
|
const now = nowIso()
|
|
|
|
return dbAdapter.drizzle.transaction(async (tx) => {
|
|
const votesDeleted = await deleteScope(tx, voteTable, bundle.scope)
|
|
const talliesDeleted = await deleteScope(tx, tallyTable, bundle.scope)
|
|
const candidatesDeleted = await deleteScope(tx, candidateTable, bundle.scope)
|
|
|
|
for (const batch of chunk(bundle.votes, INSERT_CHUNK_SIZE)) {
|
|
if (batch.length > 0) {
|
|
await tx.insert(voteTable).values(batch.map((row) => withTimestamps(row, now)))
|
|
}
|
|
}
|
|
|
|
for (const batch of chunk(talliesWithWinners, INSERT_CHUNK_SIZE)) {
|
|
if (batch.length > 0) {
|
|
await tx
|
|
.insert(tallyTable)
|
|
.values(batch.map((row: ElectionTallyRow) => withTimestamps(row, now)))
|
|
}
|
|
}
|
|
|
|
for (const batch of chunk(bundle.candidates, INSERT_CHUNK_SIZE)) {
|
|
if (batch.length > 0) {
|
|
await tx.insert(candidateTable).values(batch.map((row) => withTimestamps(row, now)))
|
|
}
|
|
}
|
|
|
|
return {
|
|
scope: bundle.scope,
|
|
votesDeleted,
|
|
votesInserted: bundle.votes.length,
|
|
talliesDeleted,
|
|
talliesInserted: talliesWithWinners.length,
|
|
candidatesDeleted,
|
|
candidatesInserted: bundle.candidates.length,
|
|
}
|
|
})
|
|
}
|
|
|
|
/** Import many scopes sequentially (each scope is its own transaction). */
|
|
export const importElectionBundles = async (
|
|
payload: Payload,
|
|
bundles: readonly ElectionImportBundle[],
|
|
): Promise<ElectionImportCounts[]> => {
|
|
const results: ElectionImportCounts[] = []
|
|
for (const bundle of bundles) {
|
|
results.push(await importElectionScope(payload, bundle))
|
|
}
|
|
return results
|
|
}
|
|
|
|
/**
|
|
* Build import bundles from flat parsed arrays, grouping by (year, office, turn).
|
|
* Candidates without matching vote/tally scopes are still imported under their own scope.
|
|
*/
|
|
export const buildImportBundles = (args: {
|
|
votes: readonly CandidateVoteRow[]
|
|
tallies: readonly TseDetalheApuracaoRow[]
|
|
candidates: readonly ElectionCandidateRow[]
|
|
}): ElectionImportBundle[] => {
|
|
const votesByScope = groupByScope(args.votes)
|
|
const talliesByScope = groupByScope(args.tallies)
|
|
const candidatesByScope = groupByScope(args.candidates)
|
|
|
|
const keys = new Set([
|
|
...votesByScope.keys(),
|
|
...talliesByScope.keys(),
|
|
...candidatesByScope.keys(),
|
|
])
|
|
|
|
return [...keys].map((key) => {
|
|
const sample =
|
|
votesByScope.get(key)?.[0] ?? talliesByScope.get(key)?.[0] ?? candidatesByScope.get(key)?.[0]
|
|
if (!sample) {
|
|
throw new Error(`Empty election import scope key: ${key}`)
|
|
}
|
|
return {
|
|
scope: {
|
|
year: sample.year,
|
|
office: sample.office,
|
|
turn: sample.turn,
|
|
},
|
|
votes: votesByScope.get(key) ?? [],
|
|
tallies: talliesByScope.get(key) ?? [],
|
|
candidates: candidatesByScope.get(key) ?? [],
|
|
}
|
|
})
|
|
}
|