From c42b16baab3e9a574506c89d3212828ac16c9598 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Ign=C3=A1cio=20Fontoura=20Solla?= Date: Sat, 18 Jul 2026 18:25:19 -0300 Subject: [PATCH] Add TSE 2022 election results model and local seed import. --- .gitignore | 3 + docs/plans/baseline-eleitoral-tse.md | 23 +- docs/roadmap.md | 6 +- package.json | 6 +- pnpm-lock.yaml | 34 + scripts/assert-local-database.mjs | 45 + scripts/guard-dev-db.mjs | 47 +- scripts/seed-loader.mjs | 19 +- scripts/seed-posts.mjs | 47 +- scripts/seed-tse-results.mjs | 173 + src/collections/ElectionCandidate.ts | 159 + src/collections/ElectionCandidateVote.ts | 158 + src/collections/ElectionTally.ts | 188 + src/lib/electionCandidateIdentity.ts | 30 + src/lib/electionResults.ts | 317 + src/lib/electionResultsBuild.ts | 98 + src/lib/electionResultsCsv.ts | 22 + src/lib/electionResultsParse.ts | 220 + src/lib/electionResultsZip.ts | 69 + .../20260718_195854_add_election_results.json | 7709 +++++++++++++++++ .../20260718_195854_add_election_results.ts | 153 + src/migrations/index.ts | 9 +- src/payload-types.ts | 176 + src/payload.config.ts | 6 + src/utilities/campaignAccess.ts | 7 + src/utilities/electionResultsImport.ts | 206 + .../fixtures/tse/consulta_cand_ba_fixture.csv | 8 + .../fixtures/tse/consulta_cand_br_fixture.csv | 5 + .../tse/detalhe_votacao_munzona_fixture.csv | 9 + .../tse/votacao_candidato_munzona_fixture.csv | 17 + tests/helpers/tseFixtures.ts | 35 + tests/int/electionResultsImport.int.spec.ts | 186 + tests/unit/electionResults.unit.spec.ts | 161 + 33 files changed, 10255 insertions(+), 96 deletions(-) create mode 100644 scripts/assert-local-database.mjs create mode 100644 scripts/seed-tse-results.mjs create mode 100644 src/collections/ElectionCandidate.ts create mode 100644 src/collections/ElectionCandidateVote.ts create mode 100644 src/collections/ElectionTally.ts create mode 100644 src/lib/electionCandidateIdentity.ts create mode 100644 src/lib/electionResults.ts create mode 100644 src/lib/electionResultsBuild.ts create mode 100644 src/lib/electionResultsCsv.ts create mode 100644 src/lib/electionResultsParse.ts create mode 100644 src/lib/electionResultsZip.ts create mode 100644 src/migrations/20260718_195854_add_election_results.json create mode 100644 src/migrations/20260718_195854_add_election_results.ts create mode 100644 src/utilities/electionResultsImport.ts create mode 100644 tests/fixtures/tse/consulta_cand_ba_fixture.csv create mode 100644 tests/fixtures/tse/consulta_cand_br_fixture.csv create mode 100644 tests/fixtures/tse/detalhe_votacao_munzona_fixture.csv create mode 100644 tests/fixtures/tse/votacao_candidato_munzona_fixture.csv create mode 100644 tests/helpers/tseFixtures.ts create mode 100644 tests/int/electionResultsImport.int.spec.ts create mode 100644 tests/unit/electionResults.unit.spec.ts diff --git a/.gitignore b/.gitignore index df0d8dfd..940332d8 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,9 @@ next-env.d.ts /media +# TSE open-data zip cache (pnpm db:seed:tse) +/data/tse/ + # Playwright node_modules/ /test-results/ diff --git a/docs/plans/baseline-eleitoral-tse.md b/docs/plans/baseline-eleitoral-tse.md index 9c2a757f..68f47d80 100644 --- a/docs/plans/baseline-eleitoral-tse.md +++ b/docs/plans/baseline-eleitoral-tse.md @@ -1,8 +1,8 @@ # Baseline eleitoral TSE 2022 -Status: rascunho -Atualizado em: 2026-07-17 -Item do roadmap: [docs/roadmap.md](../roadmap.md) (seção "Campanha → Próximos ciclos") +Status: Fase 1 (A3) implementada — modelo + import +Atualizado em: 2026-07-18 +Item do roadmap: [docs/roadmap.md](../roadmap.md) (A3/A4 Baseline TSE 2022) Responsável: — ## Referência visual (UX Pilot) @@ -43,13 +43,16 @@ Candidatos de interesse (destaques na UI): **Solla = dep. federal BA, PT, nº 13 - **Access control:** dado público TSE. `read` para autenticado em `campaignUser` ou `users` (não aberto ao público). `create/update/delete` admin only. Sem `Consent`, sem transação em runtime. Import via CLI com `overrideAccess: true` fora do runtime deployado. - **Sem PII.** Não persistir CPF nem título eleitoral. A identidade cross-ano usa `identityKey = sha256(normalize(urnaName) + birthCity + birthState + party)` — só dados públicos da consulta de candidaturas. - **Migration** `add_election_results` (push:false). `pnpm build` aplica em prod no deploy. Não esbarra no bloqueador LGPD (dado público, sem liderança real). -- **Import idempotente** pela chave única (upsert), com proveniência (URL, data, SHA-256) no cabeçalho do script. Sem step de revalidate (páginas de campanha são dinâmicas com auth, sem ISR/tag). +- **Import idempotente por escopo** `(year, office, turn)`: delete + bulk insert via `payload.db.drizzle` (não upsert row a row). Proveniência (URL, SHA-256) no cabeçalho de `scripts/seed-tse-results.mjs`. Sem step de revalidate (páginas de campanha são dinâmicas com auth, sem ISR/tag). +- **Sem votos de legenda na v1.** Campo `voteType` permanece no schema; só rows `nominal` são importadas. O agregado `votosLegenda` do tally cobre o que os insights precisam. Dataset `votacao_partido_munzona` fica fora do escopo A3. +- **Votos anulados sub judice (2022):** `electionCandidateVote.votes` ← `QT_VOTOS_NOMINAIS` (apurados); validade fica no tally (`votosNominaisValidos`, `votosAnulados`). +- **Mapeamento de município:** `NM_MUNICIPIO` (TSE) → nome canônico de `bahiaTerritories.ts` via normalização case/acento/apóstrofo; falha alta em município não mapeado; persiste `cityCode` (TSE) + `cityName` canônico. Teste cobre os 417 municípios. - **Agregação núcleo→baseline é autossuficiente** (revisão 2026-07-17): `citiesForTerritory` **já existe** em `src/lib/bahiaTerritories.ts` (não vem do plano `zonas-por-municipio.md`), e a correspondência cidade↔zona pode ser resolvida pelas próprias rows de `electionTally` (cada par cidade×zona importado é uma row — `where cityName = C` retorna todas as zonas da cidade). `tseZonesForCity` (plano `zonas-por-municipio.md`) é **opcional**: quando existir, vira a fonte preferida por ser validada por fixture. Sem `tseZones` → todas as zonas da(s) cidade(s); só `region` → todas as cidades do território; sem geografia → estado "sem baseline TSE". - **i18n e naming** seguem o AGENTS.md: identificadores em inglês, strings visíveis em pt-BR. ## Questões em aberto -- Mapeamento `CD_MUNICIPIO` (TSE) → nome canônico em `CitiesByState.BA`: usar `NM_MUNICIPIO` do CSV + validação cruzada. +- ~~Mapeamento `CD_MUNICIPIO` (TSE) → nome canônico~~ — resolvido na Fase 1 via `canonicalizeMunicipalityName` + `cityCode` persistido. - Limiares de classificação territorial (defesa/ataque/indecisa/perdida) — definir com produto. - Mostrar 1º e 2º turno para presidente/governador, ou só o decisivo? Recomendação: ambos, com destaque ao decisivo. - `lideranca` vê ranking/competidor? Recomendação: sim, é dado público e útil ao engajamento. @@ -96,7 +99,7 @@ flowchart LR ### Fases -1. **Modelo + import** — collections, migration, `scripts/seed-tse-results.mjs` + `pnpm db:seed:tse`, helpers `src/lib/electionResults.ts` e `src/lib/electionCandidateIdentity.ts`, teste int (totais conhecidos, `elected=true` para Solla/Lula/Jerônimo, idempotência). +1. **Modelo + import** — **feito (A3, 2026-07-18).** Collections `electionTally` / `electionCandidateVote` / `electionCandidate`, migration `20260718_195854_add_election_results`, `pnpm db:seed:tse`, helpers em `src/lib/electionResults*.ts` + `electionCandidateIdentity.ts`, import drizzle em `src/utilities/electionResultsImport.ts`, testes unit/int com fixtures em `tests/fixtures/tse/`. Seed local BA 2022: **211.014** votos nominais, **2.700** apurações, **1.654** candidatos (6 escopos office×turn). 2. **Detalhe do núcleo** — bloco "Baseline eleitoral 2022" na aba overview; view model `NucleusElectoralBaselineViewModel` em `src/utilities/nucleusViewModels.ts`; componente `src/components/campaign/NucleusElectoralBaseline.tsx`. 3. **Overview da lista** — estende `overview-lista-nucleos.md`: somar baselines sobre o conjunto filtrado (`buildNucleusListWhere`); novo bloco no `NucleusListOverview`. 4. **Insights automáticos (Gap vs 2022)** — implementa somente o insight "Gap vs 2022" (ver seção "Insights automáticos" abaixo). `src/components/campaign/NucleusInsights.tsx` + `src/lib/electionInsights.ts`. Os demais insights viram itens separados no roadmap com planos próprios. @@ -167,8 +170,12 @@ Cada insight é uma **derivação de leitura** (sem escrita, sem `Consent`, sem ## Arquivos a criar/alterar -- Criar: `src/collections/ElectionTally.ts`, `src/collections/ElectionCandidateVote.ts`, `src/collections/ElectionCandidate.ts`, `src/lib/electionResults.ts`, `src/lib/electionInsights.ts`, `src/lib/electionCandidateIdentity.ts`, `src/utilities/nucleusElectoralBaseline.ts`, `src/components/campaign/NucleusElectoralBaseline.tsx`, `src/components/campaign/NucleusInsights.tsx`, `scripts/seed-tse-results.mjs`, `scripts/reconcile-running-again.mjs`, migration `add_election_results`, testes int. (Os demais componentes de insight — `electionAlliances.ts`, `DobradinhaOpportunities.tsx` etc. — ficam nos planos separados dos respectivos insights.) -- Alterar: `src/payload.config.ts`, `src/app/(campaign)/campanha/(app)/nucleos/[slug]/page.tsx` + `src/utilities/nucleusDetailPageData.ts`, `src/utilities/nucleusPageData.ts`/`nucleusViewModels.ts`, overview da lista, `package.json` (`db:seed:tse`). +**Fase 1 (A3) — feitos:** +- Criar: `src/collections/ElectionTally.ts`, `ElectionCandidateVote.ts`, `ElectionCandidate.ts`; `src/lib/electionResults.ts`, `electionResultsParse.ts`, `electionResultsBuild.ts`, `electionResultsCsv.ts`, `electionResultsZip.ts`, `electionCandidateIdentity.ts`; `src/utilities/electionResultsImport.ts`; `scripts/seed-tse-results.mjs`; migration `20260718_195854_add_election_results`; fixtures `tests/fixtures/tse/*`; testes unit/int. +- Alterar: `src/payload.config.ts`, `src/utilities/campaignAccess.ts` (`canReadElectionData` / `canMutateElectionData`), `package.json` (`db:seed:tse`), `scripts/seed-loader.mjs` (stub `server-only`), `.gitignore` (`/data/tse/`). + +**Fases 2–5 (A4+) — ainda a criar:** +- `src/lib/electionInsights.ts`, `src/utilities/nucleusElectoralBaseline.ts`, `src/components/campaign/NucleusElectoralBaseline.tsx`, `NucleusInsights.tsx`, `scripts/reconcile-running-again.mjs`; alterar páginas do núcleo / overview. ## Dependências diff --git a/docs/roadmap.md b/docs/roadmap.md index 94db1bbe..3b154805 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -82,7 +82,7 @@ flowchart TD subgraph TrilhaA["Trilha A — dados eleitorais e território"] A1["A1 Território multi-município/bairro
(migration)"] A2["A2 Zonas TSE por município
(auto-preenchimento)"] - A3["A3 Baseline TSE 2022 — Fase 1
(import, collections)"] + A3["A3 Baseline TSE 2022 — Fase 1
(import, collections) ✓"] A4["A4 Baseline no produto + Gap vs 2022
(detalhe, overview)"] A5["A5 Insights: conversão, classificação,
alavancagem, mobilização, competitiva"] A6["A6 Dobradinha 2026"] @@ -130,7 +130,7 @@ flowchart TD JUR -.chave de push.-> D2 ``` -Itens sem seta de entrada (**paralelizáveis a qualquer momento**): A1, A3, B1, B2, C1, C2 (engenharia — produção espera o jurídico), D1, além dos fill-ins (visitados recentemente, listas globais, reset de senha, higiene PascalCase). +Itens sem seta de entrada (**paralelizáveis a qualquer momento**): A1, B1, B2, C1, C2 (engenharia — produção espera o jurídico), D1, além dos fill-ins (visitados recentemente, listas globais, reset de senha, higiene PascalCase). **A3 entregue** (2026-07-18) — collections + `pnpm db:seed:tse`. ### Sequência de execução por janela do calendário @@ -143,7 +143,7 @@ Itens sem seta de entrada (**paralelizáveis a qualquer momento**): A1, A3, B1, | 3 | A2 Zonas TSE por município (auto-preenchimento) | [detalhes](plans/zonas-por-municipio.md) | A1 (nasce contra `cities[]`) | B1, C1, A3 | | 4 | B1 Overview da lista de núcleos | [detalhes](plans/overview-lista-nucleos.md) | — | A1, A2, C1 | | 5 | C1 Compartilhar página (quick win de campo) | [detalhes](plans/compartilhar-pagina.md) | — | tudo | -| 6 | A3 Baseline TSE 2022 — Fase 1 (import) | [detalhes](plans/baseline-eleitoral-tse.md) | — (dado público; sem bloqueador LGPD) | tudo | +| 6 | A3 Baseline TSE 2022 — Fase 1 (import) **feito** | [detalhes](plans/baseline-eleitoral-tse.md) | — (dado público; sem bloqueador LGPD) | tudo | **Janela 2 — 05/08 → 16/08 (pré-propaganda): base nominal + inteligência + agenda prontas para o arranque.** diff --git a/package.json b/package.json index be94196e..c4b73605 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "db:stop": "docker compose down", "db:pull": "node scripts/db-pull.mjs", "db:seed:posts": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm --import=./scripts/seed-loader.mjs\" node scripts/seed-posts.mjs", + "db:seed:tse": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm --import=./scripts/seed-loader.mjs\" node scripts/seed-tse-results.mjs", "dev": "node scripts/guard-dev-db.mjs && cross-env NODE_OPTIONS=--no-deprecation next dev", "devsafe": "rm -rf .next && node scripts/guard-dev-db.mjs && cross-env NODE_OPTIONS=--no-deprecation next dev", "generate:importmap": "cross-env NODE_OPTIONS=\"--no-deprecation --conditions=react-server\" payload generate:importmap", @@ -89,8 +90,10 @@ "@types/node": "22.19.9", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", + "@types/yauzl": "^3.4.0", "@vercel/blob": "2.3.1", "@vitejs/plugin-react": "4.5.2", + "csv-parse": "^7.0.1", "eslint": "^9.39.2", "eslint-config-next": "15.4.11", "eslint-config-prettier": "^10.1.8", @@ -104,7 +107,8 @@ "typescript": "5.7.3", "typescript-eslint": "^8.55.0", "vite-tsconfig-paths": "6.0.5", - "vitest": "4.0.18" + "vitest": "4.0.18", + "yauzl": "^3.4.0" }, "engines": { "node": "^18.20.2 || >=20.9.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 68f21d7d..719916d2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -138,12 +138,18 @@ importers: '@types/react-dom': specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.14) + '@types/yauzl': + specifier: ^3.4.0 + version: 3.4.0 '@vercel/blob': specifier: 2.3.1 version: 2.3.1 '@vitejs/plugin-react': specifier: 4.5.2 version: 4.5.2(vite@7.3.1(@types/node@22.19.9)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.77.4)(tsx@4.21.0)(yaml@2.8.2)) + csv-parse: + specifier: ^7.0.1 + version: 7.0.1 eslint: specifier: ^9.39.2 version: 9.39.2(jiti@2.6.1) @@ -186,6 +192,9 @@ importers: vitest: specifier: 4.0.18 version: 4.0.18(@types/node@22.19.9)(happy-dom@20.8.3)(jiti@2.6.1)(jsdom@28.0.0(@noble/hashes@1.8.0))(lightningcss@1.30.2)(msw@2.12.10(@types/node@22.19.9)(typescript@5.7.3))(sass@1.77.4)(tsx@4.21.0)(yaml@2.8.2) + yauzl: + specifier: ^3.4.0 + version: 3.4.0 packages: @@ -2828,6 +2837,9 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@types/yauzl@3.4.0': + resolution: {integrity: sha512-NRPn5w6h8dhcnmx3YIRQcqMywY/+nND/uOkJessedcrowO3C0AssHp3tMJpxKAwOhFOo0OV1y9VtsC5hbKKBAw==} + '@typescript-eslint/eslint-plugin@8.55.0': resolution: {integrity: sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3498,6 +3510,9 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + csv-parse@7.0.1: + resolution: {integrity: sha512-+2z7Ar0APQ7Uu6fX4cn+pitRmxjZ1WPBcGmZFKmA74FCyi7Et/XZx8cjNQ5CjbZ4HCOxXCOpRBYvYH08Qa003A==} + damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} @@ -5240,6 +5255,9 @@ packages: peerDependencies: graphql: ^16.8.1 + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + pg-cloudflare@1.3.0: resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} @@ -6438,6 +6456,10 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + yauzl@3.4.0: + resolution: {integrity: sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==} + engines: {node: '>=12'} + yjs@13.6.29: resolution: {integrity: sha512-kHqDPdltoXH+X4w1lVmMtddE3Oeqq48nM40FD5ojTd8xYhQpzIDcfE2keMSU5bAgRPJBe225WTUdyUgj1DtbiQ==} engines: {node: '>=16.0.0', npm: '>=8.0.0'} @@ -9098,6 +9120,10 @@ snapshots: dependencies: '@types/node': 22.19.9 + '@types/yauzl@3.4.0': + dependencies: + '@types/node': 22.19.9 + '@typescript-eslint/eslint-plugin@8.55.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.7.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.7.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -9750,6 +9776,8 @@ snapshots: csstype@3.2.3: {} + csv-parse@7.0.1: {} + damerau-levenshtein@1.0.8: {} data-uri-to-buffer@4.0.1: {} @@ -11755,6 +11783,8 @@ snapshots: - typescript - utf-8-validate + pend@1.2.0: {} + pg-cloudflare@1.3.0: optional: true @@ -13155,6 +13185,10 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yauzl@3.4.0: + dependencies: + pend: 1.2.0 + yjs@13.6.29: dependencies: lib0: 0.2.117 diff --git a/scripts/assert-local-database.mjs b/scripts/assert-local-database.mjs new file mode 100644 index 00000000..2503af49 --- /dev/null +++ b/scripts/assert-local-database.mjs @@ -0,0 +1,45 @@ +/** + * Shared local-DB guard for CLI scripts (seeds, pnpm dev guard, etc.). + * Host allowlist + ALLOW_REMOTE_DB escape hatch. + */ + +const OVERRIDE_FLAG = 'ALLOW_REMOTE_DB' +const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '0.0.0.0', '::1', 'postgres']) + +/** + * @param {string} label Log/error prefix, e.g. "seed:tse" + * @param {string} [usageHint] Extra lines shown when refusing a remote host + */ +export const assertLocalDatabase = (label, usageHint = '') => { + const die = (message) => { + console.error(`\n[${label}] ${message}\n`) + process.exit(1) + } + + const databaseUrl = process.env.DATABASE_URL + if (!databaseUrl) die('DATABASE_URL is not set. Refusing to continue.') + + if (process.env[OVERRIDE_FLAG] === 'true' || process.env[OVERRIDE_FLAG] === '1') { + console.warn( + `\n[${label}] ${OVERRIDE_FLAG} is set — connecting to a remote database on purpose. Be careful.\n`, + ) + return + } + + let host + try { + host = new URL(databaseUrl).hostname + } catch { + die('DATABASE_URL is not a valid connection string.') + } + + if (!LOCAL_HOSTS.has(host)) { + die( + `DATABASE_URL points at a non-local host ("${host}").\n` + + (usageHint ? `${usageHint}\n\n` : '') + + `If you REALLY mean to use a remote database, re-run with:\n ${OVERRIDE_FLAG}=true …`, + ) + } + + console.log(`[${label}] OK — using local database host "${host}".`) +} diff --git a/scripts/guard-dev-db.mjs b/scripts/guard-dev-db.mjs index 11decf64..b2d2c7e4 100644 --- a/scripts/guard-dev-db.mjs +++ b/scripts/guard-dev-db.mjs @@ -9,8 +9,7 @@ */ import { config as loadEnv } from 'dotenv' -const OVERRIDE_FLAG = 'ALLOW_REMOTE_DB' -const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '0.0.0.0', '::1', 'postgres']) +import { assertLocalDatabase } from './assert-local-database.mjs' // Mirror Next.js precedence (.env.local wins over .env) WITHOUT overriding a // DATABASE_URL that is already set in the real environment (e.g. injected by @@ -18,40 +17,10 @@ const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '0.0.0.0', '::1', 'postgr loadEnv({ path: '.env.local' }) loadEnv({ path: '.env' }) -const fail = (message) => { - console.error(`\n[guard-dev-db] ${message}\n`) - process.exit(1) -} - -const databaseUrl = process.env.DATABASE_URL - -if (!databaseUrl) { - fail('DATABASE_URL is not set. Refusing to start the dev server.') -} - -if (process.env[OVERRIDE_FLAG] === 'true' || process.env[OVERRIDE_FLAG] === '1') { - console.warn( - `\n[guard-dev-db] ${OVERRIDE_FLAG} is set — connecting to a remote database on purpose. Be careful.\n`, - ) - process.exit(0) -} - -let host -try { - host = new URL(databaseUrl).hostname -} catch { - fail('DATABASE_URL is not a valid connection string.') -} - -if (!LOCAL_HOSTS.has(host)) { - fail( - `DATABASE_URL points at a non-local host ("${host}").\n` + - 'This is almost certainly the production or a shared database.\n\n' + - 'Local development should use the local Postgres from docker-compose:\n' + - ' 1. pnpm db:start\n' + - ' 2. set DATABASE_URL=postgresql://teqo:teqo@localhost:5432/teqo in .env.local\n\n' + - `If you REALLY mean to connect to a remote database, re-run with:\n ${OVERRIDE_FLAG}=true pnpm dev`, - ) -} - -console.log(`[guard-dev-db] OK — using local database host "${host}".`) +assertLocalDatabase( + 'guard-dev-db', + 'This is almost certainly the production or a shared database.\n\n' + + 'Local development should use the local Postgres from docker-compose:\n' + + ' 1. pnpm db:start\n' + + ' 2. set DATABASE_URL=postgresql://teqo:teqo@localhost:5432/teqo in .env.local', +) diff --git a/scripts/seed-loader.mjs b/scripts/seed-loader.mjs index 1a375f51..15d21646 100644 --- a/scripts/seed-loader.mjs +++ b/scripts/seed-loader.mjs @@ -1,5 +1,5 @@ /** - * Seed-only Node module loader: stubs `next/cache` with no-ops. + * Seed-only Node module loader: stubs `next/cache` and `server-only`. * * The `post` and `tag` collections call `revalidateTag()` (via * `src/utilities/documents.ts`) inside their `afterChange` hooks. `revalidateTag` @@ -8,12 +8,16 @@ * every `payload.create`. Rather than editing the collections (owned elsewhere), * we replace `next/cache` with harmless no-ops for the duration of the seed. * - * Registered via `--import=./scripts/seed-loader.mjs` in the `db:seed:posts` - * npm script; it affects only that process. + * Campaign collections also pull in utilities marked `import 'server-only'`; that + * package throws outside the Next.js server runtime, so we stub it as an empty + * module for CLI seeds (`db:seed:posts`, `db:seed:tse`). + * + * Registered via `--import=./scripts/seed-loader.mjs` in the seed npm scripts; + * it affects only that process. */ import { register } from 'node:module' -const shim = +const nextCacheShim = 'data:text/javascript,' + encodeURIComponent( [ @@ -24,12 +28,17 @@ const shim = ].join('\n'), ) +const serverOnlyShim = 'data:text/javascript,' + encodeURIComponent('export {};') + const hooks = 'data:text/javascript,' + encodeURIComponent( `export function resolve(specifier, context, nextResolve) { if (specifier === 'next/cache') { - return { url: ${JSON.stringify(shim)}, shortCircuit: true } + return { url: ${JSON.stringify(nextCacheShim)}, shortCircuit: true } + } + if (specifier === 'server-only') { + return { url: ${JSON.stringify(serverOnlyShim)}, shortCircuit: true } } return nextResolve(specifier, context) }`, diff --git a/scripts/seed-posts.mjs b/scripts/seed-posts.mjs index 8803cb82..79aff666 100644 --- a/scripts/seed-posts.mjs +++ b/scripts/seed-posts.mjs @@ -29,6 +29,8 @@ import { convertHTMLToLexical, editorConfigFactory } from '@payloadcms/richtext- import { del as blobDel } from '@vercel/blob' import { getPayload } from 'payload' +import { assertLocalDatabase } from './assert-local-database.mjs' + // Mirror Next.js precedence (.env.local wins over .env) without clobbering a // DATABASE_URL already set in the real environment. loadEnv({ path: '.env.local' }) @@ -36,49 +38,11 @@ loadEnv({ path: '.env' }) const config = (await import('../src/payload.config.ts')).default -// --------------------------------------------------------------------------- -// DB-safety guard (same shape as scripts/guard-dev-db.mjs) -// --------------------------------------------------------------------------- - -const OVERRIDE_FLAG = 'ALLOW_REMOTE_DB' -const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '0.0.0.0', '::1', 'postgres']) - const die = (message) => { console.error(`\n[seed:posts] ${message}\n`) process.exit(1) } -const assertLocalDatabase = () => { - const databaseUrl = process.env.DATABASE_URL - if (!databaseUrl) die('DATABASE_URL is not set. Refusing to seed.') - - if (process.env[OVERRIDE_FLAG] === 'true' || process.env[OVERRIDE_FLAG] === '1') { - console.warn( - `\n[seed:posts] ${OVERRIDE_FLAG} is set — seeding a remote database on purpose. Be careful.\n`, - ) - return - } - - let host - try { - host = new URL(databaseUrl).hostname - } catch { - die('DATABASE_URL is not a valid connection string.') - } - - if (!LOCAL_HOSTS.has(host)) { - die( - `DATABASE_URL points at a non-local host ("${host}").\n` + - 'This seed WRITES data (posts/tags/media) and is meant for the local Postgres.\n\n' + - ' 1. pnpm db:start\n' + - ' 2. set DATABASE_URL=postgresql://teqo:teqo@localhost:5432/teqo in .env.local\n\n' + - `If you REALLY mean to seed a remote database, re-run with:\n ${OVERRIDE_FLAG}=true pnpm db:seed:posts`, - ) - } - - console.log(`[seed:posts] OK — using local database host "${host}".`) -} - // --------------------------------------------------------------------------- // Taxonomy (Section 7): the categories + control tag, and the per-article map. // Keys are the WordPress slugs of the live articles. Every post gets exactly @@ -450,7 +414,12 @@ async function ensureCoverMedia(payload, { slug, coverUrl, alt }) { // --------------------------------------------------------------------------- async function main() { - assertLocalDatabase() + assertLocalDatabase( + 'seed:posts', + 'This seed WRITES data (posts/tags/media) and is meant for the local Postgres.\n' + + ' 1. pnpm db:start\n' + + ' 2. set DATABASE_URL=postgresql://teqo:teqo@localhost:5432/teqo in .env.local', + ) const payload = await getPayload({ config }) const editorConfig = await editorConfigFactory.default({ config: payload.config }) diff --git a/scripts/seed-tse-results.mjs b/scripts/seed-tse-results.mjs new file mode 100644 index 00000000..c00b959d --- /dev/null +++ b/scripts/seed-tse-results.mjs @@ -0,0 +1,173 @@ +/** + * Seeds electionTally / electionCandidateVote / electionCandidate from TSE + * open data for the 2022 general election (Bahia scope). + * + * Provenance (do not strip): + * - Portal: https://dadosabertos.tse.jus.br/dataset/resultados-2022 + * - Portal: https://dadosabertos.tse.jus.br/dataset/candidatos-2022 + * - votacao_candidato_munzona_2022.zip + * https://cdn.tse.jus.br/estatistica/sead/odsele/votacao_candidato_munzona/votacao_candidato_munzona_2022.zip + * - detalhe_votacao_munzona_2022.zip + * https://cdn.tse.jus.br/estatistica/sead/odsele/detalhe_votacao_munzona/detalhe_votacao_munzona_2022.zip + * - consulta_cand_2022.zip + * https://cdn.tse.jus.br/estatistica/sead/odsele/consulta_cand/consulta_cand_2022.zip + * - License: Creative Commons Atribuição (dados abertos TSE) + * - electionCandidateVote.votes ← QT_VOTOS_NOMINAIS (votos apurados; validade no tally) + * - v1: only voteType=nominal (no votacao_partido_munzona / legenda rows) + * + * Safety: refuses non-local DATABASE_URL unless ALLOW_REMOTE_DB=true. + * Idempotent: replace-by-scope (year, office, turn) via drizzle transaction. + * + * Usage: + * pnpm db:seed:tse + * ALLOW_REMOTE_DB=true pnpm db:seed:tse + * TSE_CACHE_DIR=./data/tse pnpm db:seed:tse # reuse downloaded zips + */ + +import { createHash } from 'node:crypto' +import { mkdir, readFile, writeFile, access } from 'node:fs/promises' +import { join } from 'node:path' +import { config as loadEnv } from 'dotenv' +import { getPayload } from 'payload' + +import { assertLocalDatabase } from './assert-local-database.mjs' + +loadEnv({ path: '.env.local' }) +loadEnv({ path: '.env' }) + +const config = (await import('../src/payload.config.ts')).default +const { parseTseCsvBuffer } = await import('../src/lib/electionResultsCsv.ts') +const { buildElectionResultsFromCsvRows } = await import('../src/lib/electionResultsBuild.ts') +const { downloadToBuffer, readZipEntry } = await import('../src/lib/electionResultsZip.ts') +const { buildImportBundles, importElectionBundles } = await import( + '../src/utilities/electionResultsImport.ts' +) + +const SOURCES = [ + { + key: 'votacao_candidato_munzona_2022', + url: 'https://cdn.tse.jus.br/estatistica/sead/odsele/votacao_candidato_munzona/votacao_candidato_munzona_2022.zip', + }, + { + key: 'detalhe_votacao_munzona_2022', + url: 'https://cdn.tse.jus.br/estatistica/sead/odsele/detalhe_votacao_munzona/detalhe_votacao_munzona_2022.zip', + }, + { + key: 'consulta_cand_2022', + url: 'https://cdn.tse.jus.br/estatistica/sead/odsele/consulta_cand/consulta_cand_2022.zip', + }, +] + +const die = (message) => { + console.error(`\n[seed:tse] ${message}\n`) + process.exit(1) +} + +const sha256 = (buffer) => createHash('sha256').update(buffer).digest('hex') + +const cacheDir = () => process.env.TSE_CACHE_DIR || join(process.cwd(), 'data', 'tse') + +const ensureCachedZip = async ({ key, url }) => { + const dir = cacheDir() + await mkdir(dir, { recursive: true }) + const path = join(dir, `${key}.zip`) + try { + await access(path) + console.log(`[seed:tse] cache hit ${path}`) + const buffer = await readFile(path) + return { key, url, buffer, hash: sha256(buffer) } + } catch (error) { + if (error?.code !== 'ENOENT') throw error + } + console.log(`[seed:tse] downloading ${url}`) + const buffer = await downloadToBuffer(url) + const hash = sha256(buffer) + await writeFile(path, buffer) + console.log(`[seed:tse] saved ${path} (${buffer.length} bytes, sha256=${hash})`) + return { key, url, buffer, hash } +} + +const loadCsvFromZip = async (zipBuffer, fileName) => { + const entry = await readZipEntry(zipBuffer, (name) => name.endsWith(fileName)) + if (!entry) die(`ZIP entry not found: ${fileName}`) + console.log(`[seed:tse] parsing ${entry.fileName} (${entry.buffer.length} bytes)`) + return parseTseCsvBuffer(entry.buffer) +} + +const main = async () => { + assertLocalDatabase( + 'seed:tse', + 'This seed WRITES election reference data and is meant for the local Postgres.\n' + + ' 1. pnpm db:start\n' + + ' 2. set DATABASE_URL=postgresql://teqo:teqo@localhost:5432/teqo in .env.local', + ) + + const [votacaoZip, detalheZip, candZip] = await Promise.all(SOURCES.map(ensureCachedZip)) + + console.log('[seed:tse] provenance:') + for (const source of [votacaoZip, detalheZip, candZip]) { + console.log(` - ${source.url}`) + console.log(` sha256=${source.hash}`) + } + + // Sequential: BA vote CSV alone is hundreds of MB; parallel parse spikes peak RAM. + const voteBa = await loadCsvFromZip(votacaoZip.buffer, 'votacao_candidato_munzona_2022_BA.csv') + const voteBr = await loadCsvFromZip(votacaoZip.buffer, 'votacao_candidato_munzona_2022_BR.csv') + const detalheBa = await loadCsvFromZip( + detalheZip.buffer, + 'detalhe_votacao_munzona_2022_BA.csv', + ) + const detalheBr = await loadCsvFromZip( + detalheZip.buffer, + 'detalhe_votacao_munzona_2022_BR.csv', + ) + const candBa = await loadCsvFromZip(candZip.buffer, 'consulta_cand_2022_BA.csv') + const candBr = await loadCsvFromZip(candZip.buffer, 'consulta_cand_2022_BR.csv') + + const built = buildElectionResultsFromCsvRows({ + voteRows: [...voteBa, ...voteBr], + detalheRows: [...detalheBa, ...detalheBr], + candBaRows: candBa, + candBrRows: candBr, + }) + + if (built.unknownMunicipalities.length > 0) { + die( + `Municípios TSE sem mapeamento canônico (${built.unknownMunicipalities.length}):\n - ` + + built.unknownMunicipalities.slice(0, 20).join('\n - ') + + (built.unknownMunicipalities.length > 20 ? '\n - …' : ''), + ) + } + + console.log( + `[seed:tse] parsed votes=${built.votes.length} tallies=${built.tallies.length} candidates=${built.candidates.length}`, + ) + + const bundles = buildImportBundles(built) + console.log(`[seed:tse] importing ${bundles.length} scopes…`) + + const payload = await getPayload({ config }) + const counts = await importElectionBundles(payload, bundles) + + let votes = 0 + let tallies = 0 + let candidates = 0 + for (const count of counts) { + votes += count.votesInserted + tallies += count.talliesInserted + candidates += count.candidatesInserted + console.log( + ` ${count.scope.office} turn=${count.scope.turn}: votes=${count.votesInserted} tallies=${count.talliesInserted} candidates=${count.candidatesInserted}`, + ) + } + + console.log( + `\n[seed:tse] done. inserted votes=${votes} tallies=${tallies} candidates=${candidates}`, + ) + process.exit(0) +} + +main().catch((error) => { + console.error('[seed:tse] fatal:', error) + process.exit(1) +}) diff --git a/src/collections/ElectionCandidate.ts b/src/collections/ElectionCandidate.ts new file mode 100644 index 00000000..bf09832f --- /dev/null +++ b/src/collections/ElectionCandidate.ts @@ -0,0 +1,159 @@ +import type { CollectionConfig } from 'payload' + +import { canMutateElectionData, canReadElectionData } from '@/utilities/campaignAccess' + +import { + ELECTION_ELECTED_BY_OPTIONS, + ELECTION_OFFICE_OPTIONS, + ELECTION_RUNNING_AGAIN_OPTIONS, + ELECTION_TURN_OPTIONS, +} from '@/lib/electionResults' + +const slug = 'electionCandidate' + +export const ElectionCandidate: CollectionConfig = { + slug, + labels: { + singular: 'Candidato eleitoral', + plural: 'Candidatos eleitorais', + }, + admin: { + group: 'Dados Eleitorais', + useAsTitle: 'urnaName', + defaultColumns: [ + 'year', + 'office', + 'candidateNumber', + 'urnaName', + 'party', + 'elected', + 'updatedAt', + ], + description: 'Registro de candidatura TSE (dado público).', + }, + access: { + create: canMutateElectionData, + read: canReadElectionData, + update: canMutateElectionData, + delete: canMutateElectionData, + }, + indexes: [ + { + fields: ['year', 'office', 'turn', 'state', 'candidateNumber'], + unique: true, + }, + ], + fields: [ + { + name: 'year', + type: 'number', + label: 'Ano', + required: true, + index: true, + min: 2000, + max: 2100, + }, + { + name: 'office', + type: 'select', + label: 'Cargo', + required: true, + index: true, + options: [...ELECTION_OFFICE_OPTIONS], + }, + { + name: 'turn', + type: 'select', + label: 'Turno', + required: true, + index: true, + options: [...ELECTION_TURN_OPTIONS], + }, + { + name: 'state', + type: 'text', + label: 'UF', + required: true, + defaultValue: 'BA', + maxLength: 2, + index: true, + }, + { + name: 'candidateNumber', + type: 'number', + label: 'Número do candidato', + required: true, + index: true, + min: 0, + }, + { + name: 'urnaName', + type: 'text', + label: 'Nome na urna', + required: true, + maxLength: 160, + }, + { + name: 'completeName', + type: 'text', + label: 'Nome completo', + maxLength: 200, + }, + { + name: 'party', + type: 'text', + label: 'Partido', + maxLength: 40, + }, + { + name: 'coalition', + type: 'text', + label: 'Coligação', + maxLength: 240, + }, + { + name: 'candidateStatus', + type: 'text', + label: 'Situação da candidatura', + maxLength: 80, + }, + { + name: 'elected', + type: 'checkbox', + label: 'Eleito', + defaultValue: false, + index: true, + }, + { + name: 'electedBy', + type: 'select', + label: 'Forma de eleição', + options: [...ELECTION_ELECTED_BY_OPTIONS], + }, + { + name: 'totalVotesState', + type: 'number', + label: 'Total de votos no estado', + min: 0, + }, + { + name: 'identityKey', + type: 'text', + label: 'Chave de identidade cross-ano', + index: true, + maxLength: 64, + admin: { + description: 'sha256(nome urna + naturalidade + partido); só dados públicos.', + readOnly: true, + }, + }, + { + name: 'runningAgain2026', + type: 'select', + label: 'Concorre de novo em 2026', + required: true, + defaultValue: 'desconhecido', + options: [...ELECTION_RUNNING_AGAIN_OPTIONS], + }, + ], +} diff --git a/src/collections/ElectionCandidateVote.ts b/src/collections/ElectionCandidateVote.ts new file mode 100644 index 00000000..786eea81 --- /dev/null +++ b/src/collections/ElectionCandidateVote.ts @@ -0,0 +1,158 @@ +import type { CollectionConfig } from 'payload' + +import { canMutateElectionData, canReadElectionData } from '@/utilities/campaignAccess' + +import { + ELECTION_OFFICE_OPTIONS, + ELECTION_TURN_OPTIONS, + ELECTION_VOTE_TYPE_OPTIONS, +} from '@/lib/electionResults' + +const slug = 'electionCandidateVote' + +export const ElectionCandidateVote: CollectionConfig = { + slug, + labels: { + singular: 'Voto por candidato', + plural: 'Votos por candidato', + }, + admin: { + group: 'Dados Eleitorais', + useAsTitle: 'candidateName', + defaultColumns: [ + 'year', + 'office', + 'cityName', + 'zoneNumber', + 'candidateNumber', + 'votes', + 'updatedAt', + ], + description: 'Votos nominais oficiais TSE por município e zona (dado público).', + }, + access: { + create: canMutateElectionData, + read: canReadElectionData, + update: canMutateElectionData, + delete: canMutateElectionData, + }, + indexes: [ + { + fields: [ + 'year', + 'office', + 'turn', + 'state', + 'cityCode', + 'zoneNumber', + 'candidateNumber', + 'voteType', + ], + unique: true, + }, + ], + fields: [ + { + name: 'year', + type: 'number', + label: 'Ano', + required: true, + index: true, + min: 2000, + max: 2100, + }, + { + name: 'office', + type: 'select', + label: 'Cargo', + required: true, + index: true, + options: [...ELECTION_OFFICE_OPTIONS], + }, + { + name: 'turn', + type: 'select', + label: 'Turno', + required: true, + index: true, + options: [...ELECTION_TURN_OPTIONS], + }, + { + name: 'state', + type: 'text', + label: 'UF', + required: true, + defaultValue: 'BA', + maxLength: 2, + index: true, + }, + { + name: 'cityCode', + type: 'text', + label: 'Código do município (TSE)', + required: true, + index: true, + maxLength: 16, + }, + { + name: 'cityName', + type: 'text', + label: 'Município', + required: true, + index: true, + maxLength: 120, + }, + { + name: 'zoneNumber', + type: 'number', + label: 'Zona TSE', + required: true, + index: true, + min: 1, + max: 999, + }, + { + name: 'candidateNumber', + type: 'number', + label: 'Número do candidato', + required: true, + index: true, + min: 0, + }, + { + name: 'candidateName', + type: 'text', + label: 'Nome na urna', + required: true, + maxLength: 160, + }, + { + name: 'coalition', + type: 'text', + label: 'Coligação', + maxLength: 240, + }, + { + name: 'party', + type: 'text', + label: 'Partido', + maxLength: 40, + }, + { + name: 'voteType', + type: 'select', + label: 'Tipo de voto', + required: true, + index: true, + defaultValue: 'nominal', + options: [...ELECTION_VOTE_TYPE_OPTIONS], + }, + { + name: 'votes', + type: 'number', + label: 'Votos', + required: true, + min: 1, + }, + ], +} diff --git a/src/collections/ElectionTally.ts b/src/collections/ElectionTally.ts new file mode 100644 index 00000000..8bbc9f28 --- /dev/null +++ b/src/collections/ElectionTally.ts @@ -0,0 +1,188 @@ +import type { CollectionConfig } from 'payload' + +import { canMutateElectionData, canReadElectionData } from '@/utilities/campaignAccess' + +import { ELECTION_OFFICE_OPTIONS, ELECTION_TURN_OPTIONS } from '@/lib/electionResults' + +const slug = 'electionTally' + +export const ElectionTally: CollectionConfig = { + slug, + labels: { + singular: 'Apuração eleitoral', + plural: 'Apurações eleitorais', + }, + admin: { + group: 'Dados Eleitorais', + useAsTitle: 'cityName', + defaultColumns: ['year', 'office', 'turn', 'cityName', 'zoneNumber', 'updatedAt'], + description: 'Totais oficiais TSE por município e zona (dado público).', + }, + access: { + create: canMutateElectionData, + read: canReadElectionData, + update: canMutateElectionData, + delete: canMutateElectionData, + }, + indexes: [ + { + fields: ['year', 'office', 'turn', 'state', 'cityCode', 'zoneNumber'], + unique: true, + }, + ], + fields: [ + { + name: 'year', + type: 'number', + label: 'Ano', + required: true, + index: true, + min: 2000, + max: 2100, + }, + { + name: 'office', + type: 'select', + label: 'Cargo', + required: true, + index: true, + options: [...ELECTION_OFFICE_OPTIONS], + }, + { + name: 'turn', + type: 'select', + label: 'Turno', + required: true, + index: true, + options: [...ELECTION_TURN_OPTIONS], + }, + { + name: 'state', + type: 'text', + label: 'UF', + required: true, + defaultValue: 'BA', + maxLength: 2, + index: true, + }, + { + name: 'cityCode', + type: 'text', + label: 'Código do município (TSE)', + required: true, + index: true, + maxLength: 16, + }, + { + name: 'cityName', + type: 'text', + label: 'Município', + required: true, + index: true, + maxLength: 120, + }, + { + name: 'zoneNumber', + type: 'number', + label: 'Zona TSE', + required: true, + index: true, + min: 1, + max: 999, + }, + { + name: 'aptos', + type: 'number', + label: 'Eleitores aptos', + required: true, + min: 0, + }, + { + name: 'comparecimento', + type: 'number', + label: 'Comparecimento', + required: true, + min: 0, + }, + { + name: 'abstencoes', + type: 'number', + label: 'Abstenções', + required: true, + min: 0, + }, + { + name: 'votosValidos', + type: 'number', + label: 'Votos válidos', + required: true, + min: 0, + }, + { + name: 'votosNominaisValidos', + type: 'number', + label: 'Votos nominais válidos', + required: true, + min: 0, + }, + { + name: 'votosLegenda', + type: 'number', + label: 'Votos de legenda', + required: true, + min: 0, + }, + { + name: 'votosBranco', + type: 'number', + label: 'Votos em branco', + required: true, + min: 0, + }, + { + name: 'votosNulo', + type: 'number', + label: 'Votos nulos', + required: true, + min: 0, + }, + { + name: 'votosAnulados', + type: 'number', + label: 'Votos anulados', + required: true, + min: 0, + defaultValue: 0, + }, + { + name: 'winnerCandidateNumber', + type: 'number', + label: 'Número do mais votado', + min: 0, + }, + { + name: 'winnerCandidateName', + type: 'text', + label: 'Nome do mais votado', + maxLength: 160, + }, + { + name: 'winnerVotes', + type: 'number', + label: 'Votos do mais votado', + min: 0, + }, + { + name: 'winnerCoalition', + type: 'text', + label: 'Coligação do mais votado', + maxLength: 240, + }, + { + name: 'winnerParty', + type: 'text', + label: 'Partido do mais votado', + maxLength: 40, + }, + ], +} diff --git a/src/lib/electionCandidateIdentity.ts b/src/lib/electionCandidateIdentity.ts new file mode 100644 index 00000000..a845f5ab --- /dev/null +++ b/src/lib/electionCandidateIdentity.ts @@ -0,0 +1,30 @@ +import { createHash } from 'node:crypto' + +import { normalizeMunicipalityKey } from '@/lib/electionResults' + +/** Same NFD/case fold as municipality keys; empty string for missing parts. */ +export const normalizeIdentityPart = (value: string | null | undefined): string => + normalizeMunicipalityKey(value ?? '') + +export type IdentityKeyInput = { + urnaName: string | null | undefined + birthCity: string | null | undefined + birthState: string | null | undefined + party: string | null | undefined +} + +/** + * identityKey = sha256(normalize(urnaName) + birthCity + birthState + party) + * Collisions (homonyms) and false negatives (party change) are expected; + * admin review sets runningAgain2026 before relying on the match. + */ +export const computeIdentityKey = (input: IdentityKeyInput): string => { + const material = [ + normalizeIdentityPart(input.urnaName), + normalizeIdentityPart(input.birthCity), + normalizeIdentityPart(input.birthState), + normalizeIdentityPart(input.party), + ].join('|') + + return createHash('sha256').update(material, 'utf8').digest('hex') +} diff --git a/src/lib/electionResults.ts b/src/lib/electionResults.ts new file mode 100644 index 00000000..a6e9b87e --- /dev/null +++ b/src/lib/electionResults.ts @@ -0,0 +1,317 @@ +import { bahiaMunicipalities } from '@/lib/bahiaTerritories' + +export const ELECTION_YEAR_2022 = 2022 as const + +export const ELECTION_OFFICE_OPTIONS = [ + { label: 'Presidente', value: 'presidente' }, + { label: 'Governador', value: 'governador' }, + { label: 'Deputado federal', value: 'deputado_federal' }, + { label: 'Deputado estadual', value: 'deputado_estadual' }, +] as const + +export const ELECTION_OFFICES = ELECTION_OFFICE_OPTIONS.map((o) => o.value) +export type ElectionOffice = (typeof ELECTION_OFFICES)[number] + +export const ELECTION_TURN_OPTIONS = [ + { label: '1º turno', value: '1' }, + { label: '2º turno', value: '2' }, +] as const + +export const ELECTION_TURNS = ELECTION_TURN_OPTIONS.map((o) => o.value) +export type ElectionTurn = (typeof ELECTION_TURNS)[number] + +export const ELECTION_VOTE_TYPE_OPTIONS = [ + { label: 'Nominal', value: 'nominal' }, + { label: 'Legenda', value: 'legenda' }, +] as const + +export const ELECTION_VOTE_TYPES = ELECTION_VOTE_TYPE_OPTIONS.map((o) => o.value) +export type ElectionVoteType = (typeof ELECTION_VOTE_TYPES)[number] + +export const ELECTION_ELECTED_BY_OPTIONS = [ + { label: 'Quociente partidário', value: 'QP' }, + { label: 'Média', value: 'média' }, + { label: '2º turno', value: '2º turno' }, +] as const + +export const ELECTION_ELECTED_BY = ELECTION_ELECTED_BY_OPTIONS.map((o) => o.value) +export type ElectionElectedBy = (typeof ELECTION_ELECTED_BY)[number] + +export const ELECTION_RUNNING_AGAIN_OPTIONS = [ + { label: 'Sim', value: 'sim' }, + { label: 'Não', value: 'nao' }, + { label: 'Desconhecido', value: 'desconhecido' }, +] as const + +export const ELECTION_RUNNING_AGAIN = ELECTION_RUNNING_AGAIN_OPTIONS.map((o) => o.value) +export type ElectionRunningAgain = (typeof ELECTION_RUNNING_AGAIN)[number] + +/** Solla — dep. federal BA, PT, nº 1313 (eleito em 2022). */ +export const SOLLA_CANDIDATE_NUMBER = 1313 +export const LULA_CANDIDATE_NUMBER = 13 +export const JERONIMO_CANDIDATE_NUMBER = 13 + +/** TSE CD_CARGO → office enum used in collections. */ +export const TSE_CARGO_TO_OFFICE: Readonly> = { + '1': 'presidente', + '3': 'governador', + '6': 'deputado_federal', + '7': 'deputado_estadual', +} + +export type ParsedElectedStatus = { + elected: boolean + electedBy: ElectionElectedBy | null +} + +/** + * Map TSE DS_SIT_TOT_TURNO to elected flag + electedBy. + * Values observed in 2022 consulta_cand / votacao datasets. + * When `turn` is `2` and the candidate is elected, electedBy defaults to `2º turno` + * (TSE often writes plain "ELEITO" on the second-turn file). + */ +export const parseElectedStatus = ( + dsSitTotTurno: string | null | undefined, + turn?: ElectionTurn | null, +): ParsedElectedStatus => { + const value = (dsSitTotTurno ?? '').trim().toUpperCase() + if (!value || value === '#NULO#' || value === 'NÃO ELEITO' || value === 'NAO ELEITO') { + return { elected: false, electedBy: null } + } + if (value.includes('2º TURNO') || value.includes('2O TURNO') || value.includes('2° TURNO')) { + // "ELEITO … 2º TURNO" counts as elected; bare "2º TURNO" means advanced only. + if (value.startsWith('ELEITO')) { + return { elected: true, electedBy: '2º turno' } + } + return { elected: false, electedBy: null } + } + if (value.includes('MÉDIA') || value.includes('MEDIA')) { + return { elected: true, electedBy: 'média' } + } + if (value.startsWith('ELEITO')) { + return { elected: true, electedBy: turn === '2' ? '2º turno' : 'QP' } + } + return { elected: false, electedBy: null } +} + +/** Fold accents and case for municipality matching. */ +export const normalizeMunicipalityKey = (value: string): string => + value + .normalize('NFD') + .replace(/\p{M}/gu, '') + .replace(/['’`]/g, '') + .replace(/\s+/g, ' ') + .trim() + .toUpperCase() + +const bahiaMunicipalityByKey = new Map( + bahiaMunicipalities.map((name) => [normalizeMunicipalityKey(name), name]), +) + +/** + * Known TSE NM_MUNICIPIO spellings that diverge from the SEPLAN/SECULT canonical + * names in bahiaTerritories (2022 open-data files). Keys are normalizeMunicipalityKey outputs. + */ +const TSE_MUNICIPALITY_ALIASES: Readonly> = { + // TSE: CAMACÃ → canonical Camacan + CAMACA: 'Camacan', + // TSE: "DIAS D ÁVILA" (space, no apostrophe) → Dias d'Ávila + 'DIAS D AVILA': "Dias d'Ávila", + // TSE: MUQUÉM DO SÃO FRANCISCO → Muquém de São Francisco + 'MUQUEM DO SAO FRANCISCO': 'Muquém de São Francisco', + // TSE: SANTA TEREZINHA → Santa Teresinha + 'SANTA TEREZINHA': 'Santa Teresinha', +} + +for (const [aliasKey, canonical] of Object.entries(TSE_MUNICIPALITY_ALIASES)) { + if (!bahiaMunicipalityByKey.has(normalizeMunicipalityKey(canonical))) { + throw new Error(`TSE alias points to unknown canonical municipality: ${canonical}`) + } + bahiaMunicipalityByKey.set(aliasKey, canonical) +} + +export class UnknownMunicipalityError extends Error { + constructor(public readonly tseName: string) { + super(`Município TSE sem mapeamento canônico: "${tseName}"`) + this.name = 'UnknownMunicipalityError' + } +} + +/** Cache raw TSE NM_MUNICIPIO → canonical (millions of repeated lookups on seed). */ +const municipalityResolveCache = new Map() + +/** + * Map TSE NM_MUNICIPIO (usually ALL CAPS) to the canonical Bahia municipality name + * used by bahiaTerritories / electoralNucleus.city. Fails closed on unknown names. + */ +export const canonicalizeMunicipalityName = (tseMunicipalityName: string): string => { + const cached = municipalityResolveCache.get(tseMunicipalityName) + if (cached) return cached + + const key = normalizeMunicipalityKey(tseMunicipalityName) + const canonical = bahiaMunicipalityByKey.get(key) + if (!canonical) { + throw new UnknownMunicipalityError(tseMunicipalityName) + } + municipalityResolveCache.set(tseMunicipalityName, canonical) + return canonical +} + +export const isElectionTurn = (value: string): value is ElectionTurn => + (ELECTION_TURNS as readonly string[]).includes(value) + +export type CandidateVoteRow = { + year: number + office: ElectionOffice + turn: ElectionTurn + state: string + cityCode: string + cityName: string + zoneNumber: number + candidateNumber: number + candidateName: string + coalition: string | null + party: string | null + voteType: ElectionVoteType + votes: number +} + +/** Winner fields as stored on electionTally (null when no nominal votes in that cell). */ +export type TallyWinner = { + winnerCandidateNumber: number | null + winnerCandidateName: string | null + winnerVotes: number | null + winnerCoalition: string | null + winnerParty: string | null +} + +type ResolvedWinner = { + winnerCandidateNumber: number + winnerCandidateName: string + winnerVotes: number + winnerCoalition: string | null + winnerParty: string | null +} + +const EMPTY_WINNER: TallyWinner = { + winnerCandidateNumber: null, + winnerCandidateName: null, + winnerVotes: null, + winnerCoalition: null, + winnerParty: null, +} + +export const winnerKey = (cityCode: string, zoneNumber: number, office: ElectionOffice, turn: ElectionTurn) => + `${cityCode}|${zoneNumber}|${office}|${turn}` + +/** + * Compute the local winner (highest nominal votes) for each city+zone+office+turn. + * Ties: first seen candidate with the max vote count wins (stable for import order). + */ +export const computeWinnersByScope = ( + votes: readonly CandidateVoteRow[], +): Map => { + const winners = new Map() + + for (const row of votes) { + if (row.voteType !== 'nominal' || row.votes <= 0) continue + const key = winnerKey(row.cityCode, row.zoneNumber, row.office, row.turn) + const current = winners.get(key) + if (!current || current.winnerVotes < row.votes) { + winners.set(key, { + winnerCandidateNumber: row.candidateNumber, + winnerCandidateName: row.candidateName, + winnerVotes: row.votes, + winnerCoalition: row.coalition, + winnerParty: row.party, + }) + } + } + + return winners +} + +export type TseDetalheApuracaoRow = { + year: number + office: ElectionOffice + turn: ElectionTurn + state: string + cityCode: string + cityName: string + zoneNumber: number + aptos: number + comparecimento: number + abstencoes: number + votosValidos: number + votosNominaisValidos: number + votosLegenda: number + votosBranco: number + votosNulo: number + votosAnulados: number +} + +export type ElectionTallyRow = TseDetalheApuracaoRow & TallyWinner + +export type ElectionCandidateRow = { + year: number + office: ElectionOffice + turn: ElectionTurn + state: string + candidateNumber: number + urnaName: string + completeName: string | null + party: string | null + coalition: string | null + candidateStatus: string | null + elected: boolean + electedBy: ElectionElectedBy | null + totalVotesState: number + identityKey: string + runningAgain2026: ElectionRunningAgain +} + +export const mergeTallyWithWinners = ( + tallies: readonly TseDetalheApuracaoRow[], + winners: ReadonlyMap, +): ElectionTallyRow[] => + tallies.map((tally) => { + const winner = + winners.get(winnerKey(tally.cityCode, tally.zoneNumber, tally.office, tally.turn)) ?? + EMPTY_WINNER + return { ...tally, ...winner } + }) + +/** Parse a TSE integer cell that may be "#NULO#" / empty. */ +export const parseTseInt = (value: string | undefined | null, fallback = 0): number => { + if (value == null) return fallback + const trimmed = value.trim() + if (!trimmed || trimmed === '#NULO#' || trimmed === '#NE#') return fallback + const n = Number(trimmed) + return Number.isFinite(n) ? n : fallback +} + +export const parseTseOptionalText = (value: string | undefined | null): string | null => { + if (value == null) return null + const trimmed = value.trim() + if (!trimmed || trimmed === '#NULO#' || trimmed === '#NE#') return null + return trimmed +} + +export const officeFromTseCargo = (cdCargo: string): ElectionOffice | null => { + const office = TSE_CARGO_TO_OFFICE[cdCargo.trim()] + return office ?? null +} + +export const turnFromTse = (nrTurno: string): ElectionTurn | null => { + const turn = nrTurno.trim() + return isElectionTurn(turn) ? turn : null +} + +export const assertAllCanonicalMunicipalitiesResolvable = (): void => { + for (const name of bahiaMunicipalities) { + const resolved = canonicalizeMunicipalityName(name) + if (resolved !== name) { + throw new Error(`canonicalizeMunicipalityName drifted for "${name}" → "${resolved}"`) + } + } +} diff --git a/src/lib/electionResultsBuild.ts b/src/lib/electionResultsBuild.ts new file mode 100644 index 00000000..5d24c6a4 --- /dev/null +++ b/src/lib/electionResultsBuild.ts @@ -0,0 +1,98 @@ +import type { + CandidateVoteRow, + ElectionCandidateRow, + ElectionOffice, + TseDetalheApuracaoRow, +} from '@/lib/electionResults' +import { ELECTION_OFFICES, ELECTION_YEAR_2022, UnknownMunicipalityError } from '@/lib/electionResults' +import { + applyStateVoteTotals, + dedupeCandidates, + parseCandidateVoteRow, + parseConsultaCandRow, + parseDetalheApuracaoRow, + type TseCsvRow, +} from '@/lib/electionResultsParse' + +const ALL_OFFICES = new Set(ELECTION_OFFICES) +const STATE_OFFICES = new Set([ + 'governador', + 'deputado_federal', + 'deputado_estadual', +]) +const PRESIDENT_OFFICE = new Set(['presidente']) + +export type BuiltElectionResults = { + votes: CandidateVoteRow[] + tallies: TseDetalheApuracaoRow[] + candidates: ElectionCandidateRow[] + unknownMunicipalities: string[] +} + +const collectParsed = ( + rows: readonly TseCsvRow[], + year: number, + unknownMunicipalities: Set, + parseRow: (row: TseCsvRow) => T | null, +): T[] => { + const out: T[] = [] + for (const row of rows) { + try { + const parsed = parseRow(row) + if (parsed && parsed.year === year) out.push(parsed) + } catch (error) { + if (error instanceof UnknownMunicipalityError) { + unknownMunicipalities.add(error.tseName) + continue + } + throw error + } + } + return out +} + +/** + * Build typed election rows from already-parsed TSE CSV tables. + * Safe for fixtures and for the full seed (no I/O). + */ +export const buildElectionResultsFromCsvRows = (args: { + voteRows: readonly TseCsvRow[] + detalheRows: readonly TseCsvRow[] + candBaRows: readonly TseCsvRow[] + candBrRows: readonly TseCsvRow[] + year?: number +}): BuiltElectionResults => { + const year = args.year ?? ELECTION_YEAR_2022 + const unknownMunicipalities = new Set() + + const votes = collectParsed(args.voteRows, year, unknownMunicipalities, (row) => + parseCandidateVoteRow(row, { stateFilter: 'BA', offices: ALL_OFFICES }), + ) + const tallies = collectParsed(args.detalheRows, year, unknownMunicipalities, (row) => + parseDetalheApuracaoRow(row, { stateFilter: 'BA', offices: ALL_OFFICES }), + ) + + const candidatesRaw: ElectionCandidateRow[] = [] + for (const row of args.candBaRows) { + const parsed = parseConsultaCandRow(row, { + stateFilter: 'BA', + offices: STATE_OFFICES, + }) + if (parsed && parsed.year === year) candidatesRaw.push(parsed) + } + for (const row of args.candBrRows) { + const parsed = parseConsultaCandRow(row, { + stateFilter: 'BR', + offices: PRESIDENT_OFFICE, + forceState: 'BA', + }) + if (parsed && parsed.year === year) candidatesRaw.push(parsed) + } + + return { + votes, + tallies, + candidates: applyStateVoteTotals(dedupeCandidates(candidatesRaw), votes), + unknownMunicipalities: [...unknownMunicipalities].sort(), + } +} diff --git a/src/lib/electionResultsCsv.ts b/src/lib/electionResultsCsv.ts new file mode 100644 index 00000000..75362b70 --- /dev/null +++ b/src/lib/electionResultsCsv.ts @@ -0,0 +1,22 @@ +import { parse } from 'csv-parse/sync' + +import type { TseCsvRow } from '@/lib/electionResultsParse' + +/** + * Parse a TSE open-data CSV buffer (Latin-1 / ISO-8859-1, `;` separator, quoted fields). + */ +export const parseTseCsvBuffer = (buffer: Buffer): TseCsvRow[] => { + const text = buffer.toString('latin1') + return parse(text, { + columns: true, + delimiter: ';', + relax_quotes: true, + relax_column_count: true, + skip_empty_lines: true, + trim: true, + bom: true, + }) as TseCsvRow[] +} + +export const parseTseCsvString = (text: string): TseCsvRow[] => + parseTseCsvBuffer(Buffer.from(text, 'utf8')) diff --git a/src/lib/electionResultsParse.ts b/src/lib/electionResultsParse.ts new file mode 100644 index 00000000..ce171761 --- /dev/null +++ b/src/lib/electionResultsParse.ts @@ -0,0 +1,220 @@ +/** Pure parsers for TSE open-data CSVs (Latin-1, `;`-separated). */ + +import { + canonicalizeMunicipalityName, + officeFromTseCargo, + parseElectedStatus, + parseTseInt, + parseTseOptionalText, + turnFromTse, + type CandidateVoteRow, + type ElectionCandidateRow, + type ElectionOffice, + type ElectionTurn, + type TseDetalheApuracaoRow, +} from '@/lib/electionResults' +import { computeIdentityKey } from '@/lib/electionCandidateIdentity' + +export type TseCsvRow = Record + +const cell = (row: TseCsvRow, key: string): string => row[key] ?? '' + +type ScopeParseOptions = { + /** Uppercase UF (e.g. BA). Compared as-is against SG_UF — normalize at the call site. */ + stateFilter?: string + offices?: ReadonlySet +} + +const matchScope = ( + row: TseCsvRow, + options: ScopeParseOptions, +): { state: string; office: ElectionOffice; turn: ElectionTurn } | null => { + const state = cell(row, 'SG_UF').trim().toUpperCase() + if (options.stateFilter && state !== options.stateFilter) return null + + const office = officeFromTseCargo(cell(row, 'CD_CARGO')) + if (!office) return null + if (options.offices && !options.offices.has(office)) return null + + const turn = turnFromTse(cell(row, 'NR_TURNO')) + if (!turn) return null + + return { state, office, turn } +} + +export type ParseVoteOptions = ScopeParseOptions & { + /** Skip zero-vote rows (default true — collection requires votes >= 1). */ + skipZeroVotes?: boolean +} + +/** + * Parse a votacao_candidato_munzona row into a CandidateVoteRow. + * Returns null when the row is out of scope (wrong UF/cargo) or has zero votes. + */ +export const parseCandidateVoteRow = ( + row: TseCsvRow, + options: ParseVoteOptions = {}, +): CandidateVoteRow | null => { + const scope = matchScope(row, options) + if (!scope) return null + + const votes = parseTseInt(cell(row, 'QT_VOTOS_NOMINAIS')) + if ((options.skipZeroVotes ?? true) && votes <= 0) return null + + return { + year: parseTseInt(cell(row, 'ANO_ELEICAO')), + office: scope.office, + turn: scope.turn, + state: scope.state, + cityCode: cell(row, 'CD_MUNICIPIO').trim(), + cityName: canonicalizeMunicipalityName(cell(row, 'NM_MUNICIPIO')), + zoneNumber: parseTseInt(cell(row, 'NR_ZONA')), + candidateNumber: parseTseInt(cell(row, 'NR_CANDIDATO')), + candidateName: parseTseOptionalText(cell(row, 'NM_URNA_CANDIDATO')) ?? '', + coalition: parseTseOptionalText(cell(row, 'NM_COLIGACAO')), + party: parseTseOptionalText(cell(row, 'SG_PARTIDO')), + voteType: 'nominal', + votes, + } +} + +export type ParseDetalheOptions = ScopeParseOptions + +/** + * Parse detalhe_votacao_munzona into a tally row (without winner — filled later). + */ +export const parseDetalheApuracaoRow = ( + row: TseCsvRow, + options: ParseDetalheOptions = {}, +): TseDetalheApuracaoRow | null => { + const scope = matchScope(row, options) + if (!scope) return null + + // Prefer QT_VOTOS_VALIDOS when present; else nominais_validos + legenda. + const votosNominaisValidos = parseTseInt( + cell(row, 'QT_VOTOS_NOMINAIS_VALIDOS') || cell(row, 'QT_VOTOS_NOMINAIS'), + ) + const votosLegenda = parseTseInt(cell(row, 'QT_VOTOS_LEGENDA')) + const votosValidos = + parseTseInt(cell(row, 'QT_VOTOS_VALIDOS')) || votosNominaisValidos + votosLegenda + + return { + year: parseTseInt(cell(row, 'ANO_ELEICAO')), + office: scope.office, + turn: scope.turn, + state: scope.state, + cityCode: cell(row, 'CD_MUNICIPIO').trim(), + cityName: canonicalizeMunicipalityName(cell(row, 'NM_MUNICIPIO')), + zoneNumber: parseTseInt(cell(row, 'NR_ZONA')), + aptos: parseTseInt(cell(row, 'QT_APTOS')), + comparecimento: parseTseInt(cell(row, 'QT_COMPARECIMENTO')), + abstencoes: parseTseInt(cell(row, 'QT_ABSTENCOES')), + votosValidos, + votosNominaisValidos, + votosLegenda, + votosBranco: parseTseInt(cell(row, 'QT_VOTOS_BRANCOS')), + votosNulo: parseTseInt(cell(row, 'QT_VOTOS_NULOS')), + votosAnulados: parseTseInt( + cell(row, 'QT_VOTOS_ANULADOS') || cell(row, 'QT_VOTOS_ANULADOS_APU_SEP'), + ), + } +} + +export type ParseCandidateOptions = ScopeParseOptions & { + /** Override state stored (presidente candidacies are national; store BA for our model). */ + forceState?: string +} + +/** + * Parse consulta_cand into ElectionCandidateRow. + * President candidates live in the BR file (SG_UF=BR); we store state=BA for the BA campaign model. + */ +export const parseConsultaCandRow = ( + row: TseCsvRow, + options: ParseCandidateOptions = {}, +): ElectionCandidateRow | null => { + const scope = matchScope(row, options) + if (!scope) return null + + const urnaName = parseTseOptionalText(cell(row, 'NM_URNA_CANDIDATO')) ?? '' + const party = parseTseOptionalText(cell(row, 'SG_PARTIDO')) + const birthCity = parseTseOptionalText(cell(row, 'NM_MUNICIPIO_NASCIMENTO')) + const birthState = parseTseOptionalText(cell(row, 'SG_UF_NASCIMENTO')) + const { elected, electedBy } = parseElectedStatus(cell(row, 'DS_SIT_TOT_TURNO'), scope.turn) + + return { + year: parseTseInt(cell(row, 'ANO_ELEICAO')), + office: scope.office, + turn: scope.turn, + state: (options.forceState ?? scope.state).toUpperCase(), + candidateNumber: parseTseInt(cell(row, 'NR_CANDIDATO')), + urnaName, + completeName: parseTseOptionalText(cell(row, 'NM_CANDIDATO')), + party, + coalition: parseTseOptionalText(cell(row, 'NM_COLIGACAO')), + candidateStatus: parseTseOptionalText(cell(row, 'DS_SITUACAO_CANDIDATURA')), + elected, + electedBy, + totalVotesState: 0, + identityKey: computeIdentityKey({ urnaName, birthCity, birthState, party }), + runningAgain2026: 'desconhecido', + } +} + +export type ScopeKey = { + year: number + office: ElectionOffice + turn: ElectionTurn +} + +export const scopeKeyString = (scope: ScopeKey): string => + `${scope.year}|${scope.office}|${scope.turn}` + +const candidateAggregateKey = (row: { + year: number + office: ElectionOffice + turn: ElectionTurn + state: string + candidateNumber: number +}): string => + `${row.year}|${row.office}|${row.turn}|${row.state}|${row.candidateNumber}` + +/** Deduplicate candidate rows by unique key, preferring elected=true when colliding. */ +export const dedupeCandidates = (rows: readonly ElectionCandidateRow[]): ElectionCandidateRow[] => { + const map = new Map() + for (const row of rows) { + const key = candidateAggregateKey(row) + const existing = map.get(key) + if (!existing || (!existing.elected && row.elected)) { + map.set(key, row) + } + } + return [...map.values()] +} + +/** Aggregate nominal votes per candidate across all zones → totalVotesState. */ +export const applyStateVoteTotals = ( + candidates: readonly ElectionCandidateRow[], + votes: readonly CandidateVoteRow[], +): ElectionCandidateRow[] => { + const totals = new Map() + for (const vote of votes) { + const key = candidateAggregateKey(vote) + totals.set(key, (totals.get(key) ?? 0) + vote.votes) + } + return candidates.map((candidate) => ({ + ...candidate, + totalVotesState: totals.get(candidateAggregateKey(candidate)) ?? 0, + })) +} + +export const groupByScope = (rows: readonly T[]): Map => { + const map = new Map() + for (const row of rows) { + const key = scopeKeyString(row) + const list = map.get(key) + if (list) list.push(row) + else map.set(key, [row]) + } + return map +} diff --git a/src/lib/electionResultsZip.ts b/src/lib/electionResultsZip.ts new file mode 100644 index 00000000..5f0c9f12 --- /dev/null +++ b/src/lib/electionResultsZip.ts @@ -0,0 +1,69 @@ +import { fromBuffer as yauzlFromBuffer } from 'yauzl' + +/** + * Read a single entry from a ZIP buffer by exact filename or suffix match. + * Returns null when no matching entry exists. + */ +export const readZipEntry = async ( + zipBuffer: Buffer, + matcher: string | ((fileName: string) => boolean), +): Promise<{ fileName: string; buffer: Buffer } | null> => { + const match = + typeof matcher === 'string' + ? (name: string) => name === matcher || name.endsWith(`/${matcher}`) || name.endsWith(matcher) + : matcher + + return new Promise((resolve, reject) => { + yauzlFromBuffer(zipBuffer, { lazyEntries: true }, (err, zipfile) => { + if (err || !zipfile) { + reject(err ?? new Error('Falha ao abrir ZIP')) + return + } + + let settled = false + const finish = (value: { fileName: string; buffer: Buffer } | null) => { + if (settled) return + settled = true + try { + zipfile.close() + } catch { + // ignore close errors after resolve + } + resolve(value) + } + + zipfile.readEntry() + zipfile.on('entry', (entry) => { + if (settled) return + if (/\/$/.test(entry.fileName) || !match(entry.fileName)) { + zipfile.readEntry() + return + } + zipfile.openReadStream(entry, (streamErr, readStream) => { + if (streamErr || !readStream) { + reject(streamErr ?? new Error(`Falha ao ler ${entry.fileName}`)) + return + } + const chunks: Buffer[] = [] + readStream.on('data', (chunk: Buffer) => chunks.push(chunk)) + readStream.on('error', reject) + readStream.on('end', () => { + finish({ fileName: entry.fileName, buffer: Buffer.concat(chunks) }) + }) + }) + }) + zipfile.on('end', () => { + finish(null) + }) + zipfile.on('error', reject) + }) + }) +} + +export const downloadToBuffer = async (url: string): Promise => { + const response = await fetch(url) + if (!response.ok) { + throw new Error(`Download falhou (${response.status}): ${url}`) + } + return Buffer.from(await response.arrayBuffer()) +} diff --git a/src/migrations/20260718_195854_add_election_results.json b/src/migrations/20260718_195854_add_election_results.json new file mode 100644 index 00000000..6147a1aa --- /dev/null +++ b/src/migrations/20260718_195854_add_election_results.json @@ -0,0 +1,7709 @@ +{ + "version": "7", + "dialect": "postgresql", + "tables": { + "public.users_sessions": { + "name": "users_sessions", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "users_sessions_order_idx": { + "name": "users_sessions_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_sessions_parent_id_idx": { + "name": "users_sessions_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_sessions_parent_id_fk": { + "name": "users_sessions_parent_id_fk", + "tableFrom": "users_sessions", + "tableTo": "users", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "reset_password_token": { + "name": "reset_password_token", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "reset_password_expiration": { + "name": "reset_password_expiration", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "salt": { + "name": "salt", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "hash": { + "name": "hash", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "login_attempts": { + "name": "login_attempts", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "lock_until": { + "name": "lock_until", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "users_updated_at_idx": { + "name": "users_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_created_at_idx": { + "name": "users_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_email_idx": { + "name": "users_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.campaign_user_sessions": { + "name": "campaign_user_sessions", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "campaign_user_sessions_order_idx": { + "name": "campaign_user_sessions_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaign_user_sessions_parent_id_idx": { + "name": "campaign_user_sessions_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "campaign_user_sessions_parent_id_fk": { + "name": "campaign_user_sessions_parent_id_fk", + "tableFrom": "campaign_user_sessions", + "tableTo": "campaign_user", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.campaign_user": { + "name": "campaign_user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "enum_campaign_user_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'lideranca'" + }, + "phone": { + "name": "phone", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "reset_password_token": { + "name": "reset_password_token", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "reset_password_expiration": { + "name": "reset_password_expiration", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "salt": { + "name": "salt", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "hash": { + "name": "hash", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "login_attempts": { + "name": "login_attempts", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "lock_until": { + "name": "lock_until", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "campaign_user_phone_idx": { + "name": "campaign_user_phone_idx", + "columns": [ + { + "expression": "phone", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaign_user_updated_at_idx": { + "name": "campaign_user_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaign_user_created_at_idx": { + "name": "campaign_user_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaign_user_email_idx": { + "name": "campaign_user_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaign_user_username_idx": { + "name": "campaign_user_username_idx", + "columns": [ + { + "expression": "username", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.campaign_invite": { + "name": "campaign_invite", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "leadership_id": { + "name": "leadership_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "enum_campaign_invite_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_id": { + "name": "created_by_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "campaign_invite_token_hash_idx": { + "name": "campaign_invite_token_hash_idx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaign_invite_leadership_idx": { + "name": "campaign_invite_leadership_idx", + "columns": [ + { + "expression": "leadership_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaign_invite_kind_idx": { + "name": "campaign_invite_kind_idx", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaign_invite_expires_at_idx": { + "name": "campaign_invite_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaign_invite_used_at_idx": { + "name": "campaign_invite_used_at_idx", + "columns": [ + { + "expression": "used_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaign_invite_revoked_at_idx": { + "name": "campaign_invite_revoked_at_idx", + "columns": [ + { + "expression": "revoked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaign_invite_created_by_idx": { + "name": "campaign_invite_created_by_idx", + "columns": [ + { + "expression": "created_by_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaign_invite_updated_at_idx": { + "name": "campaign_invite_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaign_invite_created_at_idx": { + "name": "campaign_invite_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "leadership_kind_idx": { + "name": "leadership_kind_idx", + "columns": [ + { + "expression": "leadership_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "campaign_invite_leadership_id_leadership_id_fk": { + "name": "campaign_invite_leadership_id_leadership_id_fk", + "tableFrom": "campaign_invite", + "tableTo": "leadership", + "columnsFrom": [ + "leadership_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "campaign_invite_created_by_id_campaign_user_id_fk": { + "name": "campaign_invite_created_by_id_campaign_user_id_fk", + "tableFrom": "campaign_invite", + "tableTo": "campaign_user", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.electoral_nucleus_tse_zones": { + "name": "electoral_nucleus_tse_zones", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "zone_number": { + "name": "zone_number", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "electoral_nucleus_tse_zones_order_idx": { + "name": "electoral_nucleus_tse_zones_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "electoral_nucleus_tse_zones_parent_id_idx": { + "name": "electoral_nucleus_tse_zones_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "electoral_nucleus_tse_zones_parent_id_fk": { + "name": "electoral_nucleus_tse_zones_parent_id_fk", + "tableFrom": "electoral_nucleus_tse_zones", + "tableTo": "electoral_nucleus", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.electoral_nucleus_voter_profiles": { + "name": "electoral_nucleus_voter_profiles", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "age_range": { + "name": "age_range", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "income_band": { + "name": "income_band", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "occupation": { + "name": "occupation", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "local_traits": { + "name": "local_traits", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "electoral_nucleus_voter_profiles_order_idx": { + "name": "electoral_nucleus_voter_profiles_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "electoral_nucleus_voter_profiles_parent_id_idx": { + "name": "electoral_nucleus_voter_profiles_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "electoral_nucleus_voter_profiles_parent_id_fk": { + "name": "electoral_nucleus_voter_profiles_parent_id_fk", + "tableFrom": "electoral_nucleus_voter_profiles", + "tableTo": "electoral_nucleus", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.electoral_nucleus_strengths": { + "name": "electoral_nucleus_strengths", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "text": { + "name": "text", + "type": "varchar", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "electoral_nucleus_strengths_order_idx": { + "name": "electoral_nucleus_strengths_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "electoral_nucleus_strengths_parent_id_idx": { + "name": "electoral_nucleus_strengths_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "electoral_nucleus_strengths_parent_id_fk": { + "name": "electoral_nucleus_strengths_parent_id_fk", + "tableFrom": "electoral_nucleus_strengths", + "tableTo": "electoral_nucleus", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.electoral_nucleus_risks": { + "name": "electoral_nucleus_risks", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "text": { + "name": "text", + "type": "varchar", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "electoral_nucleus_risks_order_idx": { + "name": "electoral_nucleus_risks_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "electoral_nucleus_risks_parent_id_idx": { + "name": "electoral_nucleus_risks_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "electoral_nucleus_risks_parent_id_fk": { + "name": "electoral_nucleus_risks_parent_id_fk", + "tableFrom": "electoral_nucleus_risks", + "tableTo": "electoral_nucleus", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.electoral_nucleus": { + "name": "electoral_nucleus", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "enum_electoral_nucleus_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ativo'" + }, + "region": { + "name": "region", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "city": { + "name": "city", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "neighborhood": { + "name": "neighborhood", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "locality": { + "name": "locality", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "territory_notes": { + "name": "territory_notes", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "organization_kind": { + "name": "organization_kind", + "type": "enum_electoral_nucleus_organization_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'territorial'" + }, + "organization_label": { + "name": "organization_label", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sector_kind": { + "name": "sector_kind", + "type": "enum_electoral_nucleus_sector_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "primary_contact_id": { + "name": "primary_contact_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "confirmed_vote_estimate": { + "name": "confirmed_vote_estimate", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "confirmed_vote_estimate_at": { + "name": "confirmed_vote_estimate_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "confirmed_vote_estimate_by_id": { + "name": "confirmed_vote_estimate_by_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "confirmation_note": { + "name": "confirmation_note", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "proposed_vote_estimate": { + "name": "proposed_vote_estimate", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "proposed_vote_estimate_at": { + "name": "proposed_vote_estimate_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "proposed_vote_estimate_by_id": { + "name": "proposed_vote_estimate_by_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "proposed_vote_estimate_version": { + "name": "proposed_vote_estimate_version", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "ticket_alliance_partner_name": { + "name": "ticket_alliance_partner_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "ticket_alliance_office": { + "name": "ticket_alliance_office", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "ticket_alliance_is_campaign_partner": { + "name": "ticket_alliance_is_campaign_partner", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ticket_alliance_notes": { + "name": "ticket_alliance_notes", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "last_update_at": { + "name": "last_update_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_id": { + "name": "created_by_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "electoral_nucleus_name_idx": { + "name": "electoral_nucleus_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "electoral_nucleus_slug_idx": { + "name": "electoral_nucleus_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "electoral_nucleus_status_idx": { + "name": "electoral_nucleus_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "electoral_nucleus_region_idx": { + "name": "electoral_nucleus_region_idx", + "columns": [ + { + "expression": "region", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "electoral_nucleus_city_idx": { + "name": "electoral_nucleus_city_idx", + "columns": [ + { + "expression": "city", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "electoral_nucleus_organization_kind_idx": { + "name": "electoral_nucleus_organization_kind_idx", + "columns": [ + { + "expression": "organization_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "electoral_nucleus_sector_kind_idx": { + "name": "electoral_nucleus_sector_kind_idx", + "columns": [ + { + "expression": "sector_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "electoral_nucleus_primary_contact_idx": { + "name": "electoral_nucleus_primary_contact_idx", + "columns": [ + { + "expression": "primary_contact_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "electoral_nucleus_confirmed_vote_estimate_idx": { + "name": "electoral_nucleus_confirmed_vote_estimate_idx", + "columns": [ + { + "expression": "confirmed_vote_estimate", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "electoral_nucleus_confirmed_vote_estimate_by_idx": { + "name": "electoral_nucleus_confirmed_vote_estimate_by_idx", + "columns": [ + { + "expression": "confirmed_vote_estimate_by_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "electoral_nucleus_proposed_vote_estimate_idx": { + "name": "electoral_nucleus_proposed_vote_estimate_idx", + "columns": [ + { + "expression": "proposed_vote_estimate", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "electoral_nucleus_proposed_vote_estimate_by_idx": { + "name": "electoral_nucleus_proposed_vote_estimate_by_idx", + "columns": [ + { + "expression": "proposed_vote_estimate_by_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "electoral_nucleus_proposed_vote_estimate_version_idx": { + "name": "electoral_nucleus_proposed_vote_estimate_version_idx", + "columns": [ + { + "expression": "proposed_vote_estimate_version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "electoral_nucleus_last_update_at_idx": { + "name": "electoral_nucleus_last_update_at_idx", + "columns": [ + { + "expression": "last_update_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "electoral_nucleus_created_by_idx": { + "name": "electoral_nucleus_created_by_idx", + "columns": [ + { + "expression": "created_by_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "electoral_nucleus_updated_at_idx": { + "name": "electoral_nucleus_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "electoral_nucleus_created_at_idx": { + "name": "electoral_nucleus_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "electoral_nucleus_primary_contact_id_contact_id_fk": { + "name": "electoral_nucleus_primary_contact_id_contact_id_fk", + "tableFrom": "electoral_nucleus", + "tableTo": "contact", + "columnsFrom": [ + "primary_contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "electoral_nucleus_confirmed_vote_estimate_by_id_campaign_user_id_fk": { + "name": "electoral_nucleus_confirmed_vote_estimate_by_id_campaign_user_id_fk", + "tableFrom": "electoral_nucleus", + "tableTo": "campaign_user", + "columnsFrom": [ + "confirmed_vote_estimate_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "electoral_nucleus_proposed_vote_estimate_by_id_campaign_user_id_fk": { + "name": "electoral_nucleus_proposed_vote_estimate_by_id_campaign_user_id_fk", + "tableFrom": "electoral_nucleus", + "tableTo": "campaign_user", + "columnsFrom": [ + "proposed_vote_estimate_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "electoral_nucleus_created_by_id_campaign_user_id_fk": { + "name": "electoral_nucleus_created_by_id_campaign_user_id_fk", + "tableFrom": "electoral_nucleus", + "tableTo": "campaign_user", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.electoral_nucleus_rels": { + "name": "electoral_nucleus_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "campaign_user_id": { + "name": "campaign_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "electoral_nucleus_rels_order_idx": { + "name": "electoral_nucleus_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "electoral_nucleus_rels_parent_idx": { + "name": "electoral_nucleus_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "electoral_nucleus_rels_path_idx": { + "name": "electoral_nucleus_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "electoral_nucleus_rels_campaign_user_id_idx": { + "name": "electoral_nucleus_rels_campaign_user_id_idx", + "columns": [ + { + "expression": "campaign_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "electoral_nucleus_rels_parent_fk": { + "name": "electoral_nucleus_rels_parent_fk", + "tableFrom": "electoral_nucleus_rels", + "tableTo": "electoral_nucleus", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "electoral_nucleus_rels_campaign_user_fk": { + "name": "electoral_nucleus_rels_campaign_user_fk", + "tableFrom": "electoral_nucleus_rels", + "tableTo": "campaign_user", + "columnsFrom": [ + "campaign_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.leadership": { + "name": "leadership", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "nucleus_id": { + "name": "nucleus_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sector": { + "name": "sector", + "type": "enum_leadership_sector", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "sector_notes": { + "name": "sector_notes", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "support_status": { + "name": "support_status", + "type": "enum_leadership_support_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'a_abordar'" + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "consent_id": { + "name": "consent_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "consent_content_hash": { + "name": "consent_content_hash", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "consented_at": { + "name": "consented_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "consent_note": { + "name": "consent_note", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "created_by_id": { + "name": "created_by_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "leadership_contact_idx": { + "name": "leadership_contact_idx", + "columns": [ + { + "expression": "contact_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "leadership_nucleus_idx": { + "name": "leadership_nucleus_idx", + "columns": [ + { + "expression": "nucleus_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "leadership_sector_idx": { + "name": "leadership_sector_idx", + "columns": [ + { + "expression": "sector", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "leadership_support_status_idx": { + "name": "leadership_support_status_idx", + "columns": [ + { + "expression": "support_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "leadership_user_idx": { + "name": "leadership_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "leadership_consent_idx": { + "name": "leadership_consent_idx", + "columns": [ + { + "expression": "consent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "leadership_created_by_idx": { + "name": "leadership_created_by_idx", + "columns": [ + { + "expression": "created_by_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "leadership_updated_at_idx": { + "name": "leadership_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "leadership_created_at_idx": { + "name": "leadership_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "contact_nucleus_idx": { + "name": "contact_nucleus_idx", + "columns": [ + { + "expression": "contact_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "nucleus_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "leadership_contact_id_contact_id_fk": { + "name": "leadership_contact_id_contact_id_fk", + "tableFrom": "leadership", + "tableTo": "contact", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "leadership_nucleus_id_electoral_nucleus_id_fk": { + "name": "leadership_nucleus_id_electoral_nucleus_id_fk", + "tableFrom": "leadership", + "tableTo": "electoral_nucleus", + "columnsFrom": [ + "nucleus_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "leadership_user_id_campaign_user_id_fk": { + "name": "leadership_user_id_campaign_user_id_fk", + "tableFrom": "leadership", + "tableTo": "campaign_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "leadership_consent_id_consent_id_fk": { + "name": "leadership_consent_id_consent_id_fk", + "tableFrom": "leadership", + "tableTo": "consent", + "columnsFrom": [ + "consent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "leadership_created_by_id_campaign_user_id_fk": { + "name": "leadership_created_by_id_campaign_user_id_fk", + "tableFrom": "leadership", + "tableTo": "campaign_user", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.nucleus_update": { + "name": "nucleus_update", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "nucleus_id": { + "name": "nucleus_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "enum_nucleus_update_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'semanal'" + }, + "worked": { + "name": "worked", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "failed": { + "name": "failed", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "needs": { + "name": "needs", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "active_volunteers": { + "name": "active_volunteers", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "new_supports": { + "name": "new_supports", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "nucleus_update_nucleus_idx": { + "name": "nucleus_update_nucleus_idx", + "columns": [ + { + "expression": "nucleus_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "nucleus_update_author_idx": { + "name": "nucleus_update_author_idx", + "columns": [ + { + "expression": "author_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "nucleus_update_kind_idx": { + "name": "nucleus_update_kind_idx", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "nucleus_update_updated_at_idx": { + "name": "nucleus_update_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "nucleus_update_created_at_idx": { + "name": "nucleus_update_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "nucleus_update_nucleus_id_electoral_nucleus_id_fk": { + "name": "nucleus_update_nucleus_id_electoral_nucleus_id_fk", + "tableFrom": "nucleus_update", + "tableTo": "electoral_nucleus", + "columnsFrom": [ + "nucleus_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "nucleus_update_author_id_campaign_user_id_fk": { + "name": "nucleus_update_author_id_campaign_user_id_fk", + "tableFrom": "nucleus_update", + "tableTo": "campaign_user", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.election_tally": { + "name": "election_tally", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "year": { + "name": "year", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "office": { + "name": "office", + "type": "enum_election_tally_office", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "turn": { + "name": "turn", + "type": "enum_election_tally_turn", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'BA'" + }, + "city_code": { + "name": "city_code", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "city_name": { + "name": "city_name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "zone_number": { + "name": "zone_number", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "aptos": { + "name": "aptos", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "comparecimento": { + "name": "comparecimento", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "abstencoes": { + "name": "abstencoes", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "votos_validos": { + "name": "votos_validos", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "votos_nominais_validos": { + "name": "votos_nominais_validos", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "votos_legenda": { + "name": "votos_legenda", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "votos_branco": { + "name": "votos_branco", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "votos_nulo": { + "name": "votos_nulo", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "votos_anulados": { + "name": "votos_anulados", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "winner_candidate_number": { + "name": "winner_candidate_number", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "winner_candidate_name": { + "name": "winner_candidate_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "winner_votes": { + "name": "winner_votes", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "winner_coalition": { + "name": "winner_coalition", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "winner_party": { + "name": "winner_party", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "election_tally_year_idx": { + "name": "election_tally_year_idx", + "columns": [ + { + "expression": "year", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "election_tally_office_idx": { + "name": "election_tally_office_idx", + "columns": [ + { + "expression": "office", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "election_tally_turn_idx": { + "name": "election_tally_turn_idx", + "columns": [ + { + "expression": "turn", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "election_tally_state_idx": { + "name": "election_tally_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "election_tally_city_code_idx": { + "name": "election_tally_city_code_idx", + "columns": [ + { + "expression": "city_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "election_tally_city_name_idx": { + "name": "election_tally_city_name_idx", + "columns": [ + { + "expression": "city_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "election_tally_zone_number_idx": { + "name": "election_tally_zone_number_idx", + "columns": [ + { + "expression": "zone_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "election_tally_updated_at_idx": { + "name": "election_tally_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "election_tally_created_at_idx": { + "name": "election_tally_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "year_office_turn_state_cityCode_zoneNumber_idx": { + "name": "year_office_turn_state_cityCode_zoneNumber_idx", + "columns": [ + { + "expression": "year", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "office", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "turn", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "city_code", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "zone_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.election_candidate_vote": { + "name": "election_candidate_vote", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "year": { + "name": "year", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "office": { + "name": "office", + "type": "enum_election_candidate_vote_office", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "turn": { + "name": "turn", + "type": "enum_election_candidate_vote_turn", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'BA'" + }, + "city_code": { + "name": "city_code", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "city_name": { + "name": "city_name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "zone_number": { + "name": "zone_number", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "candidate_number": { + "name": "candidate_number", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "candidate_name": { + "name": "candidate_name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "coalition": { + "name": "coalition", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "party": { + "name": "party", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "vote_type": { + "name": "vote_type", + "type": "enum_election_candidate_vote_vote_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'nominal'" + }, + "votes": { + "name": "votes", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "election_candidate_vote_year_idx": { + "name": "election_candidate_vote_year_idx", + "columns": [ + { + "expression": "year", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "election_candidate_vote_office_idx": { + "name": "election_candidate_vote_office_idx", + "columns": [ + { + "expression": "office", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "election_candidate_vote_turn_idx": { + "name": "election_candidate_vote_turn_idx", + "columns": [ + { + "expression": "turn", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "election_candidate_vote_state_idx": { + "name": "election_candidate_vote_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "election_candidate_vote_city_code_idx": { + "name": "election_candidate_vote_city_code_idx", + "columns": [ + { + "expression": "city_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "election_candidate_vote_city_name_idx": { + "name": "election_candidate_vote_city_name_idx", + "columns": [ + { + "expression": "city_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "election_candidate_vote_zone_number_idx": { + "name": "election_candidate_vote_zone_number_idx", + "columns": [ + { + "expression": "zone_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "election_candidate_vote_candidate_number_idx": { + "name": "election_candidate_vote_candidate_number_idx", + "columns": [ + { + "expression": "candidate_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "election_candidate_vote_vote_type_idx": { + "name": "election_candidate_vote_vote_type_idx", + "columns": [ + { + "expression": "vote_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "election_candidate_vote_updated_at_idx": { + "name": "election_candidate_vote_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "election_candidate_vote_created_at_idx": { + "name": "election_candidate_vote_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "compound_index_idx": { + "name": "compound_index_idx", + "columns": [ + { + "expression": "year", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "office", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "turn", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "city_code", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "zone_number", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "candidate_number", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "vote_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.election_candidate": { + "name": "election_candidate", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "year": { + "name": "year", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "office": { + "name": "office", + "type": "enum_election_candidate_office", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "turn": { + "name": "turn", + "type": "enum_election_candidate_turn", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'BA'" + }, + "candidate_number": { + "name": "candidate_number", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "urna_name": { + "name": "urna_name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "complete_name": { + "name": "complete_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "party": { + "name": "party", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "coalition": { + "name": "coalition", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "candidate_status": { + "name": "candidate_status", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "elected": { + "name": "elected", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "elected_by": { + "name": "elected_by", + "type": "enum_election_candidate_elected_by", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "total_votes_state": { + "name": "total_votes_state", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "identity_key": { + "name": "identity_key", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "running_again2026": { + "name": "running_again2026", + "type": "enum_election_candidate_running_again2026", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'desconhecido'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "election_candidate_year_idx": { + "name": "election_candidate_year_idx", + "columns": [ + { + "expression": "year", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "election_candidate_office_idx": { + "name": "election_candidate_office_idx", + "columns": [ + { + "expression": "office", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "election_candidate_turn_idx": { + "name": "election_candidate_turn_idx", + "columns": [ + { + "expression": "turn", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "election_candidate_state_idx": { + "name": "election_candidate_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "election_candidate_candidate_number_idx": { + "name": "election_candidate_candidate_number_idx", + "columns": [ + { + "expression": "candidate_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "election_candidate_elected_idx": { + "name": "election_candidate_elected_idx", + "columns": [ + { + "expression": "elected", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "election_candidate_identity_key_idx": { + "name": "election_candidate_identity_key_idx", + "columns": [ + { + "expression": "identity_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "election_candidate_updated_at_idx": { + "name": "election_candidate_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "election_candidate_created_at_idx": { + "name": "election_candidate_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "year_office_turn_state_candidateNumber_idx": { + "name": "year_office_turn_state_candidateNumber_idx", + "columns": [ + { + "expression": "year", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "office", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "turn", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "candidate_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.media": { + "name": "media", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "alt": { + "name": "alt", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "thumbnail_u_r_l": { + "name": "thumbnail_u_r_l", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "filename": { + "name": "filename", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "filesize": { + "name": "filesize", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "focal_x": { + "name": "focal_x", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "focal_y": { + "name": "focal_y", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "media_updated_at_idx": { + "name": "media_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_created_at_idx": { + "name": "media_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_filename_idx": { + "name": "media_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.petition": { + "name": "petition", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "subtitle": { + "name": "subtitle", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "body": { + "name": "body", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "form_title": { + "name": "form_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "form_subtitle": { + "name": "form_subtitle", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "form_consent_id": { + "name": "form_consent_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "_status": { + "name": "_status", + "type": "enum_petition_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'draft'" + } + }, + "indexes": { + "petition_form_form_consent_idx": { + "name": "petition_form_form_consent_idx", + "columns": [ + { + "expression": "form_consent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "petition_updated_at_idx": { + "name": "petition_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "petition_created_at_idx": { + "name": "petition_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "petition__status_idx": { + "name": "petition__status_idx", + "columns": [ + { + "expression": "_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "petition_form_consent_id_consent_id_fk": { + "name": "petition_form_consent_id_consent_id_fk", + "tableFrom": "petition", + "tableTo": "consent", + "columnsFrom": [ + "form_consent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._petition_v": { + "name": "_petition_v", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_title": { + "name": "version_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_subtitle": { + "name": "version_subtitle", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_enabled": { + "name": "version_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "version_body": { + "name": "version_body", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "version_form_title": { + "name": "version_form_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_form_subtitle": { + "name": "version_form_subtitle", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_form_consent_id": { + "name": "version_form_consent_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_updated_at": { + "name": "version_updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_created_at": { + "name": "version_created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version__status": { + "name": "version__status", + "type": "enum__petition_v_version_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'draft'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "latest": { + "name": "latest", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "_petition_v_parent_idx": { + "name": "_petition_v_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_petition_v_version_form_version_form_consent_idx": { + "name": "_petition_v_version_form_version_form_consent_idx", + "columns": [ + { + "expression": "version_form_consent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_petition_v_version_version_updated_at_idx": { + "name": "_petition_v_version_version_updated_at_idx", + "columns": [ + { + "expression": "version_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_petition_v_version_version_created_at_idx": { + "name": "_petition_v_version_version_created_at_idx", + "columns": [ + { + "expression": "version_created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_petition_v_version_version__status_idx": { + "name": "_petition_v_version_version__status_idx", + "columns": [ + { + "expression": "version__status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_petition_v_created_at_idx": { + "name": "_petition_v_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_petition_v_updated_at_idx": { + "name": "_petition_v_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_petition_v_latest_idx": { + "name": "_petition_v_latest_idx", + "columns": [ + { + "expression": "latest", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_petition_v_parent_id_petition_id_fk": { + "name": "_petition_v_parent_id_petition_id_fk", + "tableFrom": "_petition_v", + "tableTo": "petition", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "_petition_v_version_form_consent_id_consent_id_fk": { + "name": "_petition_v_version_form_consent_id_consent_id_fk", + "tableFrom": "_petition_v", + "tableTo": "consent", + "columnsFrom": [ + "version_form_consent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contact": { + "name": "contact", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "gender": { + "name": "gender", + "type": "enum_contact_gender", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "enum_contact_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "city": { + "name": "city", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "postal_code": { + "name": "postal_code", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "contact_phone_idx": { + "name": "contact_phone_idx", + "columns": [ + { + "expression": "phone", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "contact_updated_at_idx": { + "name": "contact_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "contact_created_at_idx": { + "name": "contact_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.consent": { + "name": "consent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "text": { + "name": "text", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "consent_key_idx": { + "name": "consent_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "consent_updated_at_idx": { + "name": "consent_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "consent_created_at_idx": { + "name": "consent_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.signature": { + "name": "signature", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "petition_id": { + "name": "petition_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "consent_id": { + "name": "consent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "comment": { + "name": "comment", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "signature_contact_idx": { + "name": "signature_contact_idx", + "columns": [ + { + "expression": "contact_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "signature_petition_idx": { + "name": "signature_petition_idx", + "columns": [ + { + "expression": "petition_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "signature_consent_idx": { + "name": "signature_consent_idx", + "columns": [ + { + "expression": "consent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "signature_updated_at_idx": { + "name": "signature_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "signature_created_at_idx": { + "name": "signature_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "signature_contact_id_contact_id_fk": { + "name": "signature_contact_id_contact_id_fk", + "tableFrom": "signature", + "tableTo": "contact", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "signature_petition_id_petition_id_fk": { + "name": "signature_petition_id_petition_id_fk", + "tableFrom": "signature", + "tableTo": "petition", + "columnsFrom": [ + "petition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "signature_consent_id_consent_id_fk": { + "name": "signature_consent_id_consent_id_fk", + "tableFrom": "signature", + "tableTo": "consent", + "columnsFrom": [ + "consent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "consent_id": { + "name": "consent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "comment": { + "name": "comment", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "subscription_contact_idx": { + "name": "subscription_contact_idx", + "columns": [ + { + "expression": "contact_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "subscription_consent_idx": { + "name": "subscription_consent_idx", + "columns": [ + { + "expression": "consent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "subscription_updated_at_idx": { + "name": "subscription_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "subscription_created_at_idx": { + "name": "subscription_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "subscription_contact_id_contact_id_fk": { + "name": "subscription_contact_id_contact_id_fk", + "tableFrom": "subscription", + "tableTo": "contact", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "subscription_consent_id_consent_id_fk": { + "name": "subscription_consent_id_consent_id_fk", + "tableFrom": "subscription", + "tableTo": "consent", + "columnsFrom": [ + "consent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.post": { + "name": "post", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "enum_post_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "subtitle": { + "name": "subtitle", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "cover_image_id": { + "name": "cover_image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "published_date": { + "name": "published_date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "_status": { + "name": "_status", + "type": "enum_post_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'draft'" + } + }, + "indexes": { + "post_slug_idx": { + "name": "post_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "post_category_idx": { + "name": "post_category_idx", + "columns": [ + { + "expression": "category_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "post_cover_image_idx": { + "name": "post_cover_image_idx", + "columns": [ + { + "expression": "cover_image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "post_updated_at_idx": { + "name": "post_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "post_created_at_idx": { + "name": "post_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "post__status_idx": { + "name": "post__status_idx", + "columns": [ + { + "expression": "_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "post_category_id_tag_id_fk": { + "name": "post_category_id_tag_id_fk", + "tableFrom": "post", + "tableTo": "tag", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "post_cover_image_id_media_id_fk": { + "name": "post_cover_image_id_media_id_fk", + "tableFrom": "post", + "tableTo": "media", + "columnsFrom": [ + "cover_image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.post_rels": { + "name": "post_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "post_rels_order_idx": { + "name": "post_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "post_rels_parent_idx": { + "name": "post_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "post_rels_path_idx": { + "name": "post_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "post_rels_tag_id_idx": { + "name": "post_rels_tag_id_idx", + "columns": [ + { + "expression": "tag_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "post_rels_parent_fk": { + "name": "post_rels_parent_fk", + "tableFrom": "post_rels", + "tableTo": "post", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "post_rels_tag_fk": { + "name": "post_rels_tag_fk", + "tableFrom": "post_rels", + "tableTo": "tag", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._post_v": { + "name": "_post_v", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_title": { + "name": "version_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_slug": { + "name": "version_slug", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_type": { + "name": "version_type", + "type": "enum__post_v_version_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "version_category_id": { + "name": "version_category_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_subtitle": { + "name": "version_subtitle", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_cover_image_id": { + "name": "version_cover_image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_published_date": { + "name": "version_published_date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_body": { + "name": "version_body", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "version_updated_at": { + "name": "version_updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_created_at": { + "name": "version_created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version__status": { + "name": "version__status", + "type": "enum__post_v_version_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'draft'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "latest": { + "name": "latest", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "_post_v_parent_idx": { + "name": "_post_v_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_post_v_version_version_slug_idx": { + "name": "_post_v_version_version_slug_idx", + "columns": [ + { + "expression": "version_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_post_v_version_version_category_idx": { + "name": "_post_v_version_version_category_idx", + "columns": [ + { + "expression": "version_category_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_post_v_version_version_cover_image_idx": { + "name": "_post_v_version_version_cover_image_idx", + "columns": [ + { + "expression": "version_cover_image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_post_v_version_version_updated_at_idx": { + "name": "_post_v_version_version_updated_at_idx", + "columns": [ + { + "expression": "version_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_post_v_version_version_created_at_idx": { + "name": "_post_v_version_version_created_at_idx", + "columns": [ + { + "expression": "version_created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_post_v_version_version__status_idx": { + "name": "_post_v_version_version__status_idx", + "columns": [ + { + "expression": "version__status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_post_v_created_at_idx": { + "name": "_post_v_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_post_v_updated_at_idx": { + "name": "_post_v_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_post_v_latest_idx": { + "name": "_post_v_latest_idx", + "columns": [ + { + "expression": "latest", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_post_v_parent_id_post_id_fk": { + "name": "_post_v_parent_id_post_id_fk", + "tableFrom": "_post_v", + "tableTo": "post", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "_post_v_version_category_id_tag_id_fk": { + "name": "_post_v_version_category_id_tag_id_fk", + "tableFrom": "_post_v", + "tableTo": "tag", + "columnsFrom": [ + "version_category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "_post_v_version_cover_image_id_media_id_fk": { + "name": "_post_v_version_cover_image_id_media_id_fk", + "tableFrom": "_post_v", + "tableTo": "media", + "columnsFrom": [ + "version_cover_image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._post_v_rels": { + "name": "_post_v_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "_post_v_rels_order_idx": { + "name": "_post_v_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_post_v_rels_parent_idx": { + "name": "_post_v_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_post_v_rels_path_idx": { + "name": "_post_v_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_post_v_rels_tag_id_idx": { + "name": "_post_v_rels_tag_id_idx", + "columns": [ + { + "expression": "tag_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_post_v_rels_parent_fk": { + "name": "_post_v_rels_parent_fk", + "tableFrom": "_post_v_rels", + "tableTo": "_post_v", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "_post_v_rels_tag_fk": { + "name": "_post_v_rels_tag_fk", + "tableFrom": "_post_v_rels", + "tableTo": "tag", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tag": { + "name": "tag", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "hidden": { + "name": "hidden", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tag_slug_idx": { + "name": "tag_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tag_updated_at_idx": { + "name": "tag_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tag_created_at_idx": { + "name": "tag_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_kv": { + "name": "payload_kv", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "payload_kv_key_idx": { + "name": "payload_kv_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_jobs_log": { + "name": "payload_jobs_log", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "executed_at": { + "name": "executed_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + }, + "task_slug": { + "name": "task_slug", + "type": "enum_payload_jobs_log_task_slug", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "task_i_d": { + "name": "task_i_d", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "input": { + "name": "input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "enum_payload_jobs_log_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "payload_jobs_log_order_idx": { + "name": "payload_jobs_log_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_log_parent_id_idx": { + "name": "payload_jobs_log_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payload_jobs_log_parent_id_fk": { + "name": "payload_jobs_log_parent_id_fk", + "tableFrom": "payload_jobs_log", + "tableTo": "payload_jobs", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_jobs": { + "name": "payload_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "input": { + "name": "input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "total_tried": { + "name": "total_tried", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "has_error": { + "name": "has_error", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "error": { + "name": "error", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "task_slug": { + "name": "task_slug", + "type": "enum_payload_jobs_task_slug", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "queue": { + "name": "queue", + "type": "varchar", + "primaryKey": false, + "notNull": false, + "default": "'default'" + }, + "wait_until": { + "name": "wait_until", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "processing": { + "name": "processing", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payload_jobs_completed_at_idx": { + "name": "payload_jobs_completed_at_idx", + "columns": [ + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_total_tried_idx": { + "name": "payload_jobs_total_tried_idx", + "columns": [ + { + "expression": "total_tried", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_has_error_idx": { + "name": "payload_jobs_has_error_idx", + "columns": [ + { + "expression": "has_error", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_task_slug_idx": { + "name": "payload_jobs_task_slug_idx", + "columns": [ + { + "expression": "task_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_queue_idx": { + "name": "payload_jobs_queue_idx", + "columns": [ + { + "expression": "queue", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_wait_until_idx": { + "name": "payload_jobs_wait_until_idx", + "columns": [ + { + "expression": "wait_until", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_processing_idx": { + "name": "payload_jobs_processing_idx", + "columns": [ + { + "expression": "processing", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_updated_at_idx": { + "name": "payload_jobs_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_created_at_idx": { + "name": "payload_jobs_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_locked_documents": { + "name": "payload_locked_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "global_slug": { + "name": "global_slug", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payload_locked_documents_global_slug_idx": { + "name": "payload_locked_documents_global_slug_idx", + "columns": [ + { + "expression": "global_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_updated_at_idx": { + "name": "payload_locked_documents_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_created_at_idx": { + "name": "payload_locked_documents_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_locked_documents_rels": { + "name": "payload_locked_documents_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "users_id": { + "name": "users_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "campaign_user_id": { + "name": "campaign_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "campaign_invite_id": { + "name": "campaign_invite_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "electoral_nucleus_id": { + "name": "electoral_nucleus_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "leadership_id": { + "name": "leadership_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "nucleus_update_id": { + "name": "nucleus_update_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "election_tally_id": { + "name": "election_tally_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "election_candidate_vote_id": { + "name": "election_candidate_vote_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "election_candidate_id": { + "name": "election_candidate_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "media_id": { + "name": "media_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "petition_id": { + "name": "petition_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "contact_id": { + "name": "contact_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "consent_id": { + "name": "consent_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "signature_id": { + "name": "signature_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "subscription_id": { + "name": "subscription_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "post_id": { + "name": "post_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tag_id": { + "name": "tag_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "payload_locked_documents_rels_order_idx": { + "name": "payload_locked_documents_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_parent_idx": { + "name": "payload_locked_documents_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_path_idx": { + "name": "payload_locked_documents_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_users_id_idx": { + "name": "payload_locked_documents_rels_users_id_idx", + "columns": [ + { + "expression": "users_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_campaign_user_id_idx": { + "name": "payload_locked_documents_rels_campaign_user_id_idx", + "columns": [ + { + "expression": "campaign_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_campaign_invite_id_idx": { + "name": "payload_locked_documents_rels_campaign_invite_id_idx", + "columns": [ + { + "expression": "campaign_invite_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_electoral_nucleus_id_idx": { + "name": "payload_locked_documents_rels_electoral_nucleus_id_idx", + "columns": [ + { + "expression": "electoral_nucleus_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_leadership_id_idx": { + "name": "payload_locked_documents_rels_leadership_id_idx", + "columns": [ + { + "expression": "leadership_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_nucleus_update_id_idx": { + "name": "payload_locked_documents_rels_nucleus_update_id_idx", + "columns": [ + { + "expression": "nucleus_update_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_election_tally_id_idx": { + "name": "payload_locked_documents_rels_election_tally_id_idx", + "columns": [ + { + "expression": "election_tally_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_election_candidate_vote_id_idx": { + "name": "payload_locked_documents_rels_election_candidate_vote_id_idx", + "columns": [ + { + "expression": "election_candidate_vote_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_election_candidate_id_idx": { + "name": "payload_locked_documents_rels_election_candidate_id_idx", + "columns": [ + { + "expression": "election_candidate_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_media_id_idx": { + "name": "payload_locked_documents_rels_media_id_idx", + "columns": [ + { + "expression": "media_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_petition_id_idx": { + "name": "payload_locked_documents_rels_petition_id_idx", + "columns": [ + { + "expression": "petition_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_contact_id_idx": { + "name": "payload_locked_documents_rels_contact_id_idx", + "columns": [ + { + "expression": "contact_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_consent_id_idx": { + "name": "payload_locked_documents_rels_consent_id_idx", + "columns": [ + { + "expression": "consent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_signature_id_idx": { + "name": "payload_locked_documents_rels_signature_id_idx", + "columns": [ + { + "expression": "signature_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_subscription_id_idx": { + "name": "payload_locked_documents_rels_subscription_id_idx", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_post_id_idx": { + "name": "payload_locked_documents_rels_post_id_idx", + "columns": [ + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_tag_id_idx": { + "name": "payload_locked_documents_rels_tag_id_idx", + "columns": [ + { + "expression": "tag_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payload_locked_documents_rels_parent_fk": { + "name": "payload_locked_documents_rels_parent_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "payload_locked_documents", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_users_fk": { + "name": "payload_locked_documents_rels_users_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "users", + "columnsFrom": [ + "users_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_campaign_user_fk": { + "name": "payload_locked_documents_rels_campaign_user_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "campaign_user", + "columnsFrom": [ + "campaign_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_campaign_invite_fk": { + "name": "payload_locked_documents_rels_campaign_invite_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "campaign_invite", + "columnsFrom": [ + "campaign_invite_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_electoral_nucleus_fk": { + "name": "payload_locked_documents_rels_electoral_nucleus_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "electoral_nucleus", + "columnsFrom": [ + "electoral_nucleus_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_leadership_fk": { + "name": "payload_locked_documents_rels_leadership_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "leadership", + "columnsFrom": [ + "leadership_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_nucleus_update_fk": { + "name": "payload_locked_documents_rels_nucleus_update_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "nucleus_update", + "columnsFrom": [ + "nucleus_update_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_election_tally_fk": { + "name": "payload_locked_documents_rels_election_tally_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "election_tally", + "columnsFrom": [ + "election_tally_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_election_candidate_vote_fk": { + "name": "payload_locked_documents_rels_election_candidate_vote_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "election_candidate_vote", + "columnsFrom": [ + "election_candidate_vote_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_election_candidate_fk": { + "name": "payload_locked_documents_rels_election_candidate_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "election_candidate", + "columnsFrom": [ + "election_candidate_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_media_fk": { + "name": "payload_locked_documents_rels_media_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "media", + "columnsFrom": [ + "media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_petition_fk": { + "name": "payload_locked_documents_rels_petition_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "petition", + "columnsFrom": [ + "petition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_contact_fk": { + "name": "payload_locked_documents_rels_contact_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "contact", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_consent_fk": { + "name": "payload_locked_documents_rels_consent_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "consent", + "columnsFrom": [ + "consent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_signature_fk": { + "name": "payload_locked_documents_rels_signature_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "signature", + "columnsFrom": [ + "signature_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_subscription_fk": { + "name": "payload_locked_documents_rels_subscription_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "subscription", + "columnsFrom": [ + "subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_post_fk": { + "name": "payload_locked_documents_rels_post_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "post", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_tag_fk": { + "name": "payload_locked_documents_rels_tag_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "tag", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_preferences": { + "name": "payload_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payload_preferences_key_idx": { + "name": "payload_preferences_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_updated_at_idx": { + "name": "payload_preferences_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_created_at_idx": { + "name": "payload_preferences_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_preferences_rels": { + "name": "payload_preferences_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "users_id": { + "name": "users_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "campaign_user_id": { + "name": "campaign_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "payload_preferences_rels_order_idx": { + "name": "payload_preferences_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_rels_parent_idx": { + "name": "payload_preferences_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_rels_path_idx": { + "name": "payload_preferences_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_rels_users_id_idx": { + "name": "payload_preferences_rels_users_id_idx", + "columns": [ + { + "expression": "users_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_rels_campaign_user_id_idx": { + "name": "payload_preferences_rels_campaign_user_id_idx", + "columns": [ + { + "expression": "campaign_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payload_preferences_rels_parent_fk": { + "name": "payload_preferences_rels_parent_fk", + "tableFrom": "payload_preferences_rels", + "tableTo": "payload_preferences", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_preferences_rels_users_fk": { + "name": "payload_preferences_rels_users_fk", + "tableFrom": "payload_preferences_rels", + "tableTo": "users", + "columnsFrom": [ + "users_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_preferences_rels_campaign_user_fk": { + "name": "payload_preferences_rels_campaign_user_fk", + "tableFrom": "payload_preferences_rels", + "tableTo": "campaign_user", + "columnsFrom": [ + "campaign_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_migrations": { + "name": "payload_migrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "batch": { + "name": "batch", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payload_migrations_updated_at_idx": { + "name": "payload_migrations_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_migrations_created_at_idx": { + "name": "payload_migrations_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.site_settings_social_links": { + "name": "site_settings_social_links", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "enum_site_settings_social_links_platform", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "site_settings_social_links_order_idx": { + "name": "site_settings_social_links_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "site_settings_social_links_parent_id_idx": { + "name": "site_settings_social_links_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "site_settings_social_links_parent_id_fk": { + "name": "site_settings_social_links_parent_id_fk", + "tableFrom": "site_settings_social_links", + "tableTo": "site_settings", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.site_settings": { + "name": "site_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "header_title": { + "name": "header_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "_status": { + "name": "_status", + "type": "enum_site_settings_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'draft'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "site_settings__status_idx": { + "name": "site_settings__status_idx", + "columns": [ + { + "expression": "_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._site_settings_v_version_social_links": { + "name": "_site_settings_v_version_social_links", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "enum__site_settings_v_version_social_links_platform", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "_uuid": { + "name": "_uuid", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "_site_settings_v_version_social_links_order_idx": { + "name": "_site_settings_v_version_social_links_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_site_settings_v_version_social_links_parent_id_idx": { + "name": "_site_settings_v_version_social_links_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_site_settings_v_version_social_links_parent_id_fk": { + "name": "_site_settings_v_version_social_links_parent_id_fk", + "tableFrom": "_site_settings_v_version_social_links", + "tableTo": "_site_settings_v", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._site_settings_v": { + "name": "_site_settings_v", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "version_header_title": { + "name": "version_header_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version__status": { + "name": "version__status", + "type": "enum__site_settings_v_version_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'draft'" + }, + "version_updated_at": { + "name": "version_updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_created_at": { + "name": "version_created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "latest": { + "name": "latest", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "autosave": { + "name": "autosave", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "_site_settings_v_version_version__status_idx": { + "name": "_site_settings_v_version_version__status_idx", + "columns": [ + { + "expression": "version__status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_site_settings_v_created_at_idx": { + "name": "_site_settings_v_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_site_settings_v_updated_at_idx": { + "name": "_site_settings_v_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_site_settings_v_latest_idx": { + "name": "_site_settings_v_latest_idx", + "columns": [ + { + "expression": "latest", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_site_settings_v_autosave_idx": { + "name": "_site_settings_v_autosave_idx", + "columns": [ + { + "expression": "autosave", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.home": { + "name": "home", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "image_id": { + "name": "image_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "home_image_idx": { + "name": "home_image_idx", + "columns": [ + { + "expression": "image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "home_image_id_media_id_fk": { + "name": "home_image_id_media_id_fk", + "tableFrom": "home", + "tableTo": "media", + "columnsFrom": [ + "image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.metadata_keywords": { + "name": "metadata_keywords", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "keyword": { + "name": "keyword", + "type": "varchar", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "metadata_keywords_order_idx": { + "name": "metadata_keywords_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "metadata_keywords_parent_id_idx": { + "name": "metadata_keywords_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "metadata_keywords_parent_id_fk": { + "name": "metadata_keywords_parent_id_fk", + "tableFrom": "metadata_keywords", + "tableTo": "metadata", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.metadata": { + "name": "metadata", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "open_graph_site_name": { + "name": "open_graph_site_name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "open_graph_description": { + "name": "open_graph_description", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "twitter_creator": { + "name": "twitter_creator", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "twitter_description": { + "name": "twitter_description", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "image_id": { + "name": "image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "metadata_image_idx": { + "name": "metadata_image_idx", + "columns": [ + { + "expression": "image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "metadata_image_id_media_id_fk": { + "name": "metadata_image_id_media_id_fk", + "tableFrom": "metadata", + "tableTo": "media", + "columnsFrom": [ + "image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.enum_campaign_user_role": { + "name": "enum_campaign_user_role", + "schema": "public", + "values": [ + "geral", + "coordenador", + "lideranca" + ] + }, + "public.enum_campaign_invite_kind": { + "name": "enum_campaign_invite_kind", + "schema": "public", + "values": [ + "login", + "autopreenchimento" + ] + }, + "public.enum_electoral_nucleus_status": { + "name": "enum_electoral_nucleus_status", + "schema": "public", + "values": [ + "ativo", + "arquivado" + ] + }, + "public.enum_electoral_nucleus_organization_kind": { + "name": "enum_electoral_nucleus_organization_kind", + "schema": "public", + "values": [ + "territorial", + "associacao", + "sindicato", + "religioso", + "movimento", + "categoria_profissional", + "outro" + ] + }, + "public.enum_electoral_nucleus_sector_kind": { + "name": "enum_electoral_nucleus_sector_kind", + "schema": "public", + "values": [ + "rural", + "religioso", + "sindical", + "empresarial", + "juventude", + "saude", + "educacao", + "cultura", + "outro" + ] + }, + "public.enum_leadership_sector": { + "name": "enum_leadership_sector", + "schema": "public", + "values": [ + "religioso", + "sindical", + "comunitario", + "rural", + "empresarial", + "juventude", + "saude", + "educacao", + "cultura", + "outro" + ] + }, + "public.enum_leadership_support_status": { + "name": "enum_leadership_support_status", + "schema": "public", + "values": [ + "engajado", + "a_abordar", + "em_disputa", + "negativo" + ] + }, + "public.enum_nucleus_update_kind": { + "name": "enum_nucleus_update_kind", + "schema": "public", + "values": [ + "semanal", + "urgente", + "nota" + ] + }, + "public.enum_election_tally_office": { + "name": "enum_election_tally_office", + "schema": "public", + "values": [ + "presidente", + "governador", + "deputado_federal", + "deputado_estadual" + ] + }, + "public.enum_election_tally_turn": { + "name": "enum_election_tally_turn", + "schema": "public", + "values": [ + "1", + "2" + ] + }, + "public.enum_election_candidate_vote_office": { + "name": "enum_election_candidate_vote_office", + "schema": "public", + "values": [ + "presidente", + "governador", + "deputado_federal", + "deputado_estadual" + ] + }, + "public.enum_election_candidate_vote_turn": { + "name": "enum_election_candidate_vote_turn", + "schema": "public", + "values": [ + "1", + "2" + ] + }, + "public.enum_election_candidate_vote_vote_type": { + "name": "enum_election_candidate_vote_vote_type", + "schema": "public", + "values": [ + "nominal", + "legenda" + ] + }, + "public.enum_election_candidate_office": { + "name": "enum_election_candidate_office", + "schema": "public", + "values": [ + "presidente", + "governador", + "deputado_federal", + "deputado_estadual" + ] + }, + "public.enum_election_candidate_turn": { + "name": "enum_election_candidate_turn", + "schema": "public", + "values": [ + "1", + "2" + ] + }, + "public.enum_election_candidate_elected_by": { + "name": "enum_election_candidate_elected_by", + "schema": "public", + "values": [ + "QP", + "média", + "2º turno" + ] + }, + "public.enum_election_candidate_running_again2026": { + "name": "enum_election_candidate_running_again2026", + "schema": "public", + "values": [ + "sim", + "nao", + "desconhecido" + ] + }, + "public.enum_petition_status": { + "name": "enum_petition_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "public.enum__petition_v_version_status": { + "name": "enum__petition_v_version_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "public.enum_contact_gender": { + "name": "enum_contact_gender", + "schema": "public", + "values": [ + "feminino", + "masculino", + "outro", + "nao_informado" + ] + }, + "public.enum_contact_state": { + "name": "enum_contact_state", + "schema": "public", + "values": [ + "AC", + "AL", + "AM", + "AP", + "BA", + "CE", + "DF", + "ES", + "GO", + "MA", + "MG", + "MS", + "MT", + "PA", + "PB", + "PE", + "PI", + "PR", + "RJ", + "RN", + "RO", + "RR", + "RS", + "SC", + "SE", + "SP", + "TO" + ] + }, + "public.enum_post_type": { + "name": "enum_post_type", + "schema": "public", + "values": [ + "noticia", + "campanha", + "artigo", + "evento" + ] + }, + "public.enum_post_status": { + "name": "enum_post_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "public.enum__post_v_version_type": { + "name": "enum__post_v_version_type", + "schema": "public", + "values": [ + "noticia", + "campanha", + "artigo", + "evento" + ] + }, + "public.enum__post_v_version_status": { + "name": "enum__post_v_version_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "public.enum_payload_jobs_log_task_slug": { + "name": "enum_payload_jobs_log_task_slug", + "schema": "public", + "values": [ + "inline", + "schedulePublish" + ] + }, + "public.enum_payload_jobs_log_state": { + "name": "enum_payload_jobs_log_state", + "schema": "public", + "values": [ + "failed", + "succeeded" + ] + }, + "public.enum_payload_jobs_task_slug": { + "name": "enum_payload_jobs_task_slug", + "schema": "public", + "values": [ + "inline", + "schedulePublish" + ] + }, + "public.enum_site_settings_social_links_platform": { + "name": "enum_site_settings_social_links_platform", + "schema": "public", + "values": [ + "instagram", + "facebook", + "youtube", + "whatsapp" + ] + }, + "public.enum_site_settings_status": { + "name": "enum_site_settings_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "public.enum__site_settings_v_version_social_links_platform": { + "name": "enum__site_settings_v_version_social_links_platform", + "schema": "public", + "values": [ + "instagram", + "facebook", + "youtube", + "whatsapp" + ] + }, + "public.enum__site_settings_v_version_status": { + "name": "enum__site_settings_v_version_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "id": "ee7e91dd-2a73-40a0-b7a9-b58a3a92b897", + "prevId": "00000000-0000-0000-0000-000000000000" +} \ No newline at end of file diff --git a/src/migrations/20260718_195854_add_election_results.ts b/src/migrations/20260718_195854_add_election_results.ts new file mode 100644 index 00000000..24ae7d5b --- /dev/null +++ b/src/migrations/20260718_195854_add_election_results.ts @@ -0,0 +1,153 @@ +import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres' + +export async function up({ db, payload, req }: MigrateUpArgs): Promise { + await db.execute(sql` + CREATE TYPE "public"."enum_election_tally_office" AS ENUM('presidente', 'governador', 'deputado_federal', 'deputado_estadual'); + CREATE TYPE "public"."enum_election_tally_turn" AS ENUM('1', '2'); + CREATE TYPE "public"."enum_election_candidate_vote_office" AS ENUM('presidente', 'governador', 'deputado_federal', 'deputado_estadual'); + CREATE TYPE "public"."enum_election_candidate_vote_turn" AS ENUM('1', '2'); + CREATE TYPE "public"."enum_election_candidate_vote_vote_type" AS ENUM('nominal', 'legenda'); + CREATE TYPE "public"."enum_election_candidate_office" AS ENUM('presidente', 'governador', 'deputado_federal', 'deputado_estadual'); + CREATE TYPE "public"."enum_election_candidate_turn" AS ENUM('1', '2'); + CREATE TYPE "public"."enum_election_candidate_elected_by" AS ENUM('QP', 'média', '2º turno'); + CREATE TYPE "public"."enum_election_candidate_running_again2026" AS ENUM('sim', 'nao', 'desconhecido'); + CREATE TABLE "election_tally" ( + "id" serial PRIMARY KEY NOT NULL, + "year" numeric NOT NULL, + "office" "enum_election_tally_office" NOT NULL, + "turn" "enum_election_tally_turn" NOT NULL, + "state" varchar DEFAULT 'BA' NOT NULL, + "city_code" varchar NOT NULL, + "city_name" varchar NOT NULL, + "zone_number" numeric NOT NULL, + "aptos" numeric NOT NULL, + "comparecimento" numeric NOT NULL, + "abstencoes" numeric NOT NULL, + "votos_validos" numeric NOT NULL, + "votos_nominais_validos" numeric NOT NULL, + "votos_legenda" numeric NOT NULL, + "votos_branco" numeric NOT NULL, + "votos_nulo" numeric NOT NULL, + "votos_anulados" numeric DEFAULT 0 NOT NULL, + "winner_candidate_number" numeric, + "winner_candidate_name" varchar, + "winner_votes" numeric, + "winner_coalition" varchar, + "winner_party" varchar, + "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL + ); + + CREATE TABLE "election_candidate_vote" ( + "id" serial PRIMARY KEY NOT NULL, + "year" numeric NOT NULL, + "office" "enum_election_candidate_vote_office" NOT NULL, + "turn" "enum_election_candidate_vote_turn" NOT NULL, + "state" varchar DEFAULT 'BA' NOT NULL, + "city_code" varchar NOT NULL, + "city_name" varchar NOT NULL, + "zone_number" numeric NOT NULL, + "candidate_number" numeric NOT NULL, + "candidate_name" varchar NOT NULL, + "coalition" varchar, + "party" varchar, + "vote_type" "enum_election_candidate_vote_vote_type" DEFAULT 'nominal' NOT NULL, + "votes" numeric NOT NULL, + "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL + ); + + CREATE TABLE "election_candidate" ( + "id" serial PRIMARY KEY NOT NULL, + "year" numeric NOT NULL, + "office" "enum_election_candidate_office" NOT NULL, + "turn" "enum_election_candidate_turn" NOT NULL, + "state" varchar DEFAULT 'BA' NOT NULL, + "candidate_number" numeric NOT NULL, + "urna_name" varchar NOT NULL, + "complete_name" varchar, + "party" varchar, + "coalition" varchar, + "candidate_status" varchar, + "elected" boolean DEFAULT false, + "elected_by" "enum_election_candidate_elected_by", + "total_votes_state" numeric, + "identity_key" varchar, + "running_again2026" "enum_election_candidate_running_again2026" DEFAULT 'desconhecido' NOT NULL, + "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL + ); + + ALTER TABLE "payload_locked_documents_rels" ADD COLUMN "election_tally_id" integer; + ALTER TABLE "payload_locked_documents_rels" ADD COLUMN "election_candidate_vote_id" integer; + ALTER TABLE "payload_locked_documents_rels" ADD COLUMN "election_candidate_id" integer; + CREATE INDEX "election_tally_year_idx" ON "election_tally" USING btree ("year"); + CREATE INDEX "election_tally_office_idx" ON "election_tally" USING btree ("office"); + CREATE INDEX "election_tally_turn_idx" ON "election_tally" USING btree ("turn"); + CREATE INDEX "election_tally_state_idx" ON "election_tally" USING btree ("state"); + CREATE INDEX "election_tally_city_code_idx" ON "election_tally" USING btree ("city_code"); + CREATE INDEX "election_tally_city_name_idx" ON "election_tally" USING btree ("city_name"); + CREATE INDEX "election_tally_zone_number_idx" ON "election_tally" USING btree ("zone_number"); + CREATE INDEX "election_tally_updated_at_idx" ON "election_tally" USING btree ("updated_at"); + CREATE INDEX "election_tally_created_at_idx" ON "election_tally" USING btree ("created_at"); + CREATE UNIQUE INDEX "year_office_turn_state_cityCode_zoneNumber_idx" ON "election_tally" USING btree ("year","office","turn","state","city_code","zone_number"); + CREATE INDEX "election_candidate_vote_year_idx" ON "election_candidate_vote" USING btree ("year"); + CREATE INDEX "election_candidate_vote_office_idx" ON "election_candidate_vote" USING btree ("office"); + CREATE INDEX "election_candidate_vote_turn_idx" ON "election_candidate_vote" USING btree ("turn"); + CREATE INDEX "election_candidate_vote_state_idx" ON "election_candidate_vote" USING btree ("state"); + CREATE INDEX "election_candidate_vote_city_code_idx" ON "election_candidate_vote" USING btree ("city_code"); + CREATE INDEX "election_candidate_vote_city_name_idx" ON "election_candidate_vote" USING btree ("city_name"); + CREATE INDEX "election_candidate_vote_zone_number_idx" ON "election_candidate_vote" USING btree ("zone_number"); + CREATE INDEX "election_candidate_vote_candidate_number_idx" ON "election_candidate_vote" USING btree ("candidate_number"); + CREATE INDEX "election_candidate_vote_vote_type_idx" ON "election_candidate_vote" USING btree ("vote_type"); + CREATE INDEX "election_candidate_vote_updated_at_idx" ON "election_candidate_vote" USING btree ("updated_at"); + CREATE INDEX "election_candidate_vote_created_at_idx" ON "election_candidate_vote" USING btree ("created_at"); + CREATE UNIQUE INDEX "compound_index_idx" ON "election_candidate_vote" USING btree ("year","office","turn","state","city_code","zone_number","candidate_number","vote_type"); + CREATE INDEX "election_candidate_year_idx" ON "election_candidate" USING btree ("year"); + CREATE INDEX "election_candidate_office_idx" ON "election_candidate" USING btree ("office"); + CREATE INDEX "election_candidate_turn_idx" ON "election_candidate" USING btree ("turn"); + CREATE INDEX "election_candidate_state_idx" ON "election_candidate" USING btree ("state"); + CREATE INDEX "election_candidate_candidate_number_idx" ON "election_candidate" USING btree ("candidate_number"); + CREATE INDEX "election_candidate_elected_idx" ON "election_candidate" USING btree ("elected"); + CREATE INDEX "election_candidate_identity_key_idx" ON "election_candidate" USING btree ("identity_key"); + CREATE INDEX "election_candidate_updated_at_idx" ON "election_candidate" USING btree ("updated_at"); + CREATE INDEX "election_candidate_created_at_idx" ON "election_candidate" USING btree ("created_at"); + CREATE UNIQUE INDEX "year_office_turn_state_candidateNumber_idx" ON "election_candidate" USING btree ("year","office","turn","state","candidate_number"); + ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_election_tally_fk" FOREIGN KEY ("election_tally_id") REFERENCES "public"."election_tally"("id") ON DELETE cascade ON UPDATE no action; + ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_election_candidate_vote_fk" FOREIGN KEY ("election_candidate_vote_id") REFERENCES "public"."election_candidate_vote"("id") ON DELETE cascade ON UPDATE no action; + ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_election_candidate_fk" FOREIGN KEY ("election_candidate_id") REFERENCES "public"."election_candidate"("id") ON DELETE cascade ON UPDATE no action; + CREATE INDEX "payload_locked_documents_rels_election_tally_id_idx" ON "payload_locked_documents_rels" USING btree ("election_tally_id"); + CREATE INDEX "payload_locked_documents_rels_election_candidate_vote_id_idx" ON "payload_locked_documents_rels" USING btree ("election_candidate_vote_id"); + CREATE INDEX "payload_locked_documents_rels_election_candidate_id_idx" ON "payload_locked_documents_rels" USING btree ("election_candidate_id");`) +} + +export async function down({ db, payload, req }: MigrateDownArgs): Promise { + await db.execute(sql` + ALTER TABLE "election_tally" DISABLE ROW LEVEL SECURITY; + ALTER TABLE "election_candidate_vote" DISABLE ROW LEVEL SECURITY; + ALTER TABLE "election_candidate" DISABLE ROW LEVEL SECURITY; + DROP TABLE "election_tally" CASCADE; + DROP TABLE "election_candidate_vote" CASCADE; + DROP TABLE "election_candidate" CASCADE; + ALTER TABLE "payload_locked_documents_rels" DROP CONSTRAINT "payload_locked_documents_rels_election_tally_fk"; + + ALTER TABLE "payload_locked_documents_rels" DROP CONSTRAINT "payload_locked_documents_rels_election_candidate_vote_fk"; + + ALTER TABLE "payload_locked_documents_rels" DROP CONSTRAINT "payload_locked_documents_rels_election_candidate_fk"; + + DROP INDEX "payload_locked_documents_rels_election_tally_id_idx"; + DROP INDEX "payload_locked_documents_rels_election_candidate_vote_id_idx"; + DROP INDEX "payload_locked_documents_rels_election_candidate_id_idx"; + ALTER TABLE "payload_locked_documents_rels" DROP COLUMN "election_tally_id"; + ALTER TABLE "payload_locked_documents_rels" DROP COLUMN "election_candidate_vote_id"; + ALTER TABLE "payload_locked_documents_rels" DROP COLUMN "election_candidate_id"; + DROP TYPE "public"."enum_election_tally_office"; + DROP TYPE "public"."enum_election_tally_turn"; + DROP TYPE "public"."enum_election_candidate_vote_office"; + DROP TYPE "public"."enum_election_candidate_vote_turn"; + DROP TYPE "public"."enum_election_candidate_vote_vote_type"; + DROP TYPE "public"."enum_election_candidate_office"; + DROP TYPE "public"."enum_election_candidate_turn"; + DROP TYPE "public"."enum_election_candidate_elected_by"; + DROP TYPE "public"."enum_election_candidate_running_again2026";`) +} diff --git a/src/migrations/index.ts b/src/migrations/index.ts index 228fd65d..71d903d8 100644 --- a/src/migrations/index.ts +++ b/src/migrations/index.ts @@ -4,6 +4,7 @@ import * as migration_20260715_181058_add_post_and_tag from './20260715_181058_a import * as migration_20260715_215834_rename_tag_visible_to_hidden from './20260715_215834_rename_tag_visible_to_hidden'; import * as migration_20260716_010420_add_campaign_user from './20260716_010420_add_campaign_user'; import * as migration_20260718_010733_consolidate_campaign_schema from './20260718_010733_consolidate_campaign_schema'; +import * as migration_20260718_195854_add_election_results from './20260718_195854_add_election_results'; export const migrations = [ { @@ -36,4 +37,10 @@ export const migrations = [ down: migration_20260718_010733_consolidate_campaign_schema.down, name: '20260718_010733_consolidate_campaign_schema', }, -]; + { + up: migration_20260718_195854_add_election_results.up, + down: migration_20260718_195854_add_election_results.down, + name: '20260718_195854_add_election_results', + }, +] + diff --git a/src/payload-types.ts b/src/payload-types.ts index 1ff2c59d..fdf8f0ea 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -74,6 +74,9 @@ export interface Config { electoralNucleus: ElectoralNucleus; leadership: Leadership; nucleusUpdate: NucleusUpdate; + electionTally: ElectionTally; + electionCandidateVote: ElectionCandidateVote; + electionCandidate: ElectionCandidate; media: Media; petition: Petition; contact: Contact; @@ -96,6 +99,9 @@ export interface Config { electoralNucleus: ElectoralNucleusSelect | ElectoralNucleusSelect; leadership: LeadershipSelect | LeadershipSelect; nucleusUpdate: NucleusUpdateSelect | NucleusUpdateSelect; + electionTally: ElectionTallySelect | ElectionTallySelect; + electionCandidateVote: ElectionCandidateVoteSelect | ElectionCandidateVoteSelect; + electionCandidate: ElectionCandidateSelect | ElectionCandidateSelect; media: MediaSelect | MediaSelect; petition: PetitionSelect | PetitionSelect; contact: ContactSelect | ContactSelect; @@ -461,6 +467,91 @@ export interface NucleusUpdate { updatedAt: string; createdAt: string; } +/** + * Totais oficiais TSE por município e zona (dado público). + * + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "electionTally". + */ +export interface ElectionTally { + id: number; + year: number; + office: 'presidente' | 'governador' | 'deputado_federal' | 'deputado_estadual'; + turn: '1' | '2'; + state: string; + cityCode: string; + cityName: string; + zoneNumber: number; + aptos: number; + comparecimento: number; + abstencoes: number; + votosValidos: number; + votosNominaisValidos: number; + votosLegenda: number; + votosBranco: number; + votosNulo: number; + votosAnulados: number; + winnerCandidateNumber?: number | null; + winnerCandidateName?: string | null; + winnerVotes?: number | null; + winnerCoalition?: string | null; + winnerParty?: string | null; + updatedAt: string; + createdAt: string; +} +/** + * Votos nominais oficiais TSE por município e zona (dado público). + * + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "electionCandidateVote". + */ +export interface ElectionCandidateVote { + id: number; + year: number; + office: 'presidente' | 'governador' | 'deputado_federal' | 'deputado_estadual'; + turn: '1' | '2'; + state: string; + cityCode: string; + cityName: string; + zoneNumber: number; + candidateNumber: number; + candidateName: string; + coalition?: string | null; + party?: string | null; + voteType: 'nominal' | 'legenda'; + votes: number; + updatedAt: string; + createdAt: string; +} +/** + * Registro de candidatura TSE (dado público). + * + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "electionCandidate". + */ +export interface ElectionCandidate { + id: number; + year: number; + office: 'presidente' | 'governador' | 'deputado_federal' | 'deputado_estadual'; + turn: '1' | '2'; + state: string; + candidateNumber: number; + urnaName: string; + completeName?: string | null; + party?: string | null; + coalition?: string | null; + candidateStatus?: string | null; + elected?: boolean | null; + electedBy?: ('QP' | 'média' | '2º turno') | null; + totalVotesState?: number | null; + /** + * sha256(nome urna + naturalidade + partido); só dados públicos. + */ + identityKey?: string | null; + runningAgain2026: 'sim' | 'nao' | 'desconhecido'; + updatedAt: string; + createdAt: string; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "media". @@ -735,6 +826,18 @@ export interface PayloadLockedDocument { relationTo: 'nucleusUpdate'; value: number | NucleusUpdate; } | null) + | ({ + relationTo: 'electionTally'; + value: number | ElectionTally; + } | null) + | ({ + relationTo: 'electionCandidateVote'; + value: number | ElectionCandidateVote; + } | null) + | ({ + relationTo: 'electionCandidate'; + value: number | ElectionCandidate; + } | null) | ({ relationTo: 'media'; value: number | Media; @@ -988,6 +1091,79 @@ export interface NucleusUpdateSelect { updatedAt?: T; createdAt?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "electionTally_select". + */ +export interface ElectionTallySelect { + year?: T; + office?: T; + turn?: T; + state?: T; + cityCode?: T; + cityName?: T; + zoneNumber?: T; + aptos?: T; + comparecimento?: T; + abstencoes?: T; + votosValidos?: T; + votosNominaisValidos?: T; + votosLegenda?: T; + votosBranco?: T; + votosNulo?: T; + votosAnulados?: T; + winnerCandidateNumber?: T; + winnerCandidateName?: T; + winnerVotes?: T; + winnerCoalition?: T; + winnerParty?: T; + updatedAt?: T; + createdAt?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "electionCandidateVote_select". + */ +export interface ElectionCandidateVoteSelect { + year?: T; + office?: T; + turn?: T; + state?: T; + cityCode?: T; + cityName?: T; + zoneNumber?: T; + candidateNumber?: T; + candidateName?: T; + coalition?: T; + party?: T; + voteType?: T; + votes?: T; + updatedAt?: T; + createdAt?: T; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "electionCandidate_select". + */ +export interface ElectionCandidateSelect { + year?: T; + office?: T; + turn?: T; + state?: T; + candidateNumber?: T; + urnaName?: T; + completeName?: T; + party?: T; + coalition?: T; + candidateStatus?: T; + elected?: T; + electedBy?: T; + totalVotesState?: T; + identityKey?: T; + runningAgain2026?: T; + updatedAt?: T; + createdAt?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "media_select". diff --git a/src/payload.config.ts b/src/payload.config.ts index d7b7cd97..8130ee23 100644 --- a/src/payload.config.ts +++ b/src/payload.config.ts @@ -11,6 +11,9 @@ import { CampaignInvite } from './collections/CampaignInvite' import { CampaignUser } from './collections/CampaignUser' import { Consent } from './collections/Consent' import { Contact } from './collections/Contact' +import { ElectionCandidate } from './collections/ElectionCandidate' +import { ElectionCandidateVote } from './collections/ElectionCandidateVote' +import { ElectionTally } from './collections/ElectionTally' import { ElectoralNucleus } from './collections/ElectoralNucleus' import { Leadership } from './collections/Leadership' import { Media } from './collections/Media' @@ -46,6 +49,9 @@ export default buildConfig({ ElectoralNucleus, Leadership, NucleusUpdate, + ElectionTally, + ElectionCandidateVote, + ElectionCandidate, Media, Petition, Contact, diff --git a/src/utilities/campaignAccess.ts b/src/utilities/campaignAccess.ts index 884b623f..6686c492 100644 --- a/src/utilities/campaignAccess.ts +++ b/src/utilities/campaignAccess.ts @@ -296,6 +296,13 @@ export const canManageNucleusLifecycle: FieldAccess = async ({ req }) => { export const canSetDerivedNucleusField: FieldAccess = ({ req }) => isPayloadAdmin(req.user) +/** Public TSE election data: any authenticated campaign or admin user may read. */ +export const canReadElectionData: Access = ({ req }) => + isPayloadAdmin(req.user) || isCampaignUser(req.user) + +/** Election reference data is mutated only by Payload admins (or CLI with overrideAccess). */ +export const canMutateElectionData: Access = ({ req }) => isPayloadAdmin(req.user) + type DynamicFind = (args: { collection: string depth: number diff --git a/src/utilities/electionResultsImport.ts b/src/utilities/electionResultsImport.ts new file mode 100644 index 00000000..32ea34e6 --- /dev/null +++ b/src/utilities/electionResultsImport.ts @@ -0,0 +1,206 @@ +import { and, eq } 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' + +const VOTE_TABLE = 'election_candidate_vote' +const TALLY_TABLE = 'election_tally' +const CANDIDATE_TABLE = 'election_candidate' + +/** ~15 columns × 500 ≈ 7500 bind params — well under PG 65535. */ +const INSERT_CHUNK_SIZE = 500 + +export 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 + } + insert: (table: unknown) => { + values: (rows: Record[]) => Promise + } +} + +type PayloadDb = { + drizzle: { + transaction: (fn: (tx: DrizzleTx) => Promise) => Promise + } + tables: Record> +} + +const chunk = (rows: T[], size: number): T[][] => { + const out: T[][] = [] + for (let i = 0; i < rows.length; i += size) out.push(rows.slice(i, i + size)) + return out +} + +const getDb = (payload: Payload): PayloadDb => payload.db as unknown as PayloadDb + +const requireTable = (db: PayloadDb, name: string) => { + const table = db.tables[name] + if (!table) throw new Error(`Tabela drizzle ausente: ${name}. Rode as migrations.`) + return table +} + +const scopeWhere = (table: Record, scope: ElectionImportScope) => + and( + eq(table.year as never, scope.year), + eq(table.office as never, scope.office), + eq(table.turn as never, scope.turn), + ) + +const deleteScope = async ( + tx: DrizzleTx, + table: Record, + scope: ElectionImportScope, +): Promise => { + // 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 = >(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. + */ +export const importElectionScope = async ( + payload: Payload, + bundle: ElectionImportBundle, +): Promise => { + const dbAdapter = getDb(payload) + const voteTable = requireTable(dbAdapter, VOTE_TABLE) + const tallyTable = requireTable(dbAdapter, TALLY_TABLE) + const candidateTable = requireTable(dbAdapter, CANDIDATE_TABLE) + + 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 => { + 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) ?? [], + } + }) +} diff --git a/tests/fixtures/tse/consulta_cand_ba_fixture.csv b/tests/fixtures/tse/consulta_cand_ba_fixture.csv new file mode 100644 index 00000000..6305eb28 --- /dev/null +++ b/tests/fixtures/tse/consulta_cand_ba_fixture.csv @@ -0,0 +1,8 @@ +ANO_ELEICAO;NR_TURNO;SG_UF;CD_CARGO;NR_CANDIDATO;NM_URNA_CANDIDATO;NM_CANDIDATO;SG_PARTIDO;NM_COLIGACAO;DS_SITUACAO_CANDIDATURA;DS_SIT_TOT_TURNO;NM_MUNICIPIO_NASCIMENTO;SG_UF_NASCIMENTO +2022;1;BA;6;1313;JORGE SOLLA;JORGE JOSE SANTOS PEREIRA SOLLA;PT;FEDERAÇÃO BRASIL DA ESPERANÇA - FE BRASIL;DEFERIDO;ELEITO;SALVADOR;BA +2022;1;BA;6;2222;RIVAL FEDERAL;RIVAL DA SILVA;PL;PARTIDO LIBERAL;DEFERIDO;#NULO#;FEIRA DE SANTANA;BA +2022;1;BA;7;13131;ESTADUAL PT;CANDIDATO ESTADUAL;PT;FEDERAÇÃO BRASIL DA ESPERANÇA - FE BRASIL;DEFERIDO;#NULO#;SALVADOR;BA +2022;1;BA;3;13;JERÔNIMO;JERONIMO RODRIGUES SOUZA;PT;FEDERAÇÃO BRASIL DA ESPERANÇA - FE BRASIL;DEFERIDO;2º TURNO;ACAJUTIBA;BA +2022;2;BA;3;13;JERÔNIMO;JERONIMO RODRIGUES SOUZA;PT;FEDERAÇÃO BRASIL DA ESPERANÇA - FE BRASIL;DEFERIDO;ELEITO;ACAJUTIBA;BA +2022;1;BA;3;45;RIVAL GOV;RIVAL GOVERNADOR;PSDB;COLIGAÇÃO OPOSIÇÃO;DEFERIDO;2º TURNO;SALVADOR;BA +2022;2;BA;3;45;RIVAL GOV;RIVAL GOVERNADOR;PSDB;COLIGAÇÃO OPOSIÇÃO;DEFERIDO;NÃO ELEITO;SALVADOR;BA diff --git a/tests/fixtures/tse/consulta_cand_br_fixture.csv b/tests/fixtures/tse/consulta_cand_br_fixture.csv new file mode 100644 index 00000000..3f29efc1 --- /dev/null +++ b/tests/fixtures/tse/consulta_cand_br_fixture.csv @@ -0,0 +1,5 @@ +ANO_ELEICAO;NR_TURNO;SG_UF;CD_CARGO;NR_CANDIDATO;NM_URNA_CANDIDATO;NM_CANDIDATO;SG_PARTIDO;NM_COLIGACAO;DS_SITUACAO_CANDIDATURA;DS_SIT_TOT_TURNO;NM_MUNICIPIO_NASCIMENTO;SG_UF_NASCIMENTO +2022;1;BR;1;13;LULA;LUIZ INACIO LULA DA SILVA;PT;FEDERAÇÃO BRASIL DA ESPERANÇA - FE BRASIL;DEFERIDO;2º TURNO;GARANHUNS;PE +2022;2;BR;1;13;LULA;LUIZ INACIO LULA DA SILVA;PT;FEDERAÇÃO BRASIL DA ESPERANÇA - FE BRASIL;DEFERIDO;ELEITO;GARANHUNS;PE +2022;1;BR;1;22;BOLSONARO;JAIR MESSIAS BOLSONARO;PL;PARTIDO LIBERAL;DEFERIDO;2º TURNO;CAMPINAS;SP +2022;2;BR;1;22;BOLSONARO;JAIR MESSIAS BOLSONARO;PL;PARTIDO LIBERAL;DEFERIDO;NÃO ELEITO;CAMPINAS;SP diff --git a/tests/fixtures/tse/detalhe_votacao_munzona_fixture.csv b/tests/fixtures/tse/detalhe_votacao_munzona_fixture.csv new file mode 100644 index 00000000..55ac6e70 --- /dev/null +++ b/tests/fixtures/tse/detalhe_votacao_munzona_fixture.csv @@ -0,0 +1,9 @@ +ANO_ELEICAO;NR_TURNO;SG_UF;CD_MUNICIPIO;NM_MUNICIPIO;NR_ZONA;CD_CARGO;QT_APTOS;QT_COMPARECIMENTO;QT_ABSTENCOES;QT_VOTOS_VALIDOS;QT_VOTOS_NOMINAIS_VALIDOS;QT_VOTOS_LEGENDA;QT_VOTOS_BRANCOS;QT_VOTOS_NULOS;QT_VOTOS_ANULADOS +2022;1;BA;38490;SALVADOR;1;6;10000;8000;2000;7500;7300;200;300;200;0 +2022;1;BA;38490;SALVADOR;2;6;9000;7200;1800;6800;6600;200;250;150;0 +2022;1;BA;35150;FEIRA DE SANTANA;10;6;5000;4000;1000;3800;3700;100;120;80;0 +2022;1;BA;38490;SALVADOR;1;7;10000;8000;2000;7400;7200;200;300;300;0 +2022;1;BA;38490;SALVADOR;1;3;10000;8000;2000;7600;7400;200;200;200;0 +2022;2;BA;38490;SALVADOR;1;3;10000;8200;1800;7800;7600;200;200;200;0 +2022;1;BA;38490;SALVADOR;1;1;10000;8000;2000;7700;7500;200;150;150;0 +2022;2;BA;38490;SALVADOR;1;1;10000;8300;1700;7900;7700;200;200;200;0 diff --git a/tests/fixtures/tse/votacao_candidato_munzona_fixture.csv b/tests/fixtures/tse/votacao_candidato_munzona_fixture.csv new file mode 100644 index 00000000..efb58929 --- /dev/null +++ b/tests/fixtures/tse/votacao_candidato_munzona_fixture.csv @@ -0,0 +1,17 @@ +ANO_ELEICAO;NR_TURNO;SG_UF;CD_MUNICIPIO;NM_MUNICIPIO;NR_ZONA;CD_CARGO;NR_CANDIDATO;NM_URNA_CANDIDATO;SG_PARTIDO;NM_COLIGACAO;QT_VOTOS_NOMINAIS;DS_SIT_TOT_TURNO +2022;1;BA;38490;SALVADOR;1;6;1313;JORGE SOLLA;PT;FEDERAÇÃO BRASIL DA ESPERANÇA - FE BRASIL;1200;ELEITO +2022;1;BA;38490;SALVADOR;1;6;2222;RIVAL FEDERAL;PL;PARTIDO LIBERAL;800;#NULO# +2022;1;BA;38490;SALVADOR;2;6;1313;JORGE SOLLA;PT;FEDERAÇÃO BRASIL DA ESPERANÇA - FE BRASIL;900;ELEITO +2022;1;BA;38490;SALVADOR;2;6;2222;RIVAL FEDERAL;PL;PARTIDO LIBERAL;1100;#NULO# +2022;1;BA;35150;FEIRA DE SANTANA;10;6;1313;JORGE SOLLA;PT;FEDERAÇÃO BRASIL DA ESPERANÇA - FE BRASIL;500;ELEITO +2022;1;BA;35150;FEIRA DE SANTANA;10;6;2222;RIVAL FEDERAL;PL;PARTIDO LIBERAL;300;#NULO# +2022;1;BA;38490;SALVADOR;1;7;13131;ESTADUAL PT;PT;FEDERAÇÃO BRASIL DA ESPERANÇA - FE BRASIL;400;#NULO# +2022;1;BA;38490;SALVADOR;1;3;13;JERÔNIMO;PT;FEDERAÇÃO BRASIL DA ESPERANÇA - FE BRASIL;5000;2º TURNO +2022;1;BA;38490;SALVADOR;1;3;45;RIVAL GOV;PSDB;COLIGAÇÃO OPOSIÇÃO;4200;2º TURNO +2022;2;BA;38490;SALVADOR;1;3;13;JERÔNIMO;PT;FEDERAÇÃO BRASIL DA ESPERANÇA - FE BRASIL;6000;ELEITO +2022;2;BA;38490;SALVADOR;1;3;45;RIVAL GOV;PSDB;COLIGAÇÃO OPOSIÇÃO;4500;NÃO ELEITO +2022;1;BA;38490;SALVADOR;1;1;13;LULA;PT;FEDERAÇÃO BRASIL DA ESPERANÇA - FE BRASIL;7000;2º TURNO +2022;1;BA;38490;SALVADOR;1;1;22;BOLSONARO;PL;PARTIDO LIBERAL;3000;2º TURNO +2022;2;BA;38490;SALVADOR;1;1;13;LULA;PT;FEDERAÇÃO BRASIL DA ESPERANÇA - FE BRASIL;8000;ELEITO +2022;2;BA;38490;SALVADOR;1;1;22;BOLSONARO;PL;PARTIDO LIBERAL;3500;NÃO ELEITO +2022;1;BA;38490;SALVADOR;1;6;9999;ZERO VOTOS;PSD;#NULO#;0;#NULO# diff --git a/tests/helpers/tseFixtures.ts b/tests/helpers/tseFixtures.ts new file mode 100644 index 00000000..1613aa37 --- /dev/null +++ b/tests/helpers/tseFixtures.ts @@ -0,0 +1,35 @@ +import { readFileSync } from 'node:fs' +import { join } from 'node:path' + +import { parseTseCsvString } from '@/lib/electionResultsCsv' +import { buildElectionResultsFromCsvRows } from '@/lib/electionResultsBuild' +import type { BuiltElectionResults } from '@/lib/electionResultsBuild' + +const fixtureDir = join(process.cwd(), 'tests/fixtures/tse') + +const readFixture = (name: string): string => readFileSync(join(fixtureDir, name), 'utf8') + +export const loadTseFixtureResults = (): BuiltElectionResults => { + const voteRows = parseTseCsvString(readFixture('votacao_candidato_munzona_fixture.csv')) + const detalheRows = parseTseCsvString(readFixture('detalhe_votacao_munzona_fixture.csv')) + const candBaRows = parseTseCsvString(readFixture('consulta_cand_ba_fixture.csv')) + const candBrRows = parseTseCsvString(readFixture('consulta_cand_br_fixture.csv')) + + return buildElectionResultsFromCsvRows({ + voteRows, + detalheRows, + candBaRows, + candBrRows, + }) +} + +/** Known totals from the fixture (after skipping the zero-vote row). */ +export const TSE_FIXTURE_EXPECTED = { + sollaVotesTotal: 1200 + 900 + 500, + lulaTurn2SalvadorZ1: 8000, + jeronimoTurn2SalvadorZ1: 6000, + federalWinnerSalvadorZ2: { candidateNumber: 2222, votes: 1100 }, + federalWinnerSalvadorZ1: { candidateNumber: 1313, votes: 1200 }, + voteRowCount: 15, // 16 CSV rows minus the zero-vote row + tallyRowCount: 8, +} as const diff --git a/tests/int/electionResultsImport.int.spec.ts b/tests/int/electionResultsImport.int.spec.ts new file mode 100644 index 00000000..f42d19f0 --- /dev/null +++ b/tests/int/electionResultsImport.int.spec.ts @@ -0,0 +1,186 @@ +// @vitest-environment node + +import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import { getPayload, type Payload } from 'payload' + +import config from '@/payload.config' +import { SOLLA_CANDIDATE_NUMBER } from '@/lib/electionResults' +import { + buildImportBundles, + importElectionBundles, +} from '@/utilities/electionResultsImport' +import { loadTseFixtureResults, TSE_FIXTURE_EXPECTED } from '../helpers/tseFixtures' + +let payload: Payload + +const deleteAllElectionData = async () => { + for (const collection of [ + 'electionCandidateVote', + 'electionTally', + 'electionCandidate', + ] as const) { + const existing = await payload.find({ + collection, + depth: 0, + limit: 0, + pagination: false, + overrideAccess: true, + }) + for (const doc of existing.docs) { + await payload.delete({ + collection, + id: doc.id, + overrideAccess: true, + }) + } + } +} + +describe('election results import', () => { + beforeAll(async () => { + payload = await getPayload({ config: await config }) + await deleteAllElectionData() + }) + + afterAll(async () => { + await deleteAllElectionData() + }) + + it('imports fixture totals, winners, elected flags, and is idempotent', async () => { + const built = loadTseFixtureResults() + const bundles = buildImportBundles(built) + + const first = await importElectionBundles(payload, bundles) + const second = await importElectionBundles(payload, bundles) + + const firstVotes = first.reduce((sum, c) => sum + c.votesInserted, 0) + const secondVotes = second.reduce((sum, c) => sum + c.votesInserted, 0) + expect(firstVotes).toBe(TSE_FIXTURE_EXPECTED.voteRowCount) + expect(secondVotes).toBe(firstVotes) + expect(second.reduce((sum, c) => sum + c.votesDeleted, 0)).toBe(firstVotes) + + const votes = await payload.find({ + collection: 'electionCandidateVote', + depth: 0, + limit: 0, + pagination: false, + overrideAccess: true, + }) + expect(votes.totalDocs).toBe(TSE_FIXTURE_EXPECTED.voteRowCount) + + const sollaVotes = votes.docs + .filter( + (doc) => + doc.office === 'deputado_federal' && + Number(doc.candidateNumber) === SOLLA_CANDIDATE_NUMBER, + ) + .reduce((sum, doc) => sum + Number(doc.votes), 0) + expect(sollaVotes).toBe(TSE_FIXTURE_EXPECTED.sollaVotesTotal) + + const tallies = await payload.find({ + collection: 'electionTally', + where: { + and: [ + { office: { equals: 'deputado_federal' } }, + { cityCode: { equals: '38490' } }, + { zoneNumber: { equals: 1 } }, + { turn: { equals: '1' } }, + ], + }, + depth: 0, + limit: 1, + overrideAccess: true, + }) + expect(tallies.docs[0]?.winnerCandidateNumber).toBe( + TSE_FIXTURE_EXPECTED.federalWinnerSalvadorZ1.candidateNumber, + ) + expect(Number(tallies.docs[0]?.winnerVotes)).toBe( + TSE_FIXTURE_EXPECTED.federalWinnerSalvadorZ1.votes, + ) + + const candidates = await payload.find({ + collection: 'electionCandidate', + depth: 0, + limit: 0, + pagination: false, + overrideAccess: true, + }) + const solla = candidates.docs.find( + (doc) => doc.office === 'deputado_federal' && Number(doc.candidateNumber) === 1313, + ) + const lula = candidates.docs.find( + (doc) => + doc.office === 'presidente' && + Number(doc.candidateNumber) === 13 && + doc.turn === '2', + ) + const jeronimo = candidates.docs.find( + (doc) => + doc.office === 'governador' && + Number(doc.candidateNumber) === 13 && + doc.turn === '2', + ) + expect(solla?.elected).toBe(true) + expect(lula?.elected).toBe(true) + expect(jeronimo?.elected).toBe(true) + expect(lula?.identityKey).toBeTruthy() + }) + + it('allows campaignUser read and denies campaignUser mutation', async () => { + // Avoid campaignFixtures here: teqo_test may already include territory migrations + // from other worktrees that diverge from this branch's electoralNucleus schema. + const campaignUser = await payload.create({ + collection: 'campaignUser', + data: { + name: 'Election Access Tester', + role: 'geral', + email: `election-access-${Date.now()}@example.com`, + password: 'Password123!', + }, + overrideAccess: true, + }) + + try { + const readable = await payload.find({ + collection: 'electionTally', + depth: 0, + limit: 1, + user: campaignUser, + overrideAccess: false, + }) + expect(readable.totalDocs).toBeGreaterThan(0) + + await expect( + payload.create({ + collection: 'electionTally', + data: { + year: 2022, + office: 'deputado_federal', + turn: '1', + state: 'BA', + cityCode: '99999', + cityName: 'Salvador', + zoneNumber: 99, + aptos: 1, + comparecimento: 1, + abstencoes: 0, + votosValidos: 1, + votosNominaisValidos: 1, + votosLegenda: 0, + votosBranco: 0, + votosNulo: 0, + votosAnulados: 0, + }, + user: campaignUser, + overrideAccess: false, + }), + ).rejects.toThrow() + } finally { + await payload.delete({ + collection: 'campaignUser', + id: campaignUser.id, + overrideAccess: true, + }) + } + }) +}) diff --git a/tests/unit/electionResults.unit.spec.ts b/tests/unit/electionResults.unit.spec.ts new file mode 100644 index 00000000..9803304b --- /dev/null +++ b/tests/unit/electionResults.unit.spec.ts @@ -0,0 +1,161 @@ +// @vitest-environment node + +import { describe, expect, it } from 'vitest' + +import { bahiaMunicipalities } from '@/lib/bahiaTerritories' +import { computeIdentityKey, normalizeIdentityPart } from '@/lib/electionCandidateIdentity' +import { + assertAllCanonicalMunicipalitiesResolvable, + canonicalizeMunicipalityName, + computeWinnersByScope, + JERONIMO_CANDIDATE_NUMBER, + LULA_CANDIDATE_NUMBER, + mergeTallyWithWinners, + normalizeMunicipalityKey, + parseElectedStatus, + SOLLA_CANDIDATE_NUMBER, + UnknownMunicipalityError, + winnerKey, +} from '@/lib/electionResults' +import { loadTseFixtureResults, TSE_FIXTURE_EXPECTED } from '../helpers/tseFixtures' + +describe('electionResults municipality mapping', () => { + it('resolves every canonical Bahia municipality through itself', () => { + expect(bahiaMunicipalities).toHaveLength(417) + expect(() => assertAllCanonicalMunicipalitiesResolvable()).not.toThrow() + }) + + it('maps TSE ALL-CAPS names to canonical names', () => { + expect(canonicalizeMunicipalityName('SALVADOR')).toBe('Salvador') + expect(canonicalizeMunicipalityName('FEIRA DE SANTANA')).toBe('Feira de Santana') + expect(canonicalizeMunicipalityName("DIAS D'AVILA")).toBe("Dias d'Ávila") + expect(canonicalizeMunicipalityName('XIQUE-XIQUE')).toBe('Xique-Xique') + }) + + it('maps known TSE spelling aliases to canonical names', () => { + expect(canonicalizeMunicipalityName('CAMACÃ')).toBe('Camacan') + expect(canonicalizeMunicipalityName('DIAS D ÁVILA')).toBe("Dias d'Ávila") + expect(canonicalizeMunicipalityName('MUQUÉM DO SÃO FRANCISCO')).toBe( + 'Muquém de São Francisco', + ) + expect(canonicalizeMunicipalityName('SANTA TEREZINHA')).toBe('Santa Teresinha') + }) + + it('fails closed on unknown municipality names', () => { + expect(() => canonicalizeMunicipalityName('CIDADE INEXISTENTE')).toThrow(UnknownMunicipalityError) + }) + + it('normalizes accent and apostrophe variants to the same key', () => { + expect(normalizeMunicipalityKey("Dias d'Ávila")).toBe(normalizeMunicipalityKey("DIAS D'AVILA")) + }) +}) + +describe('electionResults elected status', () => { + it('maps DS_SIT_TOT_TURNO variants', () => { + expect(parseElectedStatus('ELEITO')).toEqual({ elected: true, electedBy: 'QP' }) + expect(parseElectedStatus('ELEITO POR MÉDIA')).toEqual({ elected: true, electedBy: 'média' }) + expect(parseElectedStatus('ELEITO EM 2º TURNO')).toEqual({ + elected: true, + electedBy: '2º turno', + }) + expect(parseElectedStatus('2º TURNO')).toEqual({ elected: false, electedBy: null }) + expect(parseElectedStatus('NÃO ELEITO')).toEqual({ elected: false, electedBy: null }) + expect(parseElectedStatus('#NULO#')).toEqual({ elected: false, electedBy: null }) + }) +}) + +describe('electionCandidateIdentity', () => { + it('is stable for equivalent normalized inputs', () => { + const a = computeIdentityKey({ + urnaName: 'Jorge Solla', + birthCity: 'Salvador', + birthState: 'BA', + party: 'PT', + }) + const b = computeIdentityKey({ + urnaName: 'JORGE SOLLA', + birthCity: 'SALVADOR', + birthState: 'ba', + party: 'pt', + }) + expect(a).toBe(b) + expect(a).toHaveLength(64) + }) + + it('changes when party changes', () => { + const base = { + urnaName: 'LULA', + birthCity: 'GARANHUNS', + birthState: 'PE', + party: 'PT', + } + expect(computeIdentityKey(base)).not.toBe(computeIdentityKey({ ...base, party: 'PL' })) + }) + + it('strips accents in identity parts', () => { + expect(normalizeIdentityPart('Jerônimo')).toBe('JERONIMO') + }) +}) + +describe('electionResults fixture parse + winners', () => { + const built = loadTseFixtureResults() + + it('builds expected vote totals and skips zero-vote rows', () => { + expect(built.unknownMunicipalities).toEqual([]) + expect(built.votes).toHaveLength(TSE_FIXTURE_EXPECTED.voteRowCount) + expect(built.tallies).toHaveLength(TSE_FIXTURE_EXPECTED.tallyRowCount) + + const sollaVotes = built.votes + .filter( + (row) => + row.office === 'deputado_federal' && row.candidateNumber === SOLLA_CANDIDATE_NUMBER, + ) + .reduce((sum, row) => sum + row.votes, 0) + expect(sollaVotes).toBe(TSE_FIXTURE_EXPECTED.sollaVotesTotal) + }) + + it('marks Solla, Lula and Jerônimo as elected', () => { + const solla = built.candidates.find( + (c) => c.office === 'deputado_federal' && c.candidateNumber === SOLLA_CANDIDATE_NUMBER, + ) + const lula = built.candidates.find( + (c) => + c.office === 'presidente' && + c.candidateNumber === LULA_CANDIDATE_NUMBER && + c.turn === '2', + ) + const jeronimo = built.candidates.find( + (c) => + c.office === 'governador' && + c.candidateNumber === JERONIMO_CANDIDATE_NUMBER && + c.turn === '2', + ) + + expect(solla?.elected).toBe(true) + expect(lula?.elected).toBe(true) + expect(lula?.electedBy).toBe('2º turno') + expect(jeronimo?.elected).toBe(true) + expect(jeronimo?.electedBy).toBe('2º turno') + expect(lula?.state).toBe('BA') + }) + + it('computes local winners per city+zone', () => { + const winners = computeWinnersByScope(built.votes) + const z1 = winners.get(winnerKey('38490', 1, 'deputado_federal', '1')) + const z2 = winners.get(winnerKey('38490', 2, 'deputado_federal', '1')) + expect(z1?.winnerCandidateNumber).toBe( + TSE_FIXTURE_EXPECTED.federalWinnerSalvadorZ1.candidateNumber, + ) + expect(z1?.winnerVotes).toBe(TSE_FIXTURE_EXPECTED.federalWinnerSalvadorZ1.votes) + expect(z2?.winnerCandidateNumber).toBe( + TSE_FIXTURE_EXPECTED.federalWinnerSalvadorZ2.candidateNumber, + ) + expect(z2?.winnerVotes).toBe(TSE_FIXTURE_EXPECTED.federalWinnerSalvadorZ2.votes) + + const merged = mergeTallyWithWinners(built.tallies, winners) + const federalZ1 = merged.find( + (t) => t.office === 'deputado_federal' && t.zoneNumber === 1 && t.cityCode === '38490', + ) + expect(federalZ1?.winnerCandidateName).toBe('JORGE SOLLA') + }) +})