From 9fdbae05d7aff1553029370135fc32d76080752b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Francisco=20Ign=C3=A1cio=20Fontoura=20Solla?=
Date: Sat, 18 Jul 2026 22:17:00 -0300
Subject: [PATCH] Add C6 scale/DRY for supporters: bulk import, KPI aggregate,
shared shells.
Phase 1 UX: StrictCombobox municipality in SupporterForm, AlertDialog in
RemoveSupporterDataButton. Phase 2 DRY: campaignListUrl + CampaignListPagination
+ campaignFormFields + mapCampaignFormActionError shared across nuclei,
supporters and action plans. Phase 3: single SQL aggregate for supporter list
KPIs via drizzle in the Payload txn. Phase 4: bulk drizzle insert for CSV import
inside the existing Payload txn with ON CONFLICT DO NOTHING, plus a fail-closed
skipContactPhoneInvariant context flag. Phase 5: HMAC-signed single-use import
token backed by a new supporterImportBatch staging collection so the full ok-row
set never crosses the Server Action boundary twice.
---
docs/plans/escala-dry-pos-c2.md | 39 +-
docs/roadmap.md | 4 +-
.../(app)/apoiadores/novo/formActions.ts | 27 +-
.../campanha/(app)/apoiadores/page.tsx | 9 +-
.../nucleos/[slug]/leadershipFormActions.ts | 40 +-
.../campanha/(app)/nucleos/page.tsx | 9 +-
.../(campaign)/campanha/(app)/planos/page.tsx | 10 +-
.../(campaign)/campanha/actions/supporter.ts | 193 +-
src/collections/Contact.ts | 14 +
src/collections/SupporterImportBatch.ts | 72 +
.../campaign/ActionPlanPagination.tsx | 61 -
.../campaign/CampaignListPagination.tsx | 68 +
src/components/campaign/LeadershipForm.tsx | 14 +-
src/components/campaign/NucleusPagination.tsx | 67 -
.../campaign/RemoveSupporterDataButton.tsx | 69 +-
src/components/campaign/SupporterForm.tsx | 29 +-
.../campaign/SupporterImportWizard.tsx | 28 +-
.../campaign/SupporterPagination.tsx | 61 -
src/lib/schemas/supporter.ts | 12 +-
...719_011015_add_supporter_import_batch.json | 9157 +++++++++++++++++
...60719_011015_add_supporter_import_batch.ts | 77 +
src/migrations/index.ts | 6 +
src/payload-types.ts | 39 +
src/payload.config.ts | 2 +
src/utilities/actionPlanUi.ts | 80 +-
src/utilities/campaignFormActionError.ts | 40 +
src/utilities/campaignFormFields.ts | 21 +
src/utilities/campaignListUrl.ts | 86 +
src/utilities/nucleusUi.ts | 80 +-
src/utilities/supporterImport.ts | 7 +-
src/utilities/supporterImportBulk.ts | 256 +
src/utilities/supporterImportToken.ts | 165 +
.../supporterListOverviewAggregate.ts | 149 +
src/utilities/supporterPageData.ts | 41 +-
src/utilities/supporterUi.ts | 80 +-
tests/e2e/campaign-supporters.e2e.spec.ts | 77 +
tests/e2e/fixtures/campaignE2EFixtures.ts | 17 +
tests/int/campaignSupporter.int.spec.ts | 150 +-
tests/unit/campaignNucleusUi.unit.spec.ts | 8 +-
tests/unit/supporterSchema.unit.spec.ts | 16 +-
40 files changed, 10712 insertions(+), 668 deletions(-)
create mode 100644 src/collections/SupporterImportBatch.ts
delete mode 100644 src/components/campaign/ActionPlanPagination.tsx
create mode 100644 src/components/campaign/CampaignListPagination.tsx
delete mode 100644 src/components/campaign/NucleusPagination.tsx
delete mode 100644 src/components/campaign/SupporterPagination.tsx
create mode 100644 src/migrations/20260719_011015_add_supporter_import_batch.json
create mode 100644 src/migrations/20260719_011015_add_supporter_import_batch.ts
create mode 100644 src/utilities/campaignFormActionError.ts
create mode 100644 src/utilities/campaignFormFields.ts
create mode 100644 src/utilities/campaignListUrl.ts
create mode 100644 src/utilities/supporterImportBulk.ts
create mode 100644 src/utilities/supporterImportToken.ts
create mode 100644 src/utilities/supporterListOverviewAggregate.ts
create mode 100644 tests/e2e/campaign-supporters.e2e.spec.ts
diff --git a/docs/plans/escala-dry-pos-c2.md b/docs/plans/escala-dry-pos-c2.md
index 9329d3de..d8ac0fdb 100644
--- a/docs/plans/escala-dry-pos-c2.md
+++ b/docs/plans/escala-dry-pos-c2.md
@@ -1,7 +1,7 @@
# Escala e DRY pós-C2 (apoiadores + listas)
-Status: rascunho
-Atualizado em: 2026-07-18
+Status: implementado (Fases 1–5)
+Atualizado em: 2026-07-19
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha C, item C6)
Responsável: —
@@ -121,3 +121,38 @@ flowchart TD
- `src/collections/Contact.ts` — `enforceUniqueContactPhone`
- `scripts/seed-tse-results.mjs` — bulk drizzle em txn
- AGENTS.md — `overrideAccess: false`, transações, Contact + locks, naming
+
+## Implementação (2026-07-19)
+
+Todas as cinco fases foram implementadas e validadas localmente (`tsc --noEmit`, `pnpm lint`, `pnpm test:int`, `pnpm test:unit`) contra `teqo_test`. Sem regressões nas rotas de núcleos/apoiadores/planos já entregues.
+
+### Fase 1 — UX alinhada
+- `SupporterForm` agora usa `StrictCombobox` + `municipalityComboboxOptions()` no município (validação continua em `optionalBahiaCity`).
+- `RemoveSupporterDataButton` trocou `window.confirm` por `AlertDialog` no padrão de `ArchiveNucleusDialog.tsx`.
+
+### Fase 2 — DRY com núcleos / leadership
+- `src/utilities/campaignListUrl.ts` (novo): `firstValue`, `normalizedText`, `strictDecimalInteger`, `inspectRawListParams`, `resolveListUrl`, `buildListHref`. `nucleusUi.ts`, `supporterUi.ts` e `actionPlanUi.ts` delegam para ele (domínio permanece nos módulos de domínio).
+- `src/components/campaign/CampaignListPagination.tsx` (novo) + `getPaginationPages`: substitui `NucleusPagination`, `SupporterPagination` e `ActionPlanPagination` (os três foram removidos). Listagens de núcleos/apoiadores/planos e o teste unitário `campaignNucleusUi` apontam para o novo componente.
+- `src/utilities/campaignFormFields.ts` (novo): `fieldError` / `errorProps` compartilhados por `SupporterForm` e `LeadershipForm` (cada form liga um `errorProps` local ao seu prefixo de id).
+- `src/utilities/campaignFormActionError.ts` (novo): `mapCampaignFormActionError` consolida FormDataBoundaryError → Zod → safelist → genérico. `apoiadores/novo/formActions.ts`, `apoiadores/[id]/formActions.ts` e `nucleos/[slug]/leadershipFormActions.ts` usam o mapper.
+
+### Fase 3 — KPI da lista
+- `src/utilities/supporterListOverviewAggregate.ts` (novo): `computeSupporterListOverviewAggregate` faz um único `SELECT COUNT(*) FILTER (...)` via drizzle na sessão da txn Payload, espelhando `buildSupporterListWhere` + a constraint de acesso (`nucleus_id IN (...)` para coordenador, sem constraint para geral, `none` para liderança). `loadSupporterListOverviewData` passou de 3× `payload.count` para um round-trip. Testes int cobrem geral, coordenador (escopo + filtro de cidade) e base vazia.
+
+### Fase 4 — Import em escala
+- `src/utilities/supporterImportBulk.ts` (novo): `bulkInsertSupporterImport` insere contacts e supporters via `payload.db.drizzle` na sessão da txn Payload (`getPostgresTransactionDatabase`), em chunks de 500, com `ON CONFLICT DO NOTHING` no índice unique `(contact_id, nucleus_id)`. Substitui até 10k `payload.create` por poucos inserts bulk.
+- `src/collections/Contact.ts`: `enforceUniqueContactPhone` honra `context.skipContactPhoneInvariant` **fail-closed** — só respeita quando `req.transactionID` está ativo (sem txn, os locks xact-level não podem estar segurados → throw). `upsertContactByPhone` passa o context (o caller já segurou os locks), eliminando o re-lock/re-check redundante do path single-create.
+
+### Fase 5 — Preview sem round-trip
+- `src/collections/SupporterImportBatch.ts` (novo, admin hidden, acesso `geral`/admin) + migration `20260719_011015_add_supporter_import_batch`: staging efêmero do conjunto `ok` no servidor.
+- `src/utilities/supporterImportToken.ts` (novo): token HMAC-SHA256(`${batchId}.${actorID}.${expiresAt}`, PAYLOAD_SECRET) com `timingSafeEqual` e TTL 10 min. `previewSupporterImportText` agora retorna `{ counts, sampleRows (100), errorReportCsv, importToken }` e estageia o conjunto `ok` em `supporterImportBatch`. `confirmSupporterImportRecord` aceita `importToken` (schema trocou `rows` por `importToken`), verifica HMAC + ator + expiração, consome o lote (single-use, delete após commit) e alimenta o bulk insert. O wizard parou de guardar/reenviar as rows.
+- Testes int: fluxo preview→confirm; token tampered (assinatura alterada) rejeitado; token de outro ator rejeitado; reuso de token consumido rejeitado.
+
+## Achados da auditoria (decisões de implementação)
+
+- **Nomes de coluna drizzle para relationships NÃO levam sufixo `Id`.** Em `payload.db.tables.supporter`, as colunas de relationship são `contact`, `nucleus`, `consent`, `voteIntentionConsent`, `createdBy` — não `contactId`, etc. O insert bulk inicial usou `contactId`/`createdById` e o drizzle silenciou essas chaves (gerou `default` → violação NOT NULL). Corrigido para os nomes sem sufixo. Lição para C7 e futuros inserts bulk: inspecionar `Object.keys(payload.db.tables.)` antes de montar as rows.
+- **`payload-preferences` não serve de staging genérico.** Tem campo `user` (relationship users/campaignUser) obrigatório e validado em `beforeChange`, e semântica de preferência por usuário. Tentar usá-lo para o lote de importação falhou com `ValidationError: User`. Decidido pela tabela dedicada `supporterImportBatch` (migration), como o plano já previa como fallback. Não há Redis novo no v1 do C6.
+- **Bulk insert deve rodar na txn Payload existente, não numa txn drizzle nova.** O seed TSE (`electionResultsImport.ts`) abre `dbAdapter.drizzle.transaction(...)` própria; para C4 isso quebraria a atomicidade com os locks advisory. Usado `getPostgresTransactionDatabase(payload, req)` para pegar a sessão drizzle vinculada à txn Payload.
+- **`skipContactPhoneInvariant` é fail-closed por txn, não por introspecção de lock.** Não há maneira barata de perguntar ao PG "seguro o lock X?"; o proxy usado é `req.transactionID` ativo (o lock `pg_advisory_xact_lock` é xact-level e só existe dentro de uma txn). Sem txn + flag setado → throw 500.
+- **`supporter.beforeChange` (coexistência com liderança) é no-op para import sem núcleo.** O hook só dispara quando `contactID` e `nucleusID` estão presentes; import cria supporters sem núcleo, então o bypass via drizzle insert é seguro. `createdBy` é setado explicitamente no row bulk.
+- **Escopo do mapper de form errors:** `ActionPlanPagination` e os forms de planos também consomem `campaignListUrl`/`CampaignListPagination`, mas o mapper de form actions de planos ficou para C7 (escopo de copy/mensagens diferente).
diff --git a/docs/roadmap.md b/docs/roadmap.md
index fc7a5839..bde596e0 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -1,6 +1,6 @@
# Roadmap — Teqo
-Atualizado em: 2026-07-18 (MVP + Ciclo 2 deployados; A2 entregue; C2 engenharia pronta e mesclada em `main`; C3 Planos de Ação implementado e mesclado em `main`; C6/C7 registrados — escala/DRY pós-C2 e pós-C3; B2 fundação de geometrias do mapa entregue; B5 registrado — escala/DRY pós-B2 do `/simplify`)
+Atualizado em: 2026-07-19 (MVP + Ciclo 2 deployados; A2 entregue; C2 engenharia pronta e mesclada em `main`; C3 Planos de Ação implementado e mesclado em `main`; C6 Escala e DRY pós-C2 implementado (Fases 1–5) — aguarda deploy; C7 registrado — escala/DRY pós-C3; B2 fundação de geometrias do mapa entregue; B5 registrado — escala/DRY pós-B2 do `/simplify`)
Registro canônico no repositório dos planos futuros e débitos conhecidos. Status operacional do ciclo atual de Núcleos fica em [`.cursor/rules/projects/nucleos-eleitorais.mdc`](../.cursor/rules/projects/nucleos-eleitorais.mdc); este arquivo lista o que ainda é futuro ou bloqueador, **em ordem de execução**, com dependências e paralelismo explícitos.
@@ -184,7 +184,7 @@ Itens sem seta de entrada (**paralelizáveis a qualquer momento**): fill-ins (vi
| Ordem | Item | Plano | Depende de | Paralelizável com |
| ----- | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | -------------------------------- | ----------------- |
| 7 | C2 Cadastro nominal de apoiadores _(engenharia pronta e mesclada em `main` 2026-07-18 — deploy + Consent keys ainda pendentes)_ | [detalhes](plans/cadastro-nominal-apoiadores.md) | produção: lote jurídico (Onda 0) | A4, C3, C6, C7 |
-| 8 | C6 Escala e DRY pós-C2 (import em massa, KPI, shells compartilhados com núcleos) | [detalhes](plans/escala-dry-pos-c2.md) | C2 (merge) | A4, C3, C7 |
+| 8 | C6 Escala e DRY pós-C2 (import em massa, KPI, shells compartilhados com núcleos) **(implementado 2026-07-19 — Fases 1–5; aguarda deploy)** | [detalhes](plans/escala-dry-pos-c2.md) | C2 (merge) | A4, C3, C7 |
| 9 | A4 Baseline no produto + insight Gap vs 2022 | [detalhes](plans/baseline-eleitoral-tse.md) | A3 + B1 (suave: A2) | C2, C3, C6, C7 |
| 10 | C3 Eventos / agenda de mobilização (`actionPlan`) **(implementado e mesclado em `main` 2026-07-18)** | [detalhes](plans/eventos-agenda-mobilizacao.md) | A1 | C2, A4, C6, C7 |
| 11 | C7 Escala e DRY pós-C3 (território/contato compostos, leituras por aba, RMW/índice) | [detalhes](plans/escala-dry-pos-c3.md) | C3 (suave: C6 F2) | A4, C6 |
diff --git a/src/app/(campaign)/campanha/(app)/apoiadores/novo/formActions.ts b/src/app/(campaign)/campanha/(app)/apoiadores/novo/formActions.ts
index 3db2cda4..faa5a8dd 100644
--- a/src/app/(campaign)/campanha/(app)/apoiadores/novo/formActions.ts
+++ b/src/app/(campaign)/campanha/(app)/apoiadores/novo/formActions.ts
@@ -1,17 +1,15 @@
'use server'
import { revalidatePath } from 'next/cache'
-import { ZodError } from 'zod'
import { createSupporter } from '@/app/(campaign)/campanha/actions/supporter'
import {
checkboxFormValue,
- FormDataBoundaryError,
nullableRelationshipFormValue,
optionalFormText,
- validationFieldErrors,
} from '@/lib/formData'
import { supporterCreateSchema } from '@/lib/schemas/supporter'
+import { mapCampaignFormActionError } from '@/utilities/campaignFormActionError'
import { sanitizeBrazilianPhoneInput } from '@/utilities/phone'
export type SupporterFormState = {
@@ -45,25 +43,14 @@ const getSupporterFormError = (
error: unknown,
values?: SupporterFormValues,
revision?: number,
-): SupporterFormState => {
- if (error instanceof FormDataBoundaryError) {
- return { fieldErrors: { [error.field]: [error.message] }, values, revision }
- }
- if (error instanceof ZodError) {
- return { fieldErrors: validationFieldErrors(error), values, revision }
- }
- if (
- error instanceof Error &&
- safeActionMessages.includes(error.message as (typeof safeActionMessages)[number])
- ) {
- return { message: error.message, values, revision }
- }
- return {
- message: 'Não foi possível cadastrar o apoiador. Verifique os dados e tente novamente.',
+): SupporterFormState =>
+ mapCampaignFormActionError({
+ error,
+ safeMessages: safeActionMessages,
+ genericMessage: 'Não foi possível cadastrar o apoiador. Verifique os dados e tente novamente.',
values,
revision,
- }
-}
+ })
export const createSupporterFormAction = async (
state: SupporterFormState,
diff --git a/src/app/(campaign)/campanha/(app)/apoiadores/page.tsx b/src/app/(campaign)/campanha/(app)/apoiadores/page.tsx
index d451a134..703d6570 100644
--- a/src/app/(campaign)/campanha/(app)/apoiadores/page.tsx
+++ b/src/app/(campaign)/campanha/(app)/apoiadores/page.tsx
@@ -4,11 +4,11 @@ import { redirect } from 'next/navigation'
import config from '@payload-config'
import { getPayload } from 'payload'
+import { CampaignListPagination } from '@/components/campaign/CampaignListPagination'
import { CampaignScopeBadge } from '@/components/campaign/CampaignScopeBadge'
import { SupporterFilters } from '@/components/campaign/SupporterFilters'
import { SupporterList } from '@/components/campaign/SupporterList'
import { SupporterListOverview } from '@/components/campaign/SupporterListOverview'
-import { SupporterPagination } from '@/components/campaign/SupporterPagination'
import { Button } from '@/components/ui/button'
import {
Empty,
@@ -28,6 +28,7 @@ import {
import { toSupporterListItemViewModel } from '@/utilities/supporterViewModels'
import {
buildSupporterFiltersKey,
+ buildSupporterListHref,
canAccessSupporterArea,
getSupporterScopeLabel,
resolveSupporterListUrl,
@@ -101,7 +102,11 @@ export default async function SupportersPage({ searchParams }: SupportersPagePro
{result.totalDocs}{' '}
{result.totalDocs === 1 ? 'apoiador encontrado' : 'apoiadores encontrados'}
-
+ buildSupporterListHref(state, page)}
+ />
>
) : (
diff --git a/src/app/(campaign)/campanha/(app)/nucleos/[slug]/leadershipFormActions.ts b/src/app/(campaign)/campanha/(app)/nucleos/[slug]/leadershipFormActions.ts
index 1cca5c12..9386038d 100644
--- a/src/app/(campaign)/campanha/(app)/nucleos/[slug]/leadershipFormActions.ts
+++ b/src/app/(campaign)/campanha/(app)/nucleos/[slug]/leadershipFormActions.ts
@@ -1,7 +1,6 @@
'use server'
import { revalidatePath } from 'next/cache'
-import { ZodError } from 'zod'
import {
createLeadership,
@@ -14,8 +13,8 @@ import {
nullableFormText,
optionalFormText,
requiredRelationshipFormValue,
- validationFieldErrors,
} from '@/lib/formData'
+import { mapCampaignFormActionError } from '@/utilities/campaignFormActionError'
export type LeadershipFormState = {
status?: 'success'
@@ -46,35 +45,26 @@ const safeActionMessages = [
'Somente a coordenação pode gerenciar lideranças.',
] as const
+const resolveLeadershipBoundaryMessage = (error: FormDataBoundaryError): string =>
+ error.field === 'contact'
+ ? 'Não foi possível identificar a liderança. Atualize a página e tente novamente.'
+ : error.field === 'id' || error.field === 'nucleus'
+ ? 'Não foi possível identificar o registro. Atualize a página e tente novamente.'
+ : error.message
+
const getLeadershipFormError = (
error: unknown,
values?: LeadershipFormValues,
revision?: number,
-): LeadershipFormState => {
- if (error instanceof FormDataBoundaryError) {
- const message =
- error.field === 'contact'
- ? 'Não foi possível identificar a liderança. Atualize a página e tente novamente.'
- : error.field === 'id' || error.field === 'nucleus'
- ? 'Não foi possível identificar o registro. Atualize a página e tente novamente.'
- : error.message
- return { fieldErrors: { [error.field]: [message] }, values, revision }
- }
- if (error instanceof ZodError) {
- return { fieldErrors: validationFieldErrors(error), values, revision }
- }
- if (
- error instanceof Error &&
- safeActionMessages.includes(error.message as (typeof safeActionMessages)[number])
- ) {
- return { message: error.message, values, revision }
- }
- return {
- message: 'Não foi possível salvar a liderança. Verifique os dados e tente novamente.',
+): LeadershipFormState =>
+ mapCampaignFormActionError({
+ error,
+ safeMessages: safeActionMessages,
+ genericMessage: 'Não foi possível salvar a liderança. Verifique os dados e tente novamente.',
values,
revision,
- }
-}
+ resolveBoundaryMessage: resolveLeadershipBoundaryMessage,
+ })
export const createLeadershipFormAction = async (
state: LeadershipFormState,
diff --git a/src/app/(campaign)/campanha/(app)/nucleos/page.tsx b/src/app/(campaign)/campanha/(app)/nucleos/page.tsx
index 370cdd0e..c0242f4c 100644
--- a/src/app/(campaign)/campanha/(app)/nucleos/page.tsx
+++ b/src/app/(campaign)/campanha/(app)/nucleos/page.tsx
@@ -6,10 +6,10 @@ import { redirect } from 'next/navigation'
import { getPayload } from 'payload'
import { CampaignScopeBadge } from '@/components/campaign/CampaignScopeBadge'
+import { CampaignListPagination } from '@/components/campaign/CampaignListPagination'
import { NucleusFilters } from '@/components/campaign/NucleusFilters'
import { NucleusList } from '@/components/campaign/NucleusList'
import { NucleusListOverview } from '@/components/campaign/NucleusListOverview'
-import { NucleusPagination } from '@/components/campaign/NucleusPagination'
import { Button } from '@/components/ui/button'
import {
Empty,
@@ -24,6 +24,7 @@ import { loadNucleusListOverviewData } from '@/utilities/nucleusListOverviewPage
import { loadNucleusListPageData } from '@/utilities/nucleusPageData'
import {
buildNucleusFiltersKey,
+ buildNucleusListHref,
getCampaignScopeLabel,
resolveNucleusListUrl,
} from '@/utilities/nucleusUi'
@@ -89,7 +90,11 @@ export default async function NucleiPage({ searchParams }: NucleiPageProps) {
{result.totalDocs}{' '}
{result.totalDocs === 1 ? 'núcleo encontrado' : 'núcleos encontrados'}
-
+ buildNucleusListHref(state, page)}
+ />
>
) : (
diff --git a/src/app/(campaign)/campanha/(app)/planos/page.tsx b/src/app/(campaign)/campanha/(app)/planos/page.tsx
index b72ddecc..efa88550 100644
--- a/src/app/(campaign)/campanha/(app)/planos/page.tsx
+++ b/src/app/(campaign)/campanha/(app)/planos/page.tsx
@@ -6,7 +6,7 @@ import { getPayload } from 'payload'
import { ActionPlanFilters } from '@/components/campaign/ActionPlanFilters'
import { ActionPlanList } from '@/components/campaign/ActionPlanList'
-import { ActionPlanPagination } from '@/components/campaign/ActionPlanPagination'
+import { CampaignListPagination } from '@/components/campaign/CampaignListPagination'
import { Button } from '@/components/ui/button'
import {
Empty,
@@ -19,7 +19,7 @@ import {
import type { ActionPlan } from '@/payload-types'
import { getCampaignUser } from '@/utilities/campaignAuth'
import { loadActionPlanListPageData } from '@/utilities/actionPlanPageData'
-import { buildActionPlanFiltersKey, resolveActionPlanListUrl } from '@/utilities/actionPlanUi'
+import { buildActionPlanFiltersKey, buildActionPlanListHref, resolveActionPlanListUrl } from '@/utilities/actionPlanUi'
import { toActionPlanListViewModel } from '@/utilities/actionPlanViewModels'
type ActionPlanListPageProps = {
@@ -71,7 +71,11 @@ export default async function ActionPlanListPage({ searchParams }: ActionPlanLis
{result.totalDocs} {result.totalDocs === 1 ? 'plano encontrado' : 'planos encontrados'}
-
+ buildActionPlanListHref(state, page)}
+ />
>
) : (
diff --git a/src/app/(campaign)/campanha/actions/supporter.ts b/src/app/(campaign)/campanha/actions/supporter.ts
index 02fe6f69..965a7a7d 100644
--- a/src/app/(campaign)/campanha/actions/supporter.ts
+++ b/src/app/(campaign)/campanha/actions/supporter.ts
@@ -23,6 +23,14 @@ import type { PayloadTransactionRequest } from '@/utilities/payloadTransaction'
import { withPayloadTransaction } from '@/utilities/payloadTransaction'
import { normalizeBrazilianPhone } from '@/utilities/phone'
import { relationshipId, requireRelationshipId } from '@/utilities/relationship'
+import { bulkInsertSupporterImport } from '@/utilities/supporterImportBulk'
+import {
+ issueSupporterImportToken,
+ loadSupporterImportBatch,
+ deleteSupporterImportBatch,
+ storeSupporterImportBatch,
+ verifySupporterImportToken,
+} from '@/utilities/supporterImportToken'
import {
isSupporterImportOkRow,
type SupporterImportPreviewResult,
@@ -39,6 +47,35 @@ export type {
const MAX_IMPORT_ROWS = 5000
+/** On-screen preview is capped; the full ok set is staged server-side (Phase 5). */
+const SUPPORTER_IMPORT_SAMPLE_SIZE = 100
+
+const isPreviewErrorRow = (row: SupporterImportPreviewRow): boolean =>
+ row.status !== 'ok' && row.status !== 'duplicado_pelo_telefone'
+
+const importStatusCsvValue: Record = {
+ ok: 'ok',
+ duplicado_pelo_telefone: 'duplicado_pelo_telefone',
+ telefone_invalido: 'telefone_invalido',
+ municipio_nao_reconhecido: 'municipio_nao_reconhecido',
+ nome_invalido: 'nome_invalido',
+ intencao_invalida: 'intencao_invalida',
+}
+
+const escapeCsvCell = (value: string): string => `"${value.replace(/"/g, '""')}"`
+
+const buildSupporterImportErrorReportCsv = (rows: SupporterImportPreviewRow[]): string => {
+ const header = 'linha,nome,telefone,municipio,intencao,status\n'
+ const body = rows
+ .filter(isPreviewErrorRow)
+ .map(
+ (row) =>
+ `${row.line},${escapeCsvCell(row.nome)},${escapeCsvCell(row.telefone)},${escapeCsvCell(row.municipio)},${escapeCsvCell(row.intencao)},${importStatusCsvValue[row.status]}`,
+ )
+ .join('\n')
+ return header + body
+}
+
const getFreshStaffActor = async (
payload: Payload,
actor: CampaignUser,
@@ -149,6 +186,10 @@ const upsertContactByPhone = async ({
},
depth: 0,
overrideAccess: true,
+ // Callers of upsertContactByPhone have already acquired the phone advisory
+ // lock in the same transaction, so the Contact phone-invariant hook can
+ // skip its redundant lock+availability check.
+ context: { skipContactPhoneInvariant: true },
req,
})
@@ -333,7 +374,7 @@ export const previewSupporterImportText = async (
actor: CampaignUser,
csvText: string,
): Promise => {
- await getFreshGeneralActor(payload, actor)
+ const currentActor = await getFreshGeneralActor(payload, actor)
await requireSupporterRegistrationConsent(payload)
await requireSupporterVoteIntentionConsent(payload)
@@ -490,7 +531,31 @@ export const previewSupporterImportText = async (
.length,
}
- return { rows, counts }
+ const okRows = rows.filter(isSupporterImportOkRow).map((row) => ({
+ nome: row.nome,
+ telefone: row.normalizedPhone,
+ municipio: row.canonicalCity,
+ intencao: row.voteIntention,
+ }))
+
+ const errorReportCsv = buildSupporterImportErrorReportCsv(rows)
+ const sampleRows = rows.slice(0, SUPPORTER_IMPORT_SAMPLE_SIZE)
+
+ // Stage the full ok set server-side keyed by an HMAC-signed token so the wizard
+ // never has to hold/re-send thousands of rows across the Server Action boundary.
+ const issued = issueSupporterImportToken(currentActor.id)
+ await storeSupporterImportBatch(payload, issued.batchId, {
+ actorID: currentActor.id,
+ expiresAt: issued.expiresAt,
+ okRows,
+ })
+
+ return {
+ counts,
+ sampleRows,
+ errorReportCsv,
+ importToken: issued.token,
+ }
}
export const previewSupporterImport = async (csvText: string) => {
@@ -505,6 +570,14 @@ export const confirmSupporterImportRecord = async (
) => {
const data = supporterImportConfirmSchema.parse(input)
+ // Verify the HMAC token and load the staged batch BEFORE opening the write
+ // transaction, so an invalid/expired token is rejected without holding locks.
+ const verified = verifySupporterImportToken(data.importToken, actor.id)
+ const batch = await loadSupporterImportBatch(payload, verified.batchId, actor.id)
+ if (batch.okRows.length === 0) {
+ throw new Error('O lote de importação não contém apoiadores válidos.')
+ }
+
return withPayloadTransaction(
payload,
async ({ req }) => {
@@ -516,111 +589,31 @@ export const confirmSupporterImportRecord = async (
throw new Error('O bloqueio de deduplicação exige o adaptador PostgreSQL.')
}
- const phones = [...new Set(data.rows.map((row) => row.telefone))]
+ const phones = [...new Set(batch.okRows.map((row) => row.telefone))]
await acquireContactPhoneLocks(payload, req, phones)
- const existingContacts = await payload.find({
- collection: 'contact',
- where: { phone: { in: phones } },
- depth: 0,
- limit: phones.length,
- pagination: false,
- select: { phone: true },
- overrideAccess: true,
- req,
- })
- const contactIdByPhone = new Map(
- existingContacts.docs.map((doc) => [doc.phone, doc.id] as const),
- )
-
- for (const row of data.rows) {
- if (contactIdByPhone.has(row.telefone)) continue
- const { contactID } = await upsertContactByPhone({
- payload,
- req,
- phone: row.telefone,
- name: row.nome,
- city: row.municipio,
- })
- contactIdByPhone.set(row.telefone, contactID)
- }
-
- const contactIDs = [...new Set(contactIdByPhone.values())]
- const existingSupporters = await payload.find({
- collection: 'supporter',
- where: {
- and: [{ contact: { in: contactIDs } }, { nucleus: { exists: false } }],
- },
- depth: 0,
- limit: contactIDs.length,
- pagination: false,
- select: { contact: true },
- overrideAccess: true,
- req,
- })
- const supporterContactIDs = new Set(
- existingSupporters.docs
- .map((doc) => relationshipId(doc.contact))
- .filter((id): id is number => id !== null),
- )
-
- let created = 0
- let skipped = 0
- const errors: Array<{ telefone: string; message: string }> = []
const consentNote =
data.consentNote ??
'Importação CSV com atestado do operador sobre consentimento dos titulares.'
- const consentedAt = new Date().toISOString()
- for (const row of data.rows) {
- const contactID = contactIdByPhone.get(row.telefone)
- if (!contactID) {
- skipped += 1
- errors.push({ telefone: row.telefone, message: 'Contato não encontrado após upsert.' })
- continue
- }
+ const result = await bulkInsertSupporterImport({
+ payload,
+ req,
+ actorID: currentActor.id,
+ rows: batch.okRows.map((row) => ({
+ telefone: row.telefone,
+ nome: row.nome,
+ municipio: row.municipio,
+ intencao: row.intencao,
+ })),
+ registrationConsent,
+ voteIntentionConsent,
+ consentNote,
+ })
- if (supporterContactIDs.has(contactID)) {
- skipped += 1
- continue
- }
-
- try {
- await payload.create({
- collection: 'supporter',
- data: {
- contact: contactID,
- source: 'import_csv',
- voteIntention: row.intencao,
- consent: registrationConsent.id,
- consentContentHash: registrationConsent.contentHash,
- consentedAt,
- consentNote,
- ...(row.intencao
- ? {
- voteIntentionConsent: voteIntentionConsent.id,
- voteIntentionConsentContentHash: voteIntentionConsent.contentHash,
- voteIntentionConsentedAt: consentedAt,
- }
- : {}),
- createdBy: currentActor.id,
- },
- depth: 0,
- overrideAccess: true,
- req,
- })
- supporterContactIDs.add(contactID)
- created += 1
- } catch (error) {
- skipped += 1
- errors.push({
- telefone: row.telefone,
- message: error instanceof Error ? error.message : 'Erro ao importar linha.',
- })
- }
- }
-
- return { created, skipped, errors }
+ // Single-use token: drop the staged batch once the import commits.
+ await deleteSupporterImportBatch(payload, verified.batchId, req)
+ return result
},
{ beginFailureMessage: 'Não foi possível iniciar a transação de importação de apoiadores.' },
)
diff --git a/src/collections/Contact.ts b/src/collections/Contact.ts
index 01d7ff82..87f7c0e0 100644
--- a/src/collections/Contact.ts
+++ b/src/collections/Contact.ts
@@ -17,6 +17,20 @@ const enforceUniqueContactPhone: CollectionBeforeChangeHook = async ({
originalDoc,
req,
}) => {
+ // Opt-out for callers that have already acquired the phone advisory locks in the
+ // SAME Payload transaction (e.g. bulk import). Fail closed: the flag is only
+ // honored inside an active transaction — without one the xact-level advisory
+ // lock cannot be held, so we run the full check regardless of the caller's claim.
+ if (req.context?.skipContactPhoneInvariant === true) {
+ if (req.transactionID === undefined || req.transactionID === null) {
+ throw new APIError(
+ 'skipContactPhoneInvariant exige uma transação ativa com os locks de telefone já adquiridos.',
+ 500,
+ )
+ }
+ return data
+ }
+
const phone = String(data.phone ?? originalDoc?.phone ?? '')
const oldPhone =
operation === 'update' && typeof originalDoc?.phone === 'string'
diff --git a/src/collections/SupporterImportBatch.ts b/src/collections/SupporterImportBatch.ts
new file mode 100644
index 00000000..b4582f3f
--- /dev/null
+++ b/src/collections/SupporterImportBatch.ts
@@ -0,0 +1,72 @@
+import type { Access, CollectionConfig } from 'payload'
+
+import { getFreshCampaignUser, isCampaignGeneral, isPayloadAdmin } from '@/utilities/campaignAccess'
+
+/**
+ * Short-lived server-side staging for the supporter CSV import wizard (roadmap
+ * C6, Phase 5). The preview stores the full `ok` row set here keyed by an
+ * HMAC-signed token; the confirm step redeems the token, consumes the batch,
+ * and bulk-inserts. This keeps thousands of rows from crossing the Server
+ * Action boundary twice.
+ *
+ * The collection is hidden from the admin nav and restricted to `geral` /
+ * Payload-admin; the import actions interact with it via the Local API with
+ * `overrideAccess: true`. Rows hold transient supporter PII and must be
+ * purged on consume or after `expiresAt` (lazy cleanup + sweep).
+ */
+const canAccessImportBatch: Access = async ({ req }) => {
+ if (isPayloadAdmin(req.user)) return true
+ const fresh = await getFreshCampaignUser(req)
+ return isCampaignGeneral(fresh)
+}
+
+export const SupporterImportBatch: CollectionConfig = {
+ slug: 'supporterImportBatch',
+ labels: {
+ singular: 'Lote de importação',
+ plural: 'Lotes de importação',
+ },
+ admin: {
+ group: 'Campanha',
+ hidden: () => true,
+ useAsTitle: 'batchId',
+ defaultColumns: ['batchId', 'actor', 'expiresAt'],
+ },
+ access: {
+ create: canAccessImportBatch,
+ read: canAccessImportBatch,
+ update: canAccessImportBatch,
+ delete: canAccessImportBatch,
+ },
+ fields: [
+ {
+ name: 'batchId',
+ type: 'text',
+ label: 'ID do lote',
+ required: true,
+ unique: true,
+ index: true,
+ },
+ {
+ name: 'actor',
+ type: 'relationship',
+ relationTo: 'campaignUser',
+ label: 'Operador',
+ required: true,
+ index: true,
+ },
+ {
+ name: 'expiresAt',
+ type: 'date',
+ label: 'Expira em',
+ required: true,
+ index: true,
+ },
+ {
+ name: 'rows',
+ type: 'json',
+ label: 'Linhas válidas',
+ required: true,
+ },
+ ],
+}
diff --git a/src/components/campaign/ActionPlanPagination.tsx b/src/components/campaign/ActionPlanPagination.tsx
deleted file mode 100644
index 713d825d..00000000
--- a/src/components/campaign/ActionPlanPagination.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import { getNucleusPaginationPages } from '@/components/campaign/NucleusPagination'
-import {
- Pagination,
- PaginationContent,
- PaginationEllipsis,
- PaginationItem,
- PaginationLink,
- PaginationNext,
- PaginationPrevious,
-} from '@/components/ui/Pagination'
-import { buildActionPlanListHref, type ActionPlanListState } from '@/utilities/actionPlanUi'
-
-export const ActionPlanPagination = ({
- state,
- totalPages,
-}: {
- state: ActionPlanListState
- totalPages: number
-}) => {
- if (totalPages <= 1) return null
-
- const visiblePages = getNucleusPaginationPages(state.page, totalPages)
-
- return (
-
-
-
-
-
- {visiblePages.map((page, index) => {
- const previousPage = visiblePages[index - 1]
- const hasGap = previousPage !== undefined && page - previousPage > 1
-
- return (
-
- {hasGap ? : null}
-
- {page}
-
-
- )
- })}
-
- = totalPages}
- className={state.page >= totalPages ? 'pointer-events-none opacity-50' : undefined}
- />
-
-
-
- )
-}
diff --git a/src/components/campaign/CampaignListPagination.tsx b/src/components/campaign/CampaignListPagination.tsx
new file mode 100644
index 00000000..9270e33d
--- /dev/null
+++ b/src/components/campaign/CampaignListPagination.tsx
@@ -0,0 +1,68 @@
+import {
+ Pagination,
+ PaginationContent,
+ PaginationEllipsis,
+ PaginationItem,
+ PaginationLink,
+ PaginationNext,
+ PaginationPrevious,
+} from '@/components/ui/Pagination'
+
+export const getPaginationPages = (page: number, totalPages: number): number[] => {
+ const pages = new Set([1, totalPages, page - 1, page, page + 1])
+ return [...pages]
+ .filter((candidate) => candidate >= 1 && candidate <= totalPages)
+ .sort((a, b) => a - b)
+}
+
+export const CampaignListPagination = ({
+ page,
+ totalPages,
+ hrefForPage,
+}: {
+ page: number
+ totalPages: number
+ hrefForPage: (page: number) => string
+}) => {
+ if (totalPages <= 1) return null
+
+ const visiblePages = getPaginationPages(page, totalPages)
+
+ return (
+
+
+
+
+
+ {visiblePages.map((pageNumber, index) => {
+ const previousPage = visiblePages[index - 1]
+ const hasGap = previousPage !== undefined && pageNumber - previousPage > 1
+
+ return (
+
+ {hasGap ? : null}
+
+ {pageNumber}
+
+
+ )
+ })}
+
+ = totalPages}
+ className={page >= totalPages ? 'pointer-events-none opacity-50' : undefined}
+ />
+
+
+
+ )
+}
diff --git a/src/components/campaign/LeadershipForm.tsx b/src/components/campaign/LeadershipForm.tsx
index 7917f2b8..073a5976 100644
--- a/src/components/campaign/LeadershipForm.tsx
+++ b/src/components/campaign/LeadershipForm.tsx
@@ -27,6 +27,7 @@ import { Textarea } from '@/components/ui/textarea'
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/ToggleGroup'
import { leadershipGenderLabels, leadershipSectorLabels } from '@/utilities/leadershipUi'
import type { LeadershipEditViewModel } from '@/utilities/leadershipViewModels'
+import { fieldError, errorProps as buildErrorProps } from '@/utilities/campaignFormFields'
import { formatBrazilianPhoneInput, sanitizeBrazilianPhoneInput } from '@/utilities/phone'
export type LeadershipFormAction = (
@@ -49,19 +50,10 @@ const supportStatusOptions = [
{ value: 'negativo', label: 'Negativo' },
] as const
-const fieldError = (
+const errorProps = (
fieldErrors: Record | undefined,
field: string,
-): string | undefined => fieldErrors?.[field]?.[0]
-
-const errorProps = (fieldErrors: Record | undefined, field: string) => {
- const error = fieldError(fieldErrors, field)
- return {
- error,
- invalid: Boolean(error),
- describedBy: error ? `leadership-${field}-error` : undefined,
- }
-}
+) => buildErrorProps(fieldErrors, field, 'leadership')
export const LeadershipFormFeedback = ({
message,
diff --git a/src/components/campaign/NucleusPagination.tsx b/src/components/campaign/NucleusPagination.tsx
deleted file mode 100644
index 4cbcb389..00000000
--- a/src/components/campaign/NucleusPagination.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import {
- Pagination,
- PaginationContent,
- PaginationEllipsis,
- PaginationItem,
- PaginationLink,
- PaginationNext,
- PaginationPrevious,
-} from '@/components/ui/Pagination'
-import { buildNucleusListHref, type NucleusListState } from '@/utilities/nucleusUi'
-
-export const getNucleusPaginationPages = (page: number, totalPages: number): number[] => {
- const pages = new Set([1, totalPages, page - 1, page, page + 1])
- return [...pages]
- .filter((candidate) => candidate >= 1 && candidate <= totalPages)
- .sort((a, b) => a - b)
-}
-
-export const NucleusPagination = ({
- state,
- totalPages,
-}: {
- state: NucleusListState
- totalPages: number
-}) => {
- if (totalPages <= 1) return null
-
- const visiblePages = getNucleusPaginationPages(state.page, totalPages)
-
- return (
-
-
-
-
-
- {visiblePages.map((page, index) => {
- const previousPage = visiblePages[index - 1]
- const hasGap = previousPage !== undefined && page - previousPage > 1
-
- return (
-
- {hasGap ? : null}
-
- {page}
-
-
- )
- })}
-
- = totalPages}
- className={state.page >= totalPages ? 'pointer-events-none opacity-50' : undefined}
- />
-
-
-
- )
-}
diff --git a/src/components/campaign/RemoveSupporterDataButton.tsx b/src/components/campaign/RemoveSupporterDataButton.tsx
index 118ff054..e28167e3 100644
--- a/src/components/campaign/RemoveSupporterDataButton.tsx
+++ b/src/components/campaign/RemoveSupporterDataButton.tsx
@@ -5,6 +5,16 @@ import { useActionState } from 'react'
import type { SupporterRemoveFormState } from '@/app/(campaign)/campanha/(app)/apoiadores/[id]/formActions'
import { removeSupporterDataFormAction } from '@/app/(campaign)/campanha/(app)/apoiadores/[id]/formActions'
import { Alert, AlertDescription } from '@/components/ui/Alert'
+import {
+ AlertDialog,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+} from '@/components/ui/AlertDialog'
import { Button } from '@/components/ui/button'
import { Spinner } from '@/components/ui/Spinner'
@@ -21,32 +31,39 @@ export const RemoveSupporterDataButton = ({ supporterId }: { supporterId: number
Exercício do direito de eliminação (LGPD art. 18). Remove o cadastro de apoiador e anonimiza
o contato quando não houver outros vínculos.
- {state.message ? (
-
- {state.message}
-
- ) : null}
-
+
+
+
+ Remover dados
+
+
+
+
+ Remover os dados deste apoiador?
+
+ Esta ação não pode ser desfeita. O cadastro de apoiador será removido e o contato
+ anonimizado quando não houver outros vínculos.
+
+
+ {state.message ? (
+
+ {state.message}
+
+ ) : null}
+
+
+
)
}
diff --git a/src/components/campaign/SupporterForm.tsx b/src/components/campaign/SupporterForm.tsx
index fb16c16d..c6d9b0e3 100644
--- a/src/components/campaign/SupporterForm.tsx
+++ b/src/components/campaign/SupporterForm.tsx
@@ -6,6 +6,7 @@ import { toast } from 'sonner'
import type { SupporterFormState } from '@/app/(campaign)/campanha/(app)/apoiadores/novo/formActions'
import { FormattedInput } from '@/components/FormattedInput'
+import { StrictCombobox } from '@/components/campaign/StrictCombobox'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/Alert'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/Checkbox'
@@ -22,28 +23,21 @@ import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select'
import { Spinner } from '@/components/ui/Spinner'
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/ToggleGroup'
import type { SupporterVoteIntention } from '@/lib/schemas/supporter'
+import { fieldError, errorProps as buildErrorProps } from '@/utilities/campaignFormFields'
import { formatBrazilianPhoneInput, sanitizeBrazilianPhoneInput } from '@/utilities/phone'
import { supporterVoteIntentionLabels } from '@/utilities/supporterUi'
import type { SupporterNucleusOption } from '@/utilities/supporterViewModels'
+import { municipalityComboboxOptions } from '@/utilities/territoryComboboxOptions'
export type SupporterFormAction = (
state: SupporterFormState,
formData: FormData,
) => Promise
-const fieldError = (
+const errorProps = (
fieldErrors: Record | undefined,
field: string,
-): string | undefined => fieldErrors?.[field]?.[0]
-
-const errorProps = (fieldErrors: Record | undefined, field: string) => {
- const error = fieldError(fieldErrors, field)
- return {
- error,
- invalid: Boolean(error),
- describedBy: error ? `supporter-${field}-error` : undefined,
- }
-}
+) => buildErrorProps(fieldErrors, field, 'supporter')
const ConsentBlock = ({
id,
@@ -115,6 +109,7 @@ export const SupporterForm = ({
const [voteIntention, setVoteIntention] = useState(
(state.values?.voteIntention as SupporterVoteIntention | undefined) ?? '',
)
+ const [city, setCity] = useState((state.values?.city as string | undefined) ?? '')
const values = state.values
useEffect(() => {
@@ -204,13 +199,13 @@ export const SupporterForm = ({
Município
-
+
{cityField.error ? (
{cityField.error}
diff --git a/src/components/campaign/SupporterImportWizard.tsx b/src/components/campaign/SupporterImportWizard.tsx
index 1d986574..e265134b 100644
--- a/src/components/campaign/SupporterImportWizard.tsx
+++ b/src/components/campaign/SupporterImportWizard.tsx
@@ -29,7 +29,6 @@ import { Textarea } from '@/components/ui/textarea'
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/ToggleGroup'
import { cn } from '@/lib/utils'
import {
- isSupporterImportOkRow,
type SupporterImportPreviewResult,
type SupporterImportPreviewRow,
} from '@/utilities/supporterImport'
@@ -49,16 +48,8 @@ const steps = ['Upload', 'Conferir prévia', 'Confirmação'] as const
const isPreviewErrorRow = (row: SupporterImportPreviewRow): boolean =>
row.status !== 'ok' && row.status !== 'duplicado_pelo_telefone'
-const downloadErrorReport = (rows: SupporterImportPreviewRow[]) => {
- const errorRows = rows.filter(isPreviewErrorRow)
- const header = 'linha,nome,telefone,municipio,intencao,status\n'
- const body = errorRows
- .map(
- (row) =>
- `${row.line},"${row.nome.replace(/"/g, '""')}","${row.telefone}","${row.municipio}","${row.intencao}",${row.status}`,
- )
- .join('\n')
- const blob = new Blob([header + body], { type: 'text/csv;charset=utf-8' })
+const downloadErrorReport = (errorReportCsv: string) => {
+ const blob = new Blob([errorReportCsv], { type: 'text/csv;charset=utf-8' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
@@ -99,24 +90,19 @@ export const SupporterImportWizard = () => {
}
const visibleRows =
- preview?.rows.filter((row) => (previewFilter === 'errors' ? isPreviewErrorRow(row) : true)) ??
- []
+ preview?.sampleRows.filter((row) =>
+ previewFilter === 'errors' ? isPreviewErrorRow(row) : true,
+ ) ?? []
const confirmImport = () => {
if (!preview) return
setError(undefined)
startTransition(async () => {
try {
- const rows = preview.rows.filter(isSupporterImportOkRow).map((row) => ({
- nome: row.nome,
- telefone: row.normalizedPhone,
- municipio: row.canonicalCity,
- intencao: row.voteIntention,
- }))
const result = await confirmSupporterImport({
operatorAttested: true,
consentNote: consentNote.trim() || undefined,
- rows,
+ importToken: preview.importToken,
})
setConfirmResult({ created: result.created, skipped: result.skipped })
setStep(2)
@@ -210,7 +196,7 @@ export const SupporterImportWizard = () => {
type="button"
variant="outline"
className="min-h-11"
- onClick={() => downloadErrorReport(preview.rows)}
+ onClick={() => preview && downloadErrorReport(preview.errorReportCsv)}
>
Baixar relatório de erros
diff --git a/src/components/campaign/SupporterPagination.tsx b/src/components/campaign/SupporterPagination.tsx
deleted file mode 100644
index fff2d07c..00000000
--- a/src/components/campaign/SupporterPagination.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import {
- Pagination,
- PaginationContent,
- PaginationEllipsis,
- PaginationItem,
- PaginationLink,
- PaginationNext,
- PaginationPrevious,
-} from '@/components/ui/Pagination'
-import { getNucleusPaginationPages } from '@/components/campaign/NucleusPagination'
-import { buildSupporterListHref, type SupporterListState } from '@/utilities/supporterUi'
-
-export const SupporterPagination = ({
- state,
- totalPages,
-}: {
- state: SupporterListState
- totalPages: number
-}) => {
- if (totalPages <= 1) return null
-
- const visiblePages = getNucleusPaginationPages(state.page, totalPages)
-
- return (
-
-
-
-
-
- {visiblePages.map((page, index) => {
- const previousPage = visiblePages[index - 1]
- const hasGap = previousPage !== undefined && page - previousPage > 1
-
- return (
-
- {hasGap ? : null}
-
- {page}
-
-
- )
- })}
-
- = totalPages}
- className={state.page >= totalPages ? 'pointer-events-none opacity-50' : undefined}
- />
-
-
-
- )
-}
diff --git a/src/lib/schemas/supporter.ts b/src/lib/schemas/supporter.ts
index 6661200b..a37728a6 100644
--- a/src/lib/schemas/supporter.ts
+++ b/src/lib/schemas/supporter.ts
@@ -79,17 +79,7 @@ export const supporterImportConfirmSchema = z.object({
error: 'É necessário atestar o consentimento dos apoiadores importados.',
}),
consentNote: trimmedNullableText(2000),
- rows: z
- .array(
- z.object({
- nome: z.string().trim().min(2).max(120),
- telefone: brazilianMobile,
- municipio: optionalBahiaCity,
- intencao: z.enum(supporterVoteIntentions).optional(),
- }),
- )
- .min(1)
- .max(5000),
+ importToken: z.string().min(1, { error: 'Token de importação ausente.' }),
})
export const supporterRemoveSchema = z.object({
diff --git a/src/migrations/20260719_011015_add_supporter_import_batch.json b/src/migrations/20260719_011015_add_supporter_import_batch.json
new file mode 100644
index 00000000..d1a3f0d7
--- /dev/null
+++ b/src/migrations/20260719_011015_add_supporter_import_batch.json
@@ -0,0 +1,9157 @@
+{
+ "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'"
+ },
+ "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_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_texts": {
+ "name": "electoral_nucleus_texts",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "order": {
+ "name": "order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "parent_id": {
+ "name": "parent_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "path": {
+ "name": "path",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "text": {
+ "name": "text",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "electoral_nucleus_texts_order_parent": {
+ "name": "electoral_nucleus_texts_order_parent",
+ "columns": [
+ {
+ "expression": "order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "parent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "electoral_nucleus_texts_text_idx": {
+ "name": "electoral_nucleus_texts_text_idx",
+ "columns": [
+ {
+ "expression": "text",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "electoral_nucleus_texts_parent_fk": {
+ "name": "electoral_nucleus_texts_parent_fk",
+ "tableFrom": "electoral_nucleus_texts",
+ "tableTo": "electoral_nucleus",
+ "columnsFrom": [
+ "parent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "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.supporter": {
+ "name": "supporter",
+ "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": false
+ },
+ "vote_intention": {
+ "name": "vote_intention",
+ "type": "enum_supporter_vote_intention",
+ "typeSchema": "public",
+ "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
+ },
+ "vote_intention_consent_id": {
+ "name": "vote_intention_consent_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vote_intention_consent_content_hash": {
+ "name": "vote_intention_consent_content_hash",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vote_intention_consented_at": {
+ "name": "vote_intention_consented_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source": {
+ "name": "source",
+ "type": "enum_supporter_source",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'manual'"
+ },
+ "consent_note": {
+ "name": "consent_note",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "notes": {
+ "name": "notes",
+ "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": {
+ "supporter_contact_idx": {
+ "name": "supporter_contact_idx",
+ "columns": [
+ {
+ "expression": "contact_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "supporter_nucleus_idx": {
+ "name": "supporter_nucleus_idx",
+ "columns": [
+ {
+ "expression": "nucleus_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "supporter_vote_intention_idx": {
+ "name": "supporter_vote_intention_idx",
+ "columns": [
+ {
+ "expression": "vote_intention",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "supporter_consent_idx": {
+ "name": "supporter_consent_idx",
+ "columns": [
+ {
+ "expression": "consent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "supporter_vote_intention_consent_idx": {
+ "name": "supporter_vote_intention_consent_idx",
+ "columns": [
+ {
+ "expression": "vote_intention_consent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "supporter_source_idx": {
+ "name": "supporter_source_idx",
+ "columns": [
+ {
+ "expression": "source",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "supporter_created_by_idx": {
+ "name": "supporter_created_by_idx",
+ "columns": [
+ {
+ "expression": "created_by_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "supporter_updated_at_idx": {
+ "name": "supporter_updated_at_idx",
+ "columns": [
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "supporter_created_at_idx": {
+ "name": "supporter_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "supporter_contact_id_contact_id_fk": {
+ "name": "supporter_contact_id_contact_id_fk",
+ "tableFrom": "supporter",
+ "tableTo": "contact",
+ "columnsFrom": [
+ "contact_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "supporter_nucleus_id_electoral_nucleus_id_fk": {
+ "name": "supporter_nucleus_id_electoral_nucleus_id_fk",
+ "tableFrom": "supporter",
+ "tableTo": "electoral_nucleus",
+ "columnsFrom": [
+ "nucleus_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "supporter_consent_id_consent_id_fk": {
+ "name": "supporter_consent_id_consent_id_fk",
+ "tableFrom": "supporter",
+ "tableTo": "consent",
+ "columnsFrom": [
+ "consent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "supporter_vote_intention_consent_id_consent_id_fk": {
+ "name": "supporter_vote_intention_consent_id_consent_id_fk",
+ "tableFrom": "supporter",
+ "tableTo": "consent",
+ "columnsFrom": [
+ "vote_intention_consent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "supporter_created_by_id_campaign_user_id_fk": {
+ "name": "supporter_created_by_id_campaign_user_id_fk",
+ "tableFrom": "supporter",
+ "tableTo": "campaign_user",
+ "columnsFrom": [
+ "created_by_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.supporter_import_batch": {
+ "name": "supporter_import_batch",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "batch_id": {
+ "name": "batch_id",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "actor_id": {
+ "name": "actor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "rows": {
+ "name": "rows",
+ "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": {
+ "supporter_import_batch_batch_id_idx": {
+ "name": "supporter_import_batch_batch_id_idx",
+ "columns": [
+ {
+ "expression": "batch_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "supporter_import_batch_actor_idx": {
+ "name": "supporter_import_batch_actor_idx",
+ "columns": [
+ {
+ "expression": "actor_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "supporter_import_batch_expires_at_idx": {
+ "name": "supporter_import_batch_expires_at_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "supporter_import_batch_updated_at_idx": {
+ "name": "supporter_import_batch_updated_at_idx",
+ "columns": [
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "supporter_import_batch_created_at_idx": {
+ "name": "supporter_import_batch_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "supporter_import_batch_actor_id_campaign_user_id_fk": {
+ "name": "supporter_import_batch_actor_id_campaign_user_id_fk",
+ "tableFrom": "supporter_import_batch",
+ "tableTo": "campaign_user",
+ "columnsFrom": [
+ "actor_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.action_plan_tasks": {
+ "name": "action_plan_tasks",
+ "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
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "responsible_id": {
+ "name": "responsible_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "due": {
+ "name": "due",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "done": {
+ "name": "done",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "done_at": {
+ "name": "done_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "action_plan_tasks_order_idx": {
+ "name": "action_plan_tasks_order_idx",
+ "columns": [
+ {
+ "expression": "_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "action_plan_tasks_parent_id_idx": {
+ "name": "action_plan_tasks_parent_id_idx",
+ "columns": [
+ {
+ "expression": "_parent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "action_plan_tasks_responsible_idx": {
+ "name": "action_plan_tasks_responsible_idx",
+ "columns": [
+ {
+ "expression": "responsible_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "action_plan_tasks_responsible_id_contact_id_fk": {
+ "name": "action_plan_tasks_responsible_id_contact_id_fk",
+ "tableFrom": "action_plan_tasks",
+ "tableTo": "contact",
+ "columnsFrom": [
+ "responsible_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "action_plan_tasks_parent_id_fk": {
+ "name": "action_plan_tasks_parent_id_fk",
+ "tableFrom": "action_plan_tasks",
+ "tableTo": "action_plan",
+ "columnsFrom": [
+ "_parent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.action_plan_updates": {
+ "name": "action_plan_updates",
+ "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
+ },
+ "body": {
+ "name": "body",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "author_id": {
+ "name": "author_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "action_plan_updates_order_idx": {
+ "name": "action_plan_updates_order_idx",
+ "columns": [
+ {
+ "expression": "_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "action_plan_updates_parent_id_idx": {
+ "name": "action_plan_updates_parent_id_idx",
+ "columns": [
+ {
+ "expression": "_parent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "action_plan_updates_author_idx": {
+ "name": "action_plan_updates_author_idx",
+ "columns": [
+ {
+ "expression": "author_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "action_plan_updates_author_id_campaign_user_id_fk": {
+ "name": "action_plan_updates_author_id_campaign_user_id_fk",
+ "tableFrom": "action_plan_updates",
+ "tableTo": "campaign_user",
+ "columnsFrom": [
+ "author_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "action_plan_updates_parent_id_fk": {
+ "name": "action_plan_updates_parent_id_fk",
+ "tableFrom": "action_plan_updates",
+ "tableTo": "action_plan",
+ "columnsFrom": [
+ "_parent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.action_plan": {
+ "name": "action_plan",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "kind": {
+ "name": "kind",
+ "type": "enum_action_plan_kind",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "enum_action_plan_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'rascunho'"
+ },
+ "description": {
+ "name": "description",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "start_at": {
+ "name": "start_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "end_at": {
+ "name": "end_at",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "deadline": {
+ "name": "deadline",
+ "type": "timestamp(3) with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "locality": {
+ "name": "locality",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "territory_notes": {
+ "name": "territory_notes",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "responsible_id": {
+ "name": "responsible_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "leadership_id": {
+ "name": "leadership_id",
+ "type": "integer",
+ "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": {
+ "action_plan_title_idx": {
+ "name": "action_plan_title_idx",
+ "columns": [
+ {
+ "expression": "title",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "action_plan_slug_idx": {
+ "name": "action_plan_slug_idx",
+ "columns": [
+ {
+ "expression": "slug",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "action_plan_kind_idx": {
+ "name": "action_plan_kind_idx",
+ "columns": [
+ {
+ "expression": "kind",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "action_plan_status_idx": {
+ "name": "action_plan_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "action_plan_start_at_idx": {
+ "name": "action_plan_start_at_idx",
+ "columns": [
+ {
+ "expression": "start_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "action_plan_responsible_idx": {
+ "name": "action_plan_responsible_idx",
+ "columns": [
+ {
+ "expression": "responsible_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "action_plan_leadership_idx": {
+ "name": "action_plan_leadership_idx",
+ "columns": [
+ {
+ "expression": "leadership_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "action_plan_created_by_idx": {
+ "name": "action_plan_created_by_idx",
+ "columns": [
+ {
+ "expression": "created_by_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "action_plan_updated_at_idx": {
+ "name": "action_plan_updated_at_idx",
+ "columns": [
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "action_plan_created_at_idx": {
+ "name": "action_plan_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "action_plan_responsible_id_contact_id_fk": {
+ "name": "action_plan_responsible_id_contact_id_fk",
+ "tableFrom": "action_plan",
+ "tableTo": "contact",
+ "columnsFrom": [
+ "responsible_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "action_plan_leadership_id_leadership_id_fk": {
+ "name": "action_plan_leadership_id_leadership_id_fk",
+ "tableFrom": "action_plan",
+ "tableTo": "leadership",
+ "columnsFrom": [
+ "leadership_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "action_plan_created_by_id_campaign_user_id_fk": {
+ "name": "action_plan_created_by_id_campaign_user_id_fk",
+ "tableFrom": "action_plan",
+ "tableTo": "campaign_user",
+ "columnsFrom": [
+ "created_by_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.action_plan_texts": {
+ "name": "action_plan_texts",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "order": {
+ "name": "order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "parent_id": {
+ "name": "parent_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "path": {
+ "name": "path",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "text": {
+ "name": "text",
+ "type": "varchar",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "action_plan_texts_order_parent": {
+ "name": "action_plan_texts_order_parent",
+ "columns": [
+ {
+ "expression": "order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "parent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "action_plan_texts_text_idx": {
+ "name": "action_plan_texts_text_idx",
+ "columns": [
+ {
+ "expression": "text",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "action_plan_texts_parent_fk": {
+ "name": "action_plan_texts_parent_fk",
+ "tableFrom": "action_plan_texts",
+ "tableTo": "action_plan",
+ "columnsFrom": [
+ "parent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.action_plan_rels": {
+ "name": "action_plan_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": {
+ "action_plan_rels_order_idx": {
+ "name": "action_plan_rels_order_idx",
+ "columns": [
+ {
+ "expression": "order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "action_plan_rels_parent_idx": {
+ "name": "action_plan_rels_parent_idx",
+ "columns": [
+ {
+ "expression": "parent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "action_plan_rels_path_idx": {
+ "name": "action_plan_rels_path_idx",
+ "columns": [
+ {
+ "expression": "path",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "action_plan_rels_campaign_user_id_idx": {
+ "name": "action_plan_rels_campaign_user_id_idx",
+ "columns": [
+ {
+ "expression": "campaign_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "action_plan_rels_parent_fk": {
+ "name": "action_plan_rels_parent_fk",
+ "tableFrom": "action_plan_rels",
+ "tableTo": "action_plan",
+ "columnsFrom": [
+ "parent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "action_plan_rels_campaign_user_fk": {
+ "name": "action_plan_rels_campaign_user_fk",
+ "tableFrom": "action_plan_rels",
+ "tableTo": "campaign_user",
+ "columnsFrom": [
+ "campaign_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "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
+ },
+ "supporter_id": {
+ "name": "supporter_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "supporter_import_batch_id": {
+ "name": "supporter_import_batch_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "nucleus_update_id": {
+ "name": "nucleus_update_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "action_plan_id": {
+ "name": "action_plan_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_supporter_id_idx": {
+ "name": "payload_locked_documents_rels_supporter_id_idx",
+ "columns": [
+ {
+ "expression": "supporter_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "payload_locked_documents_rels_supporter_import_batch_id_idx": {
+ "name": "payload_locked_documents_rels_supporter_import_batch_id_idx",
+ "columns": [
+ {
+ "expression": "supporter_import_batch_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_action_plan_id_idx": {
+ "name": "payload_locked_documents_rels_action_plan_id_idx",
+ "columns": [
+ {
+ "expression": "action_plan_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_supporter_fk": {
+ "name": "payload_locked_documents_rels_supporter_fk",
+ "tableFrom": "payload_locked_documents_rels",
+ "tableTo": "supporter",
+ "columnsFrom": [
+ "supporter_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "payload_locked_documents_rels_supporter_import_batch_fk": {
+ "name": "payload_locked_documents_rels_supporter_import_batch_fk",
+ "tableFrom": "payload_locked_documents_rels",
+ "tableTo": "supporter_import_batch",
+ "columnsFrom": [
+ "supporter_import_batch_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_action_plan_fk": {
+ "name": "payload_locked_documents_rels_action_plan_fk",
+ "tableFrom": "payload_locked_documents_rels",
+ "tableTo": "action_plan",
+ "columnsFrom": [
+ "action_plan_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_supporter_vote_intention": {
+ "name": "enum_supporter_vote_intention",
+ "schema": "public",
+ "values": [
+ "certo",
+ "tende_a_certo",
+ "indeciso",
+ "outro"
+ ]
+ },
+ "public.enum_supporter_source": {
+ "name": "enum_supporter_source",
+ "schema": "public",
+ "values": [
+ "import_csv",
+ "manual",
+ "convite",
+ "evento"
+ ]
+ },
+ "public.enum_nucleus_update_kind": {
+ "name": "enum_nucleus_update_kind",
+ "schema": "public",
+ "values": [
+ "semanal",
+ "urgente",
+ "nota"
+ ]
+ },
+ "public.enum_action_plan_kind": {
+ "name": "enum_action_plan_kind",
+ "schema": "public",
+ "values": [
+ "caminhada",
+ "comicio",
+ "carreata",
+ "panfletagem",
+ "porta_a_porta",
+ "reuniao_apoio",
+ "lancamento",
+ "convencao",
+ "ato",
+ "entrevista",
+ "producao_conteudo",
+ "digital",
+ "outro"
+ ]
+ },
+ "public.enum_action_plan_status": {
+ "name": "enum_action_plan_status",
+ "schema": "public",
+ "values": [
+ "rascunho",
+ "planejado",
+ "confirmado",
+ "realizado",
+ "cancelado"
+ ]
+ },
+ "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": "d77de497-7374-4455-9fd9-789a471576fc",
+ "prevId": "00000000-0000-0000-0000-000000000000"
+}
\ No newline at end of file
diff --git a/src/migrations/20260719_011015_add_supporter_import_batch.ts b/src/migrations/20260719_011015_add_supporter_import_batch.ts
new file mode 100644
index 00000000..7d033519
--- /dev/null
+++ b/src/migrations/20260719_011015_add_supporter_import_batch.ts
@@ -0,0 +1,77 @@
+import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
+
+/**
+ * Adds the `supporterImportBatch` staging collection (roadmap C6, Phase 5):
+ * short-lived server-side storage for the supporter CSV import wizard so the
+ * full ok-row set never crosses the Server Action boundary twice.
+ *
+ * Hand-trimmed from the Payload-generated diff: the generator also tried to
+ * recreate the `supporter` table, its enums, FKs, indexes, and the
+ * `payload_locked_documents_rels.supporter_id` column — all of which already
+ * exist via `20260718_222656_add_supporter`. Those drift statements were
+ * removed so this migration is idempotent against databases that already ran
+ * the supporter migration.
+ */
+export async function up({ db }: MigrateUpArgs): Promise {
+ await db.execute(sql`
+ CREATE TABLE IF NOT EXISTS "supporter_import_batch" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "batch_id" varchar NOT NULL,
+ "actor_id" integer NOT NULL,
+ "expires_at" timestamp(3) with time zone NOT NULL,
+ "rows" jsonb 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 IF NOT EXISTS "supporter_import_batch_id" integer;
+
+ DO $$
+ BEGIN
+ IF NOT EXISTS (
+ SELECT 1 FROM pg_constraint WHERE conname = 'supporter_import_batch_actor_id_campaign_user_id_fk'
+ ) THEN
+ ALTER TABLE "supporter_import_batch"
+ ADD CONSTRAINT "supporter_import_batch_actor_id_campaign_user_id_fk"
+ FOREIGN KEY ("actor_id") REFERENCES "public"."campaign_user"("id")
+ ON DELETE set null ON UPDATE no action;
+ END IF;
+ END $$;
+
+ CREATE UNIQUE INDEX IF NOT EXISTS "supporter_import_batch_batch_id_idx"
+ ON "supporter_import_batch" USING btree ("batch_id");
+ CREATE INDEX IF NOT EXISTS "supporter_import_batch_actor_idx"
+ ON "supporter_import_batch" USING btree ("actor_id");
+ CREATE INDEX IF NOT EXISTS "supporter_import_batch_expires_at_idx"
+ ON "supporter_import_batch" USING btree ("expires_at");
+ CREATE INDEX IF NOT EXISTS "supporter_import_batch_updated_at_idx"
+ ON "supporter_import_batch" USING btree ("updated_at");
+ CREATE INDEX IF NOT EXISTS "supporter_import_batch_created_at_idx"
+ ON "supporter_import_batch" USING btree ("created_at");
+
+ DO $$
+ BEGIN
+ IF NOT EXISTS (
+ SELECT 1 FROM pg_constraint WHERE conname = 'payload_locked_documents_rels_supporter_import_batch_fk'
+ ) THEN
+ ALTER TABLE "payload_locked_documents_rels"
+ ADD CONSTRAINT "payload_locked_documents_rels_supporter_import_batch_fk"
+ FOREIGN KEY ("supporter_import_batch_id") REFERENCES "public"."supporter_import_batch"("id")
+ ON DELETE cascade ON UPDATE no action;
+ END IF;
+ END $$;
+
+ CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_supporter_import_batch_id_idx"
+ ON "payload_locked_documents_rels" USING btree ("supporter_import_batch_id");
+ `)
+}
+
+export async function down({ db }: MigrateDownArgs): Promise {
+ await db.execute(sql`
+ ALTER TABLE "payload_locked_documents_rels" DROP CONSTRAINT IF EXISTS "payload_locked_documents_rels_supporter_import_batch_fk";
+ DROP INDEX IF EXISTS "payload_locked_documents_rels_supporter_import_batch_id_idx";
+ ALTER TABLE "payload_locked_documents_rels" DROP COLUMN IF EXISTS "supporter_import_batch_id";
+
+ DROP TABLE IF EXISTS "supporter_import_batch" CASCADE;
+ `)
+}
diff --git a/src/migrations/index.ts b/src/migrations/index.ts
index 332db1fa..c1f0f7be 100644
--- a/src/migrations/index.ts
+++ b/src/migrations/index.ts
@@ -8,6 +8,7 @@ import * as migration_20260718_190559_territorio_multi_municipio_bairro from './
import * as migration_20260718_195854_add_election_results from './20260718_195854_add_election_results';
import * as migration_20260718_222656_add_supporter from './20260718_222656_add_supporter';
import * as migration_20260718_222832_add_action_plan from './20260718_222832_add_action_plan';
+import * as migration_20260719_011015_add_supporter_import_batch from './20260719_011015_add_supporter_import_batch';
export const migrations = [
{
@@ -60,4 +61,9 @@ export const migrations = [
down: migration_20260718_222832_add_action_plan.down,
name: '20260718_222832_add_action_plan',
},
+ {
+ up: migration_20260719_011015_add_supporter_import_batch.up,
+ down: migration_20260719_011015_add_supporter_import_batch.down,
+ name: '20260719_011015_add_supporter_import_batch'
+ },
];
diff --git a/src/payload-types.ts b/src/payload-types.ts
index 241e7673..701ed9bf 100644
--- a/src/payload-types.ts
+++ b/src/payload-types.ts
@@ -74,6 +74,7 @@ export interface Config {
electoralNucleus: ElectoralNucleus;
leadership: Leadership;
supporter: Supporter;
+ supporterImportBatch: SupporterImportBatch;
nucleusUpdate: NucleusUpdate;
actionPlan: ActionPlan;
electionTally: ElectionTally;
@@ -101,6 +102,7 @@ export interface Config {
electoralNucleus: ElectoralNucleusSelect | ElectoralNucleusSelect;
leadership: LeadershipSelect | LeadershipSelect;
supporter: SupporterSelect | SupporterSelect;
+ supporterImportBatch: SupporterImportBatchSelect | SupporterImportBatchSelect;
nucleusUpdate: NucleusUpdateSelect | NucleusUpdateSelect;
actionPlan: ActionPlanSelect | ActionPlanSelect;
electionTally: ElectionTallySelect | ElectionTallySelect;
@@ -475,6 +477,27 @@ export interface Supporter {
updatedAt: string;
createdAt: string;
}
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "supporterImportBatch".
+ */
+export interface SupporterImportBatch {
+ id: number;
+ batchId: string;
+ actor: number | CampaignUser;
+ expiresAt: string;
+ rows:
+ | {
+ [k: string]: unknown;
+ }
+ | unknown[]
+ | string
+ | number
+ | boolean
+ | null;
+ updatedAt: string;
+ createdAt: string;
+}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "nucleusUpdate".
@@ -909,6 +932,10 @@ export interface PayloadLockedDocument {
relationTo: 'supporter';
value: number | Supporter;
} | null)
+ | ({
+ relationTo: 'supporterImportBatch';
+ value: number | SupporterImportBatch;
+ } | null)
| ({
relationTo: 'nucleusUpdate';
value: number | NucleusUpdate;
@@ -1186,6 +1213,18 @@ export interface SupporterSelect {
updatedAt?: T;
createdAt?: T;
}
+/**
+ * This interface was referenced by `Config`'s JSON-Schema
+ * via the `definition` "supporterImportBatch_select".
+ */
+export interface SupporterImportBatchSelect {
+ batchId?: T;
+ actor?: T;
+ expiresAt?: T;
+ rows?: T;
+ updatedAt?: T;
+ createdAt?: T;
+}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "nucleusUpdate_select".
diff --git a/src/payload.config.ts b/src/payload.config.ts
index 202cdcec..0cc1f16c 100644
--- a/src/payload.config.ts
+++ b/src/payload.config.ts
@@ -24,6 +24,7 @@ import { Post } from './collections/Post'
import { Signature } from './collections/Signature'
import { Subscription } from './collections/Subscription'
import { Supporter } from './collections/Supporter'
+import { SupporterImportBatch } from './collections/SupporterImportBatch'
import { Tag } from './collections/Tag'
import { Users } from './collections/Users'
import { HomePage } from './globals/HomePage'
@@ -51,6 +52,7 @@ export default buildConfig({
ElectoralNucleus,
Leadership,
Supporter,
+ SupporterImportBatch,
NucleusUpdate,
ActionPlan,
ElectionTally,
diff --git a/src/utilities/actionPlanUi.ts b/src/utilities/actionPlanUi.ts
index 1c66afd5..081436b6 100644
--- a/src/utilities/actionPlanUi.ts
+++ b/src/utilities/actionPlanUi.ts
@@ -7,6 +7,14 @@ import {
territoryForCity,
} from '@/lib/bahiaTerritories'
import { actionPlanKinds, actionPlanStatuses } from '@/lib/schemas/actionPlan'
+import {
+ buildListHref,
+ firstValue,
+ normalizedText,
+ resolveListUrl,
+ strictDecimalInteger,
+ type RawSearchParams as CampaignListRawSearchParams,
+} from '@/utilities/campaignListUrl'
import { normalizeSearchPhrase } from '@/utilities/wordStartFilter'
export const actionPlanPageSize = 25
@@ -33,27 +41,12 @@ export type ActionPlanListState = {
city?: string
}
-type RawSearchParams = Record
+type RawSearchParams = CampaignListRawSearchParams
export const actionPlanListParamNames = ['tab', 'kind', 'status', 'region', 'city', 'page'] as const
const actionPlanListParamNameSet = new Set(actionPlanListParamNames)
-const firstValue = (value: string | string[] | undefined): string | undefined =>
- Array.isArray(value) ? value[0] : value
-
-const normalizedText = (value: string | undefined): string | undefined => {
- if (typeof value !== 'string') return undefined
- const normalized = value.trim()
- return normalized || undefined
-}
-
-const strictDecimalInteger = (value: string | undefined): number | undefined => {
- if (!value || !/^[1-9]\d*$/.test(value)) return undefined
- const number = Number(value)
- return Number.isSafeInteger(number) ? number : undefined
-}
-
const canonicalTerritoryBySearchValue = new Map(
bahiaIdentityTerritories.map((territory) => [normalizeSearchPhrase(territory), territory]),
)
@@ -155,31 +148,8 @@ export const buildActionPlanListSearchParams = (
export const buildActionPlanFiltersKey = (state: ActionPlanListState): string =>
buildActionPlanListSearchParams(state).toString()
-export const buildActionPlanListHref = (state: ActionPlanListState, page: number): string => {
- const params = buildActionPlanListSearchParams(state, page)
- const query = params.toString()
- return query ? `/campanha/planos?${query}` : '/campanha/planos'
-}
-
-const inspectRawActionPlanListParams = (
- params: RawSearchParams,
-): { hasUnsupportedParams: boolean; query: string } => {
- const serialized = new URLSearchParams()
- let hasUnsupportedParams = false
-
- for (const [name, value] of Object.entries(params)) {
- if (!actionPlanListParamNameSet.has(name)) {
- hasUnsupportedParams = true
- continue
- }
- if (value === undefined) continue
- for (const item of Array.isArray(value) ? value : [value]) {
- serialized.append(name, item)
- }
- }
-
- return { hasUnsupportedParams, query: serialized.toString() }
-}
+export const buildActionPlanListHref = (state: ActionPlanListState, page: number): string =>
+ buildListHref(state, buildActionPlanListSearchParams, '/campanha/planos', page)
export const resolveActionPlanListUrl = (
params: RawSearchParams,
@@ -188,22 +158,12 @@ export const resolveActionPlanListUrl = (
state: ActionPlanListState
href: string
redirectHref?: string
-} => {
- const parsedState = parseActionPlanListParams(params)
- const page =
- totalPages !== undefined && totalPages > 0 && parsedState.page > totalPages
- ? totalPages
- : parsedState.page
- const state = page === parsedState.page ? parsedState : { ...parsedState, page }
- const canonicalParams = buildActionPlanListSearchParams(state)
- const canonicalQuery = canonicalParams.toString()
- const href = canonicalQuery ? `/campanha/planos?${canonicalQuery}` : '/campanha/planos'
- const raw = inspectRawActionPlanListParams(params)
- const needsRedirect = raw.hasUnsupportedParams || raw.query !== canonicalQuery
-
- return {
- state,
- href,
- ...(needsRedirect ? { redirectHref: href } : {}),
- }
-}
+} =>
+ resolveListUrl({
+ params,
+ paramNameSet: actionPlanListParamNameSet,
+ parse: parseActionPlanListParams,
+ buildSearchParams: buildActionPlanListSearchParams,
+ basePath: '/campanha/planos',
+ totalPages,
+ })
diff --git a/src/utilities/campaignFormActionError.ts b/src/utilities/campaignFormActionError.ts
new file mode 100644
index 00000000..7d6ec28d
--- /dev/null
+++ b/src/utilities/campaignFormActionError.ts
@@ -0,0 +1,40 @@
+import { ZodError } from 'zod'
+
+import { FormDataBoundaryError, validationFieldErrors } from '@/lib/formData'
+
+export type CampaignFormErrorState = {
+ fieldErrors?: Record
+ message?: string
+ values?: Values
+ revision?: number
+}
+
+export type MapCampaignFormActionErrorOptions = {
+ error: unknown
+ safeMessages: readonly string[]
+ genericMessage: string
+ values?: Values
+ revision?: number
+ resolveBoundaryMessage?: (error: FormDataBoundaryError) => string
+}
+
+export const mapCampaignFormActionError = (
+ options: MapCampaignFormActionErrorOptions,
+): CampaignFormErrorState => {
+ const { error, safeMessages, genericMessage, values, revision, resolveBoundaryMessage } = options
+
+ if (error instanceof FormDataBoundaryError) {
+ const message = resolveBoundaryMessage ? resolveBoundaryMessage(error) : error.message
+ return { fieldErrors: { [error.field]: [message] }, values, revision }
+ }
+ if (error instanceof ZodError) {
+ return { fieldErrors: validationFieldErrors(error), values, revision }
+ }
+ if (
+ error instanceof Error &&
+ safeMessages.includes(error.message as (typeof safeMessages)[number])
+ ) {
+ return { message: error.message, values, revision }
+ }
+ return { message: genericMessage, values, revision }
+}
diff --git a/src/utilities/campaignFormFields.ts b/src/utilities/campaignFormFields.ts
new file mode 100644
index 00000000..81b291c4
--- /dev/null
+++ b/src/utilities/campaignFormFields.ts
@@ -0,0 +1,21 @@
+export const fieldError = (
+ fieldErrors: Record | undefined,
+ field: string,
+): string | undefined => fieldErrors?.[field]?.[0]
+
+export const errorProps = (
+ fieldErrors: Record | undefined,
+ field: string,
+ idPrefix: string,
+): {
+ error: string | undefined
+ invalid: boolean
+ describedBy: string | undefined
+} => {
+ const error = fieldError(fieldErrors, field)
+ return {
+ error,
+ invalid: Boolean(error),
+ describedBy: error ? `${idPrefix}-${field}-error` : undefined,
+ }
+}
diff --git a/src/utilities/campaignListUrl.ts b/src/utilities/campaignListUrl.ts
new file mode 100644
index 00000000..bfb09b9f
--- /dev/null
+++ b/src/utilities/campaignListUrl.ts
@@ -0,0 +1,86 @@
+export type RawSearchParams = Record
+
+export const firstValue = (value: string | string[] | undefined): string | undefined =>
+ Array.isArray(value) ? value[0] : value
+
+export const normalizedText = (value: string | undefined): string | undefined => {
+ if (typeof value !== 'string') return undefined
+ const normalized = value.trim()
+ return normalized || undefined
+}
+
+export const strictDecimalInteger = (value: string | undefined): number | undefined => {
+ if (!value || !/^[1-9]\d*$/.test(value)) return undefined
+ const number = Number(value)
+ return Number.isSafeInteger(number) ? number : undefined
+}
+
+export const inspectRawListParams = (
+ params: RawSearchParams,
+ paramNameSet: ReadonlySet,
+): { hasUnsupportedParams: boolean; query: string } => {
+ const serialized = new URLSearchParams()
+ let hasUnsupportedParams = false
+
+ for (const [name, value] of Object.entries(params)) {
+ if (!paramNameSet.has(name)) {
+ hasUnsupportedParams = true
+ continue
+ }
+ if (value === undefined) continue
+ for (const item of Array.isArray(value) ? value : [value]) {
+ serialized.append(name, item)
+ }
+ }
+
+ return { hasUnsupportedParams, query: serialized.toString() }
+}
+
+export type ListStateWithPage = { page: number }
+
+export type ResolveListUrlOptions = {
+ params: RawSearchParams
+ paramNameSet: ReadonlySet
+ parse: (params: RawSearchParams) => State
+ buildSearchParams: (state: State) => URLSearchParams
+ basePath: string
+ totalPages?: number
+}
+
+export const resolveListUrl = (
+ options: ResolveListUrlOptions,
+): {
+ state: State
+ href: string
+ redirectHref?: string
+} => {
+ const { params, paramNameSet, parse, buildSearchParams, basePath, totalPages } = options
+ const parsedState = parse(params)
+ const page =
+ totalPages !== undefined && totalPages > 0 && parsedState.page > totalPages
+ ? totalPages
+ : parsedState.page
+ const state = page === parsedState.page ? parsedState : ({ ...parsedState, page } as State)
+ const canonicalParams = buildSearchParams(state)
+ const canonicalQuery = canonicalParams.toString()
+ const href = canonicalQuery ? `${basePath}?${canonicalQuery}` : basePath
+ const raw = inspectRawListParams(params, paramNameSet)
+ const needsRedirect = raw.hasUnsupportedParams || raw.query !== canonicalQuery
+
+ return {
+ state,
+ href,
+ ...(needsRedirect ? { redirectHref: href } : {}),
+ }
+}
+
+export const buildListHref = (
+ state: State,
+ buildSearchParams: (state: State, page?: number) => URLSearchParams,
+ basePath: string,
+ page: number,
+): string => {
+ const params = buildSearchParams(state, page)
+ const query = params.toString()
+ return query ? `${basePath}?${query}` : basePath
+}
diff --git a/src/utilities/nucleusUi.ts b/src/utilities/nucleusUi.ts
index cf3e376b..e75d9f6b 100644
--- a/src/utilities/nucleusUi.ts
+++ b/src/utilities/nucleusUi.ts
@@ -7,6 +7,14 @@ import {
territoryForCity,
} from '@/lib/bahiaTerritories'
import type { CampaignUser, ElectoralNucleus } from '@/payload-types'
+import {
+ buildListHref,
+ firstValue,
+ normalizedText,
+ resolveListUrl,
+ strictDecimalInteger,
+ type RawSearchParams as CampaignListRawSearchParams,
+} from '@/utilities/campaignListUrl'
import { normalizeSearchPhrase } from '@/utilities/wordStartFilter'
export const nucleusPageSize = 25
@@ -63,7 +71,7 @@ export type NucleusListState = {
estimate?: 'confirmada' | 'sem_confirmacao'
}
-type RawSearchParams = Record
+type RawSearchParams = CampaignListRawSearchParams
export const nucleusListParamNames = [
'q',
@@ -77,21 +85,6 @@ export const nucleusListParamNames = [
const nucleusListParamNameSet = new Set(nucleusListParamNames)
-const firstValue = (value: string | string[] | undefined): string | undefined =>
- Array.isArray(value) ? value[0] : value
-
-const normalizedText = (value: FormDataEntryValue | string | undefined): string | undefined => {
- if (typeof value !== 'string') return undefined
- const normalized = value.trim()
- return normalized || undefined
-}
-
-const strictDecimalInteger = (value: string | undefined): number | undefined => {
- if (!value || !/^[1-9]\d*$/.test(value)) return undefined
- const number = Number(value)
- return Number.isSafeInteger(number) ? number : undefined
-}
-
const canonicalTerritoryBySearchValue = new Map(
bahiaIdentityTerritories.map((territory) => [normalizeSearchPhrase(territory), territory]),
)
@@ -198,31 +191,8 @@ export const buildNucleusListSearchParams = (
export const buildNucleusFiltersKey = (state: NucleusListState): string =>
buildNucleusListSearchParams(state).toString()
-export const buildNucleusListHref = (state: NucleusListState, page: number): string => {
- const params = buildNucleusListSearchParams(state, page)
- const query = params.toString()
- return query ? `/campanha/nucleos?${query}` : '/campanha/nucleos'
-}
-
-const inspectRawNucleusListParams = (
- params: RawSearchParams,
-): { hasUnsupportedParams: boolean; query: string } => {
- const serialized = new URLSearchParams()
- let hasUnsupportedParams = false
-
- for (const [name, value] of Object.entries(params)) {
- if (!nucleusListParamNameSet.has(name)) {
- hasUnsupportedParams = true
- continue
- }
- if (value === undefined) continue
- for (const item of Array.isArray(value) ? value : [value]) {
- serialized.append(name, item)
- }
- }
-
- return { hasUnsupportedParams, query: serialized.toString() }
-}
+export const buildNucleusListHref = (state: NucleusListState, page: number): string =>
+ buildListHref(state, buildNucleusListSearchParams, '/campanha/nucleos', page)
export const resolveNucleusListUrl = (
params: RawSearchParams,
@@ -231,25 +201,15 @@ export const resolveNucleusListUrl = (
state: NucleusListState
href: string
redirectHref?: string
-} => {
- const parsedState = parseNucleusListParams(params)
- const page =
- totalPages !== undefined && totalPages > 0 && parsedState.page > totalPages
- ? totalPages
- : parsedState.page
- const state = page === parsedState.page ? parsedState : { ...parsedState, page }
- const canonicalParams = buildNucleusListSearchParams(state)
- const canonicalQuery = canonicalParams.toString()
- const href = canonicalQuery ? `/campanha/nucleos?${canonicalQuery}` : '/campanha/nucleos'
- const raw = inspectRawNucleusListParams(params)
- const needsRedirect = raw.hasUnsupportedParams || raw.query !== canonicalQuery
-
- return {
- state,
- href,
- ...(needsRedirect ? { redirectHref: href } : {}),
- }
-}
+} =>
+ resolveListUrl({
+ params,
+ paramNameSet: nucleusListParamNameSet,
+ parse: parseNucleusListParams,
+ buildSearchParams: buildNucleusListSearchParams,
+ basePath: '/campanha/nucleos',
+ totalPages,
+ })
export const getCampaignScopeLabel = (role: CampaignUser['role'], nucleusCount: number): string => {
if (role === 'coordenador') {
diff --git a/src/utilities/supporterImport.ts b/src/utilities/supporterImport.ts
index 3fcba4f4..df25283e 100644
--- a/src/utilities/supporterImport.ts
+++ b/src/utilities/supporterImport.ts
@@ -33,12 +33,17 @@ export type SupporterImportPreviewRow =
})
export type SupporterImportPreviewResult = {
- rows: SupporterImportPreviewRow[]
counts: {
ok: number
duplicate: number
error: number
}
+ /** First N rows for the on-screen preview table (the full set stays server-side). */
+ sampleRows: SupporterImportPreviewRow[]
+ /** Precomputed CSV of every error row, for the "Baixar relatório de erros" button. */
+ errorReportCsv: string
+ /** HMAC-signed token that `confirmSupporterImport` exchanges for the staged batch. */
+ importToken: string
}
export const isSupporterImportOkRow = (
diff --git a/src/utilities/supporterImportBulk.ts b/src/utilities/supporterImportBulk.ts
new file mode 100644
index 00000000..75b37079
--- /dev/null
+++ b/src/utilities/supporterImportBulk.ts
@@ -0,0 +1,256 @@
+import 'server-only'
+
+import type { Payload } from 'payload'
+
+import type { SupporterVoteIntention } from '@/lib/schemas/supporter'
+import type { ConsentDescriptor } from '@/utilities/campaignConsent'
+import type { PayloadTransactionRequest } from '@/utilities/payloadTransaction'
+import { getPostgresTransactionDatabase } from '@/utilities/postgresTransactionLocks'
+
+export type SupporterImportBulkRow = {
+ telefone: string
+ nome: string
+ municipio?: string
+ intencao?: SupporterVoteIntention
+}
+
+export type SupporterImportBulkResult = {
+ created: number
+ skipped: number
+ errors: Array<{ telefone: string; message: string }>
+}
+
+type DrizzleInsertResult = { rowCount?: number | null }
+
+type DrizzleTx = {
+ insert: (table: unknown) => {
+ values: (rows: Record[]) => {
+ onConflictDoNothing: () => Promise
+ }
+ }
+}
+
+type DrizzleTables = Record>
+
+type PayloadDb = {
+ drizzle: unknown
+ tables: DrizzleTables
+}
+
+const CONTACT_TABLE = 'contact'
+const SUPPORTER_TABLE = 'supporter'
+
+/** ~16 columns × 500 ≈ 8000 bind params — well under PG 65535. */
+const INSERT_CHUNK_SIZE = 500
+
+const chunk = (rows: readonly T[], size: number): T[][] => {
+ const out: T[][] = []
+ for (let i = 0; i < rows.length; i += size) out.push(rows.slice(i, i + size) as T[])
+ return out
+}
+
+const getTables = (payload: Payload): DrizzleTables =>
+ (payload.db as unknown as PayloadDb).tables
+
+const requireTable = (tables: DrizzleTables, name: string) => {
+ const table = tables[name]
+ if (!table) throw new Error(`Tabela drizzle ausente: ${name}. Rode as migrations.`)
+ return table
+}
+
+const buildContactRows = (
+ rows: SupporterImportBulkRow[],
+ now: string,
+): Array> =>
+ rows.map((row) => ({
+ name: row.nome,
+ phone: row.telefone,
+ email: null,
+ state: 'BA',
+ city: row.municipio ?? null,
+ postalCode: null,
+ gender: null,
+ createdAt: now,
+ updatedAt: now,
+ }))
+
+const buildSupporterRows = (
+ entries: Array<{
+ row: SupporterImportBulkRow
+ contactID: number
+ }>,
+ args: {
+ actorID: number
+ registrationConsent: ConsentDescriptor
+ voteIntentionConsent: ConsentDescriptor
+ consentNote: string
+ now: string
+ },
+): Array> =>
+ entries.map(({ row, contactID }) => ({
+ // Drizzle column names for Payload relationship fields omit the `Id` suffix.
+ contact: contactID,
+ nucleus: null,
+ voteIntention: row.intencao ?? null,
+ consent: args.registrationConsent.id,
+ consentContentHash: args.registrationConsent.contentHash,
+ consentedAt: args.now,
+ voteIntentionConsent: row.intencao ? args.voteIntentionConsent.id : null,
+ voteIntentionConsentContentHash: row.intencao
+ ? args.voteIntentionConsent.contentHash
+ : null,
+ voteIntentionConsentedAt: row.intencao ? args.now : null,
+ source: 'import_csv',
+ consentNote: args.consentNote,
+ notes: null,
+ createdBy: args.actorID,
+ createdAt: args.now,
+ updatedAt: args.now,
+ }))
+
+/**
+ * Bulk-insert contacts and nucleus-less supporters for a CSV import inside the
+ * caller's Payload transaction. Phone advisory locks MUST already be held for
+ * every phone in `rows` (the caller acquires them before invoking this helper).
+ *
+ * Contacts are inserted via drizzle (bypassing the Contact phone-invariant hook,
+ * which is safe because the locks guarantee uniqueness within the txn). The
+ * supporter insert uses `ON CONFLICT DO NOTHING` on the unique
+ * `(contact_id, nucleus_id)` index as a last-resort guard against races.
+ */
+export const bulkInsertSupporterImport = async (args: {
+ payload: Payload
+ req: PayloadTransactionRequest
+ actorID: number
+ rows: SupporterImportBulkRow[]
+ registrationConsent: ConsentDescriptor
+ voteIntentionConsent: ConsentDescriptor
+ consentNote: string
+}): Promise => {
+ const { payload, req, actorID, rows } = args
+ if (rows.length === 0) {
+ return { created: 0, skipped: 0, errors: [] }
+ }
+
+ const tables = getTables(payload)
+ const contactTable = requireTable(tables, CONTACT_TABLE)
+ const supporterTable = requireTable(tables, SUPPORTER_TABLE)
+ const database = await getPostgresTransactionDatabase(payload, req)
+ const tx = database as unknown as DrizzleTx
+ const now = new Date().toISOString()
+
+ const phones = [...new Set(rows.map((row) => row.telefone))]
+
+ const existingContacts = await payload.find({
+ collection: 'contact',
+ where: { phone: { in: phones } },
+ depth: 0,
+ limit: phones.length,
+ pagination: false,
+ select: { phone: true },
+ overrideAccess: true,
+ req,
+ })
+ const contactIdByPhone = new Map(
+ existingContacts.docs.map((doc) => [doc.phone, doc.id] as const),
+ )
+
+ // New contacts to create, deduped by phone (preview already dedupes, but this
+ // is defensive against a stale confirm payload).
+ const newContactByPhone = new Map()
+ for (const row of rows) {
+ if (contactIdByPhone.has(row.telefone)) continue
+ if (newContactByPhone.has(row.telefone)) continue
+ newContactByPhone.set(row.telefone, row)
+ }
+
+ if (newContactByPhone.size > 0) {
+ const newRows = [...newContactByPhone.values()]
+ for (const batch of chunk(newRows, INSERT_CHUNK_SIZE)) {
+ await tx.insert(contactTable).values(buildContactRows(batch, now))
+ }
+
+ const newPhones = [...newContactByPhone.keys()]
+ const createdContacts = await payload.find({
+ collection: 'contact',
+ where: { phone: { in: newPhones } },
+ depth: 0,
+ limit: newPhones.length,
+ pagination: false,
+ select: { phone: true },
+ overrideAccess: true,
+ req,
+ })
+ for (const doc of createdContacts.docs) {
+ contactIdByPhone.set(doc.phone, doc.id)
+ }
+ }
+
+ const contactIDs = [...new Set(contactIdByPhone.values())]
+ const existingSupporters =
+ contactIDs.length > 0
+ ? await payload.find({
+ collection: 'supporter',
+ where: {
+ and: [{ contact: { in: contactIDs } }, { nucleus: { exists: false } }],
+ },
+ depth: 0,
+ limit: contactIDs.length,
+ pagination: false,
+ select: { contact: true },
+ overrideAccess: true,
+ req,
+ })
+ : null
+ const supporterContactIDs = new Set(
+ (existingSupporters?.docs ?? [])
+ .map((doc) =>
+ typeof doc.contact === 'number'
+ ? doc.contact
+ : typeof doc.contact === 'object' && doc.contact !== null
+ ? doc.contact.id
+ : null,
+ )
+ .filter((id): id is number => id !== null),
+ )
+
+ const candidateEntries: Array<{ row: SupporterImportBulkRow; contactID: number }> = []
+ const errors: Array<{ telefone: string; message: string }> = []
+ let skipped = 0
+
+ for (const row of rows) {
+ const contactID = contactIdByPhone.get(row.telefone)
+ if (!contactID) {
+ skipped += 1
+ errors.push({ telefone: row.telefone, message: 'Contato não encontrado após upsert.' })
+ continue
+ }
+ if (supporterContactIDs.has(contactID)) {
+ skipped += 1
+ continue
+ }
+ candidateEntries.push({ row, contactID })
+ // Reserve against duplicate phones within the same payload.
+ supporterContactIDs.add(contactID)
+ }
+
+ let created = 0
+ for (const batch of chunk(candidateEntries, INSERT_CHUNK_SIZE)) {
+ if (batch.length === 0) continue
+ const result = await tx
+ .insert(supporterTable)
+ .values(
+ buildSupporterRows(batch, {
+ actorID,
+ registrationConsent: args.registrationConsent,
+ voteIntentionConsent: args.voteIntentionConsent,
+ consentNote: args.consentNote,
+ now,
+ }),
+ )
+ .onConflictDoNothing()
+ created += Number(result.rowCount ?? batch.length)
+ }
+
+ return { created, skipped, errors }
+}
diff --git a/src/utilities/supporterImportToken.ts b/src/utilities/supporterImportToken.ts
new file mode 100644
index 00000000..fbb8a5c8
--- /dev/null
+++ b/src/utilities/supporterImportToken.ts
@@ -0,0 +1,165 @@
+import 'server-only'
+
+import { createHmac, randomUUID, timingSafeEqual } from 'node:crypto'
+
+import type { Payload } from 'payload'
+
+import type { SupporterVoteIntention } from '@/lib/schemas/supporter'
+import type { PayloadTransactionRequest } from '@/utilities/payloadTransaction'
+
+export type SupporterImportOkRow = {
+ nome: string
+ telefone: string
+ municipio?: string
+ intencao?: SupporterVoteIntention
+}
+
+export type SupporterImportBatch = {
+ actorID: number
+ expiresAt: number
+ okRows: SupporterImportOkRow[]
+}
+
+const BATCH_COLLECTION = 'supporterImportBatch'
+/** Short-lived server-side staging; the wizard must confirm within this window. */
+const IMPORT_TOKEN_TTL_MS = 10 * 60 * 1000
+
+export const storeSupporterImportBatch = async (
+ payload: Payload,
+ batchId: string,
+ batch: SupporterImportBatch,
+ req?: PayloadTransactionRequest,
+): Promise => {
+ await payload.create({
+ collection: BATCH_COLLECTION,
+ data: {
+ batchId,
+ actor: batch.actorID,
+ expiresAt: new Date(batch.expiresAt).toISOString(),
+ rows: batch.okRows,
+ },
+ depth: 0,
+ overrideAccess: true,
+ req,
+ })
+}
+
+export const loadSupporterImportBatch = async (
+ payload: Payload,
+ batchId: string,
+ expectedActorID: number,
+ req?: PayloadTransactionRequest,
+): Promise => {
+ const result = await payload.find({
+ collection: BATCH_COLLECTION,
+ where: { batchId: { equals: batchId } },
+ depth: 0,
+ limit: 1,
+ pagination: false,
+ overrideAccess: true,
+ req,
+ })
+ const doc = result.docs[0] as
+ | { id: number; actor: unknown; expiresAt?: string; rows?: unknown }
+ | undefined
+ if (!doc) throw new Error('Lote de importação não encontrado ou expirado.')
+
+ const actorID =
+ typeof doc.actor === 'number'
+ ? doc.actor
+ : typeof doc.actor === 'object' && doc.actor !== null && 'id' in doc.actor
+ ? (doc.actor as { id: number }).id
+ : null
+ if (actorID !== expectedActorID) {
+ throw new Error('Lote de importação inválido.')
+ }
+
+ const expiresAtMs = doc.expiresAt ? Date.parse(doc.expiresAt) : NaN
+ if (!Number.isFinite(expiresAtMs) || expiresAtMs <= Date.now()) {
+ throw new Error('Lote de importação expirado.')
+ }
+ if (!Array.isArray(doc.rows)) {
+ throw new Error('Lote de importação inválido.')
+ }
+
+ return {
+ actorID: expectedActorID,
+ expiresAt: expiresAtMs,
+ okRows: doc.rows as SupporterImportOkRow[],
+ }
+}
+
+export const deleteSupporterImportBatch = async (
+ payload: Payload,
+ batchId: string,
+ req?: PayloadTransactionRequest,
+): Promise => {
+ const result = await payload.find({
+ collection: BATCH_COLLECTION,
+ where: { batchId: { equals: batchId } },
+ depth: 0,
+ limit: 1,
+ pagination: false,
+ overrideAccess: true,
+ req,
+ })
+ const doc = result.docs[0] as { id: number } | undefined
+ if (!doc) return
+ await payload.delete({
+ collection: BATCH_COLLECTION,
+ id: doc.id,
+ depth: 0,
+ overrideAccess: true,
+ req,
+ })
+}
+
+const base64url = (buffer: Buffer): string =>
+ buffer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
+
+const base64urlToBuffer = (value: string): Buffer => {
+ const padded = value.replace(/-/g, '+').replace(/_/g, '/')
+ return Buffer.from(padded, 'base64')
+}
+
+const getSecret = (): string => {
+ const secret = process.env.PAYLOAD_SECRET
+ if (!secret) throw new Error('PAYLOAD_SECRET não configurado para o token de importação.')
+ return secret
+}
+
+const signature = (batchId: string, actorID: number, expiresAt: number): Buffer =>
+ createHmac('sha256', getSecret()).update(`${batchId}.${actorID}.${expiresAt}`).digest()
+
+export type SignedImportToken = {
+ token: string
+ batchId: string
+ expiresAt: number
+}
+
+export const issueSupporterImportToken = (actorID: number): SignedImportToken => {
+ const batchId = randomUUID()
+ const expiresAt = Date.now() + IMPORT_TOKEN_TTL_MS
+ const sig = base64url(signature(batchId, actorID, expiresAt))
+ return { token: `${batchId}.${expiresAt}.${sig}`, batchId, expiresAt }
+}
+
+export const verifySupporterImportToken = (
+ token: string,
+ actorID: number,
+): { batchId: string; expiresAt: number } => {
+ const parts = token.split('.')
+ if (parts.length !== 3) throw new Error('Token de importação inválido.')
+ const [batchId, expiresAtText, sig] = parts as [string, string, string]
+ const expiresAt = Number(expiresAtText)
+ if (!Number.isSafeInteger(expiresAt) || expiresAt <= Date.now()) {
+ throw new Error('Token de importação expirado.')
+ }
+ const expected = signature(batchId, actorID, expiresAt)
+ const provided = base64urlToBuffer(sig)
+ if (expected.length !== provided.length || !timingSafeEqual(expected, provided)) {
+ throw new Error('Token de importação inválido.')
+ }
+ return { batchId, expiresAt }
+}
+
diff --git a/src/utilities/supporterListOverviewAggregate.ts b/src/utilities/supporterListOverviewAggregate.ts
new file mode 100644
index 00000000..6550546c
--- /dev/null
+++ b/src/utilities/supporterListOverviewAggregate.ts
@@ -0,0 +1,149 @@
+import 'server-only'
+
+import { sql } from '@payloadcms/db-postgres'
+import type { Payload } from 'payload'
+
+import type { CampaignUser } from '@/payload-types'
+import { isCampaignGeneral } from '@/utilities/campaignAccess'
+import { normalizeBrazilianPhone } from '@/utilities/phone'
+import type { SupporterListState } from '@/utilities/supporterUi'
+import type { SupporterListOverviewViewModel } from '@/utilities/supporterViewModels'
+
+type AccessConstraint =
+ | { kind: 'all' }
+ | { kind: 'none' }
+ | { kind: 'nucleusSet'; ids: number[] }
+
+type PostgresDb = {
+ execute: (query: ReturnType) => Promise
+}
+
+const resultRows = (result: unknown): Array> => {
+ if (Array.isArray(result)) return result as Array>
+ if (
+ typeof result === 'object' &&
+ result !== null &&
+ 'rows' in result &&
+ Array.isArray((result as { rows: unknown }).rows)
+ ) {
+ return (result as { rows: Array> }).rows
+ }
+ return []
+}
+
+const resolveAccessConstraint = async (
+ payload: Pick,
+ user: CampaignUser,
+): Promise => {
+ if (isCampaignGeneral(user)) return { kind: 'all' }
+ if (user.role !== 'coordenador') return { kind: 'none' }
+
+ const result = await payload.find({
+ collection: 'electoralNucleus',
+ where: { coordinators: { contains: user.id } },
+ depth: 0,
+ pagination: false,
+ overrideAccess: true,
+ })
+
+ const ids = result.docs
+ .map((doc) => (typeof doc.id === 'number' ? doc.id : Number(doc.id)))
+ .filter((id) => Number.isSafeInteger(id))
+
+ return { kind: 'nucleusSet', ids }
+}
+
+const buildAggregateSql = (
+ state: SupporterListState,
+ access: AccessConstraint,
+): ReturnType => {
+ const conditions: ReturnType[] = []
+ const needsContactJoin = Boolean(state.q || state.city)
+
+ if (access.kind === 'nucleusSet') {
+ if (access.ids.length === 0) {
+ // No accessible nuclei → no rows. Keep a trivially-false condition.
+ conditions.push(sql`FALSE`)
+ } else {
+ conditions.push(
+ sql`"supporter"."nucleus_id" IN (${sql.join(
+ access.ids.map((id) => sql`${id}`),
+ sql`, `,
+ )})`,
+ )
+ }
+ }
+
+ if (state.voteIntention) {
+ conditions.push(sql`"supporter"."vote_intention" = ${state.voteIntention}`)
+ }
+
+ if (state.nucleus) {
+ conditions.push(sql`"supporter"."nucleus_id" = ${state.nucleus}`)
+ }
+
+ if (state.city) {
+ conditions.push(sql`"contact"."city" = ${state.city}`)
+ }
+
+ if (state.q) {
+ const searchTerms: ReturnType[] = [
+ sql`"contact"."name" ILIKE ${`%${state.q}%`}`,
+ sql`"contact"."city" ILIKE ${`%${state.q}%`}`,
+ ]
+ const normalizedPhone = normalizeBrazilianPhone(state.q)
+ if (normalizedPhone) {
+ searchTerms.push(sql`"contact"."phone" = ${normalizedPhone}`)
+ } else if (/\d/.test(state.q)) {
+ const digits = state.q.replace(/\D/g, '')
+ if (digits) searchTerms.push(sql`"contact"."phone" ILIKE ${`%${digits}%`}`)
+ }
+ conditions.push(sql`(${sql.join(searchTerms, sql` OR `)})`)
+ }
+
+ const whereClause =
+ conditions.length > 0 ? sql` WHERE ${sql.join(conditions, sql` AND `)}` : sql``
+
+ const fromClause = needsContactJoin
+ ? sql`FROM "supporter" LEFT JOIN "contact" ON "contact"."id" = "supporter"."contact_id"`
+ : sql`FROM "supporter"`
+
+ return sql`
+ SELECT
+ COUNT(*) AS "total",
+ COUNT(*) FILTER (WHERE "supporter"."vote_intention" IN ('certo', 'tende_a_certo')) AS "certo_and_tende",
+ COUNT(*) FILTER (WHERE "supporter"."vote_intention" = 'indeciso') AS "indeciso"
+ ${fromClause}${whereClause}
+ `
+}
+
+export const computeSupporterListOverviewAggregate = async (
+ payload: Payload,
+ user: CampaignUser,
+ state: SupporterListState,
+): Promise => {
+ const access = await resolveAccessConstraint(payload, user)
+ if (access.kind === 'none') return null
+
+ if (payload.db.name !== 'postgres') {
+ throw new Error('O overview de apoiadores exige o adaptador PostgreSQL.')
+ }
+ const database = payload.db as unknown as { drizzle?: PostgresDb }
+ const drizzle = database.drizzle
+ if (!drizzle || typeof drizzle.execute !== 'function') {
+ throw new Error('A sessão PostgreSQL do overview de apoiadores não está disponível.')
+ }
+
+ const result = await drizzle.execute(buildAggregateSql(state, access))
+ const row = resultRows(result)[0]
+ if (!row) return null
+
+ const total = Number(row.total)
+ if (!Number.isSafeInteger(total) || total === 0) return null
+
+ return {
+ total,
+ certoAndTende: Number(row.certo_and_tende ?? 0),
+ indeciso: Number(row.indeciso ?? 0),
+ }
+}
diff --git a/src/utilities/supporterPageData.ts b/src/utilities/supporterPageData.ts
index ab6d7150..133a9025 100644
--- a/src/utilities/supporterPageData.ts
+++ b/src/utilities/supporterPageData.ts
@@ -5,6 +5,7 @@ import {
getSupporterRegistrationConsent,
getSupporterVoteIntentionConsent,
} from '@/utilities/campaignConsent'
+import { computeSupporterListOverviewAggregate } from '@/utilities/supporterListOverviewAggregate'
import {
buildSupporterListWhere,
resolveSupporterListUrl,
@@ -102,45 +103,11 @@ export const loadSupporterListPageData = async (
}
export const loadSupporterListOverviewData = async (
- payload: Pick,
+ payload: Payload,
user: CampaignUser,
state: SupporterListState,
-): Promise => {
- const where = buildSupporterListWhere(state)
-
- const [total, certoAndTende, indeciso] = await Promise.all([
- payload.count({
- collection: 'supporter',
- where,
- user,
- overrideAccess: false,
- }),
- payload.count({
- collection: 'supporter',
- where: {
- and: [where, { voteIntention: { in: ['certo', 'tende_a_certo'] } }],
- },
- user,
- overrideAccess: false,
- }),
- payload.count({
- collection: 'supporter',
- where: {
- and: [where, { voteIntention: { equals: 'indeciso' } }],
- },
- user,
- overrideAccess: false,
- }),
- ])
-
- if (total.totalDocs === 0) return null
-
- return {
- total: total.totalDocs,
- certoAndTende: certoAndTende.totalDocs,
- indeciso: indeciso.totalDocs,
- }
-}
+): Promise =>
+ computeSupporterListOverviewAggregate(payload, user, state)
export const loadSupporterDetailPageData = async (
payload: Pick,
diff --git a/src/utilities/supporterUi.ts b/src/utilities/supporterUi.ts
index ace87b5a..18c6c32d 100644
--- a/src/utilities/supporterUi.ts
+++ b/src/utilities/supporterUi.ts
@@ -6,6 +6,14 @@ import {
type SupporterVoteIntention,
} from '@/lib/schemas/supporter'
import type { CampaignUser, Supporter } from '@/payload-types'
+import {
+ buildListHref,
+ firstValue,
+ normalizedText,
+ resolveListUrl,
+ strictDecimalInteger,
+ type RawSearchParams as CampaignListRawSearchParams,
+} from '@/utilities/campaignListUrl'
import { normalizeBrazilianPhone } from '@/utilities/phone'
export const supporterPageSize = 25
@@ -32,27 +40,12 @@ export type SupporterListState = {
nucleus?: number
}
-type RawSearchParams = Record
+type RawSearchParams = CampaignListRawSearchParams
export const supporterListParamNames = ['q', 'voteIntention', 'city', 'nucleus', 'page'] as const
const supporterListParamNameSet = new Set(supporterListParamNames)
-const firstValue = (value: string | string[] | undefined): string | undefined =>
- Array.isArray(value) ? value[0] : value
-
-const normalizedText = (value: FormDataEntryValue | string | undefined): string | undefined => {
- if (typeof value !== 'string') return undefined
- const normalized = value.trim()
- return normalized || undefined
-}
-
-const strictDecimalInteger = (value: string | undefined): number | undefined => {
- if (!value || !/^[1-9]\d*$/.test(value)) return undefined
- const number = Number(value)
- return Number.isSafeInteger(number) ? number : undefined
-}
-
export const parseSupporterListParams = (params: RawSearchParams): SupporterListState => {
const rawPage = strictDecimalInteger(firstValue(params.page))
const q = normalizedText(firstValue(params.q))
@@ -125,31 +118,8 @@ export const buildSupporterListSearchParams = (
export const buildSupporterFiltersKey = (state: SupporterListState): string =>
buildSupporterListSearchParams(state).toString()
-export const buildSupporterListHref = (state: SupporterListState, page: number): string => {
- const params = buildSupporterListSearchParams(state, page)
- const query = params.toString()
- return query ? `/campanha/apoiadores?${query}` : '/campanha/apoiadores'
-}
-
-const inspectRawSupporterListParams = (
- params: RawSearchParams,
-): { hasUnsupportedParams: boolean; query: string } => {
- const serialized = new URLSearchParams()
- let hasUnsupportedParams = false
-
- for (const [name, value] of Object.entries(params)) {
- if (!supporterListParamNameSet.has(name)) {
- hasUnsupportedParams = true
- continue
- }
- if (value === undefined) continue
- for (const item of Array.isArray(value) ? value : [value]) {
- serialized.append(name, item)
- }
- }
-
- return { hasUnsupportedParams, query: serialized.toString() }
-}
+export const buildSupporterListHref = (state: SupporterListState, page: number): string =>
+ buildListHref(state, buildSupporterListSearchParams, '/campanha/apoiadores', page)
export const resolveSupporterListUrl = (
params: RawSearchParams,
@@ -158,25 +128,15 @@ export const resolveSupporterListUrl = (
state: SupporterListState
href: string
redirectHref?: string
-} => {
- const parsedState = parseSupporterListParams(params)
- const page =
- totalPages !== undefined && totalPages > 0 && parsedState.page > totalPages
- ? totalPages
- : parsedState.page
- const state = page === parsedState.page ? parsedState : { ...parsedState, page }
- const canonicalParams = buildSupporterListSearchParams(state)
- const canonicalQuery = canonicalParams.toString()
- const href = canonicalQuery ? `/campanha/apoiadores?${canonicalQuery}` : '/campanha/apoiadores'
- const raw = inspectRawSupporterListParams(params)
- const needsRedirect = raw.hasUnsupportedParams || raw.query !== canonicalQuery
-
- return {
- state,
- href,
- ...(needsRedirect ? { redirectHref: href } : {}),
- }
-}
+} =>
+ resolveListUrl({
+ params,
+ paramNameSet: supporterListParamNameSet,
+ parse: parseSupporterListParams,
+ buildSearchParams: buildSupporterListSearchParams,
+ basePath: '/campanha/apoiadores',
+ totalPages,
+ })
export const getSupporterScopeLabel = (total: number): string =>
`${total} ${total === 1 ? 'apoiador nos seus núcleos' : 'apoiadores nos seus núcleos'}`
diff --git a/tests/e2e/campaign-supporters.e2e.spec.ts b/tests/e2e/campaign-supporters.e2e.spec.ts
new file mode 100644
index 00000000..03758559
--- /dev/null
+++ b/tests/e2e/campaign-supporters.e2e.spec.ts
@@ -0,0 +1,77 @@
+/* eslint-disable check-file/filename-naming-convention */
+import { test, expect } from './fixtures/campaignE2EFixtures'
+
+test.describe('Campaign supporter journeys', () => {
+ test.describe.configure({ mode: 'parallel' })
+
+ test('removes a supporter through the confirmation dialog', async ({ campaign, page }) => {
+ test.setTimeout(240_000)
+ const generalEmail = `${campaign.fixtures.value('removal-general')}@example.com`
+ const password = campaign.fixtures.value('RemovalPassword')
+ const supporterName = campaign.fixtures.value('Apoiador removível')
+
+ const setup = await campaign.transaction(async (payload, req) => {
+ const general = await payload.create({
+ collection: 'campaignUser',
+ data: {
+ name: campaign.fixtures.value('Coordenação Geral remoção'),
+ email: generalEmail,
+ password,
+ role: 'geral',
+ },
+ depth: 0,
+ req,
+ })
+ const contact = await payload.create({
+ collection: 'contact',
+ data: {
+ name: supporterName,
+ phone: campaign.fixtures.phone(),
+ state: 'BA',
+ city: 'Salvador',
+ },
+ depth: 0,
+ req,
+ })
+ const supporter = await payload.create({
+ collection: 'supporter',
+ data: {
+ contact: contact.id,
+ source: 'manual',
+ createdBy: general.id,
+ },
+ depth: 0,
+ req,
+ })
+ return { general, supporter }
+ })
+
+ await campaign.login(page, generalEmail, password)
+ await page.goto(`${campaign.baseURL}/campanha/apoiadores/${setup.supporter.id}`)
+ await page.waitForLoadState('networkidle')
+
+ await page.getByRole('button', { name: 'Remover dados' }).click()
+ await expect(page.getByRole('dialog')).toBeVisible({ timeout: 20_000 })
+ await expect(
+ page.getByRole('heading', { name: 'Remover os dados deste apoiador?' }),
+ ).toBeVisible()
+
+ await page.getByRole('button', { name: 'Confirmar remoção' }).click()
+ await page.waitForURL(`${campaign.baseURL}/campanha/apoiadores`, { timeout: 30_000 })
+
+ await expect
+ .poll(
+ async () =>
+ (
+ await campaign.payload.find({
+ collection: 'supporter',
+ where: { id: { equals: setup.supporter.id } },
+ depth: 0,
+ limit: 1,
+ })
+ ).totalDocs,
+ { timeout: 30_000 },
+ )
+ .toBe(0)
+ })
+})
diff --git a/tests/e2e/fixtures/campaignE2EFixtures.ts b/tests/e2e/fixtures/campaignE2EFixtures.ts
index 1c6bb6fa..efc59827 100644
--- a/tests/e2e/fixtures/campaignE2EFixtures.ts
+++ b/tests/e2e/fixtures/campaignE2EFixtures.ts
@@ -21,11 +21,13 @@ type OwnedCollection =
| 'nucleusUpdate'
| 'campaignInvite'
| 'consent'
+ | 'supporter'
const deletionOrder: OwnedCollection[] = [
'campaignInvite',
'nucleusUpdate',
'leadership',
+ 'supporter',
'electoralNucleus',
'contact',
'campaignUser',
@@ -174,6 +176,21 @@ class CampaignE2EOwnership {
})
for (const update of updates.docs) this.own('nucleusUpdate', update.id)
}
+ if (contactIDs.length || nucleusIDs.length || userIDs.length) {
+ const supporters = await this.rootPayload.find({
+ collection: 'supporter',
+ where: {
+ or: [
+ ...(contactIDs.length ? [{ contact: { in: contactIDs } }] : []),
+ ...(nucleusIDs.length ? [{ nucleus: { in: nucleusIDs } }] : []),
+ ...(userIDs.length ? [{ createdBy: { in: userIDs } }] : []),
+ ],
+ },
+ depth: 0,
+ pagination: false,
+ })
+ for (const supporter of supporters.docs) this.own('supporter', supporter.id)
+ }
}
async cleanup(): Promise {
diff --git a/tests/int/campaignSupporter.int.spec.ts b/tests/int/campaignSupporter.int.spec.ts
index 4c45d5f0..da6e25c3 100644
--- a/tests/int/campaignSupporter.int.spec.ts
+++ b/tests/int/campaignSupporter.int.spec.ts
@@ -16,6 +16,7 @@ import {
SUPPORTER_REGISTRATION_CONSENT_KEY,
SUPPORTER_VOTE_INTENTION_CONSENT_KEY,
} from '@/utilities/campaignConsent'
+import { loadSupporterListOverviewData } from '@/utilities/supporterPageData'
import { installCampaignFixtures } from '../helpers/campaignFixtures'
@@ -251,22 +252,23 @@ describe('campaign supporter domain', () => {
expect(preview.counts.ok).toBe(1)
expect(preview.counts.duplicate).toBe(1)
expect(preview.counts.error).toBeGreaterThanOrEqual(2)
-
- const okRows = preview.rows
- .filter((row) => row.status === 'ok')
- .map((row) => ({
- nome: row.nome,
- telefone: row.normalizedPhone!,
- municipio: row.canonicalCity,
- intencao: row.voteIntention,
- }))
+ expect(preview.importToken).toBeTruthy()
+ expect(preview.sampleRows.length).toBeGreaterThan(0)
const result = await confirmSupporterImportRecord(payload, geral, {
operatorAttested: true,
- rows: okRows,
+ importToken: preview.importToken,
})
expect(result.created).toBe(1)
+ // Single-use token: a second confirm with the same token must fail.
+ await expect(
+ confirmSupporterImportRecord(payload, geral, {
+ operatorAttested: true,
+ importToken: preview.importToken,
+ }),
+ ).rejects.toThrow(/não encontrado|expirado|inválido/)
+
const contact = await payload.find({
collection: 'contact',
where: { phone: { equals: phoneOk } },
@@ -288,6 +290,57 @@ describe('campaign supporter domain', () => {
fixtures.own('supporter', created.docs[0]!.id)
})
+ it('rejects a tampered import token and a token issued to another actor', async () => {
+ const fixtures = campaignFixtures()
+ await ensureSupporterConsents(fixtures)
+ const geral = await fixtures.createCampaignUser('geral')
+ const otherGeral = await fixtures.createCampaignUser('geral')
+ const phoneOk = fixtures.phone()
+ const csv = `nome,telefone,municipio,intencao\nAna Silva,${phoneOk},Salvador,certo`
+
+ const preview = await previewSupporterImportText(payload, geral, csv)
+ expect(preview.counts.ok).toBe(1)
+
+ const [batchId, expiresAt, sig] = preview.importToken.split('.')
+ const tampered = `${batchId}.${expiresAt}.${sig.slice(0, -2)}xx`
+ await expect(
+ confirmSupporterImportRecord(payload, geral, {
+ operatorAttested: true,
+ importToken: tampered,
+ }),
+ ).rejects.toThrow(/inválido|expirado/)
+
+ // Token bound to a different actor cannot be redeemed by `geral`.
+ await expect(
+ confirmSupporterImportRecord(payload, otherGeral, {
+ operatorAttested: true,
+ importToken: preview.importToken,
+ }),
+ ).rejects.toThrow(/não encontrado|inválido|expirado/)
+
+ // Cleanup: consume the still-valid token so it does not leak into other tests.
+ await confirmSupporterImportRecord(payload, geral, {
+ operatorAttested: true,
+ importToken: preview.importToken,
+ })
+ const contact = await payload.find({
+ collection: 'contact',
+ where: { phone: { equals: phoneOk } },
+ depth: 0,
+ limit: 1,
+ overrideAccess: true,
+ })
+ fixtures.own('contact', contact.docs[0]!.id)
+ const created = await payload.find({
+ collection: 'supporter',
+ where: { contact: { equals: contact.docs[0]!.id } },
+ depth: 0,
+ limit: 1,
+ overrideAccess: true,
+ })
+ if (created.docs[0]) fixtures.own('supporter', created.docs[0].id)
+ })
+
it('removes supporter and anonymizes contact when no other joins remain', async () => {
const fixtures = campaignFixtures()
await ensureSupporterConsents(fixtures)
@@ -315,4 +368,81 @@ describe('campaign supporter domain', () => {
expect(contact.name).toBe('Titular removido')
expect(contact.phone).not.toBe(phone)
})
+
+ it('aggregates supporter overview KPIs in a single SQL query for geral', async () => {
+ const fixtures = campaignFixtures()
+ const registration = await ensureConsentByKey(fixtures, SUPPORTER_REGISTRATION_CONSENT_KEY)
+ const geral = await fixtures.createCampaignUser('geral')
+
+ const intentions: Array<{ voteIntention: 'certo' | 'tende_a_certo' | 'indeciso' | 'outro' | null }> = [
+ { voteIntention: 'certo' },
+ { voteIntention: 'certo' },
+ { voteIntention: 'tende_a_certo' },
+ { voteIntention: 'indeciso' },
+ { voteIntention: 'indeciso' },
+ { voteIntention: 'outro' },
+ { voteIntention: null },
+ ]
+ for (const { voteIntention } of intentions) {
+ const contact = await fixtures.createContact()
+ await fixtures.createSupporter({
+ contact: contact.id,
+ consent: registration.id,
+ consentContentHash: 'hash',
+ consentedAt: new Date().toISOString(),
+ createdBy: geral.id,
+ ...(voteIntention ? { voteIntention } : {}),
+ })
+ }
+
+ const overview = await loadSupporterListOverviewData(payload, geral, { page: 1 })
+ expect(overview).not.toBeNull()
+ expect(overview!.total).toBe(intentions.length)
+ expect(overview!.certoAndTende).toBe(3)
+ expect(overview!.indeciso).toBe(2)
+ })
+
+ it('scopes the overview aggregate to accessible nuclei for coordenador and applies filters', async () => {
+ const fixtures = campaignFixtures()
+ const registration = await ensureConsentByKey(fixtures, SUPPORTER_REGISTRATION_CONSENT_KEY)
+ const coordenador = await fixtures.createCampaignUser('coordenador')
+ const assigned = await fixtures.createNucleus({ coordinators: [coordenador.id] })
+ const other = await fixtures.createNucleus()
+
+ const makeSupporter = async (
+ nucleusId: number | undefined,
+ voteIntention: 'certo' | 'indeciso' | null,
+ city = 'Salvador',
+ name = fixtures.value('Apoiador'),
+ ) => {
+ const contact = await fixtures.createContact({ city, name })
+ await fixtures.createSupporter({
+ contact: contact.id,
+ consent: registration.id,
+ consentContentHash: 'hash',
+ consentedAt: new Date().toISOString(),
+ createdBy: coordenador.id,
+ ...(nucleusId ? { nucleus: nucleusId } : {}),
+ ...(voteIntention ? { voteIntention } : {}),
+ })
+ }
+
+ await makeSupporter(assigned.id, 'certo')
+ await makeSupporter(assigned.id, 'indeciso')
+ await makeSupporter(assigned.id, null, 'Feira de Santana')
+ await makeSupporter(other.id, 'certo') // outside scope — must not be counted
+
+ const overview = await loadSupporterListOverviewData(payload, coordenador, { page: 1 })
+ expect(overview).not.toBeNull()
+ expect(overview!.total).toBe(3)
+ expect(overview!.certoAndTende).toBe(1)
+ expect(overview!.indeciso).toBe(1)
+
+ const filtered = await loadSupporterListOverviewData(payload, coordenador, {
+ page: 1,
+ city: 'Feira de Santana',
+ })
+ expect(filtered).not.toBeNull()
+ expect(filtered!.total).toBe(1)
+ })
})
diff --git a/tests/unit/campaignNucleusUi.unit.spec.ts b/tests/unit/campaignNucleusUi.unit.spec.ts
index 1326a09e..aa91f3e2 100644
--- a/tests/unit/campaignNucleusUi.unit.spec.ts
+++ b/tests/unit/campaignNucleusUi.unit.spec.ts
@@ -5,9 +5,9 @@ import { afterEach, describe, expect, it } from 'vitest'
import { archiveNucleusFormAction } from '@/app/(campaign)/campanha/(app)/nucleos/formActions'
import { ArchiveNucleusDialog } from '@/components/campaign/ArchiveNucleusDialog'
+import { getPaginationPages } from '@/components/campaign/CampaignListPagination'
import { NucleusFormFields } from '@/components/campaign/NucleusForm'
import { NucleusList } from '@/components/campaign/NucleusList'
-import { getNucleusPaginationPages } from '@/components/campaign/NucleusPagination'
import { NucleusTerritoryAndZonesFields } from '@/components/campaign/NucleusTerritoryAndZonesFields'
import { TseZoneInput } from '@/components/campaign/TseZoneInput'
import { campaignNav, getCampaignNav, isCampaignNavActive } from '@/components/campaign/nav'
@@ -498,9 +498,9 @@ describe('campaign nucleus UI contracts', () => {
})
it('keeps pagination boundaries compact and valid', () => {
- expect(getNucleusPaginationPages(1, 10)).toEqual([1, 2, 10])
- expect(getNucleusPaginationPages(10, 10)).toEqual([1, 9, 10])
- expect(getNucleusPaginationPages(5, 10)).toEqual([1, 4, 5, 6, 10])
+ expect(getPaginationPages(1, 10)).toEqual([1, 2, 10])
+ expect(getPaginationPages(10, 10)).toEqual([1, 9, 10])
+ expect(getPaginationPages(5, 10)).toEqual([1, 4, 5, 6, 10])
})
it('returns a recoverable archive error for an invalid identifier', async () => {
diff --git a/tests/unit/supporterSchema.unit.spec.ts b/tests/unit/supporterSchema.unit.spec.ts
index 3ff67201..61daed57 100644
--- a/tests/unit/supporterSchema.unit.spec.ts
+++ b/tests/unit/supporterSchema.unit.spec.ts
@@ -45,19 +45,25 @@ describe('supporter schemas', () => {
).toBe(true)
})
- it('requires operator attestation for import confirm', () => {
+ it('requires operator attestation and a token for import confirm', () => {
expect(
supporterImportConfirmSchema.safeParse({
- rows: [{ nome: 'Ana Silva', telefone: '71988887777' }],
+ importToken: 'token',
+ }).success,
+ ).toBe(false)
+
+ expect(
+ supporterImportConfirmSchema.safeParse({
+ operatorAttested: true,
}).success,
).toBe(false)
expect(
supporterImportConfirmSchema.parse({
operatorAttested: true,
- rows: [{ nome: 'Ana Silva', telefone: '71988887777', intencao: 'certo' }],
- }).rows[0]?.telefone,
- ).toBe('71988887777')
+ importToken: 'batch.123.sig',
+ }).importToken,
+ ).toBe('batch.123.sig')
})
it('requires vote intention consent acceptance', () => {