From 033c270a898207a9ab39fa2a79edaf268e1d4452 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Ign=C3=A1cio=20Fontoura=20Solla?= Date: Thu, 6 Aug 2026 16:51:48 -0300 Subject: [PATCH] =?UTF-8?q?feat(campanha):=20B163=20=E2=80=94=20editar=20c?= =?UTF-8?q?ontatos=20das=20dobradinhas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/CHANGELOG-AGENTS.md | 2 + .../editar-campos-lista-dobradinhas-impl.md | 108 + scripts/import-projecao.mjs | 17 +- scripts/seed-minimal.mjs | 49 +- .../{[slug] => [id]}/formActions.ts | 2 +- .../dobradinhas/{[slug] => [id]}/page.tsx | 20 +- .../campanha/(app)/dobradinhas/formActions.ts | 77 + .../(app)/dobradinhas/nova/formActions.ts | 2 +- .../campanha/(app)/dobradinhas/page.tsx | 69 +- .../campanha/(app)/liderancas/page.tsx | 37 +- .../(campaign)/campanha/actions/leadership.ts | 76 +- .../campanha/actions/stateDeputy.ts | 245 +- src/collections/Contact.ts | 36 +- src/collections/StateDeputy.ts | 77 +- .../dashboard/HomeSearchStateDeputyGroup.tsx | 4 +- .../LeadershipContactFieldControl.tsx | 202 - .../leadership/LeadershipContactSection.tsx | 33 +- .../shared/CampaignInlineEditableCell.tsx | 309 + .../MunicipalityStateDeputyRelationCell.tsx | 6 +- .../campaign/stateDeputy/StateDeputyChips.tsx | 2 +- .../stateDeputy/StateDeputyContactSection.tsx | 65 + .../campaign/stateDeputy/StateDeputyForm.tsx | 10 +- src/lib/campaignHomeSearchHits.ts | 1 + src/lib/schemas/contact.ts | 33 + src/lib/schemas/leadership.ts | 25 +- src/lib/schemas/stateDeputy.ts | 15 +- ...60806_082110_add_state_deputy_contact.json | 12908 ++++++++++++++++ ...0260806_082110_add_state_deputy_contact.ts | 159 + src/migrations/index.ts | 10 +- src/payload-types.ts | 90 +- src/utilities/access/contacts.ts | 20 + src/utilities/ai/tools/getDobradinhas.ts | 100 +- src/utilities/ai/tools/searchEntities.ts | 13 +- src/utilities/campaignRelationOptions.ts | 25 +- src/utilities/contactPhoneInvariant.ts | 64 +- .../homeSearch/searchHomeStateDeputies.ts | 15 +- src/utilities/stateDeputy/nameInvariant.ts | 44 + src/utilities/stateDeputyData.ts | 56 +- src/utilities/stateDeputyListUrl.ts | 7 +- tests/helpers/campaignFixtures.ts | 14 +- tests/int/campaignEntityListData.int.spec.ts | 25 +- ...eadershipStateDeputyMembership.int.spec.ts | 4 +- ...ampaignMigrationReconciliation.int.spec.ts | 83 + ...nMunicipalityStateDeputyCreate.int.spec.ts | 140 +- ...gnStateDeputyAdvisorMembership.int.spec.ts | 10 +- .../campaignInlineEditableCell.unit.spec.tsx | 158 + tests/unit/codebaseConventions.unit.spec.ts | 2 +- ...alityStateDeputyRelationCell.unit.spec.tsx | 2 +- tests/unit/stateDeputyListUrl.unit.spec.ts | 2 +- 49 files changed, 14900 insertions(+), 573 deletions(-) create mode 100644 docs/plans/editar-campos-lista-dobradinhas-impl.md rename src/app/(campaign)/campanha/(app)/dobradinhas/{[slug] => [id]}/formActions.ts (96%) rename src/app/(campaign)/campanha/(app)/dobradinhas/{[slug] => [id]}/page.tsx (92%) delete mode 100644 src/components/campaign/leadership/LeadershipContactFieldControl.tsx create mode 100644 src/components/campaign/shared/CampaignInlineEditableCell.tsx create mode 100644 src/components/campaign/stateDeputy/StateDeputyContactSection.tsx create mode 100644 src/migrations/20260806_082110_add_state_deputy_contact.json create mode 100644 src/migrations/20260806_082110_add_state_deputy_contact.ts create mode 100644 src/utilities/stateDeputy/nameInvariant.ts create mode 100644 tests/unit/campaignInlineEditableCell.unit.spec.tsx diff --git a/docs/CHANGELOG-AGENTS.md b/docs/CHANGELOG-AGENTS.md index 3fc1a2ba..776f93c4 100644 --- a/docs/CHANGELOG-AGENTS.md +++ b/docs/CHANGELOG-AGENTS.md @@ -4,6 +4,8 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente --- +**Recently resolved (2026-08-06):** **B163** (Issue #391) — `/campanha/dobradinhas` agora edita Nome, Partido, E-mail e Telefone inline; `Contact` virou a fonte de identidade, dobradinha nova cria o Contact na mesma transação, e a ficha usa ID canônico com fallback/redirect de slugs antigos. A migration faz backfill idempotente, mantém a invariável de nome/telefone e os links de relações/busca apontam para ID. Intenção [`editar-campos-lista-dobradinhas.md`](plans/editar-campos-lista-dobradinhas.md); impl [`editar-campos-lista-dobradinhas-impl.md`](plans/editar-campos-lista-dobradinhas-impl.md). + **Recently resolved (2026-08-05):** **B158** (Issue #364) — `/campanha/municipios` troca cards↔tabela e revela colunas pela largura do painel de conteúdo com named container queries (48–84rem), sem rolagem horizontal; Território virou sublinha de Município, Tendência/Sinal preservam um único editor na tabela quando ocultos manualmente, e `campaign_columns=municipios:__none__` diferencia “todas visíveis” dos defaults. E2E mede cada limiar em Chromium e mantém um caso iPad-sized no projeto WebKit focado. Sem migration ou mudança de RBAC. Intenção [`colunas-responsivas-municipios.md`](docs/plans/colunas-responsivas-municipios.md); impl [`colunas-responsivas-municipios-impl.md`](docs/plans/colunas-responsivas-municipios-impl.md). **Recently resolved (2026-08-05):** **B159** (Issue #374) — as colunas Assessores, Lideranças e Dobradinhas de `/campanha/municipios` agora compartilham `MunicipalityRelationEditor` (`CampaignCellEditOverlay` + Command + chips + optimistic reconcile) e `MunicipalityRelationAvatarStack`; Dobradinhas deixa `RelationChipCell` só nesta superfície, preserva busca por partido e links para a ficha, e Lideranças cria inline somente com nome (`Contact.phone = null`, sem stub; telefone real depois na ficha). Sem migration, rota nova ou mudança de access. Intenção [`generalizar-colunas-relacao-municipios.md`](docs/plans/generalizar-colunas-relacao-municipios.md); impl [`generalizar-colunas-relacao-municipios-impl.md`](docs/plans/generalizar-colunas-relacao-municipios-impl.md). diff --git a/docs/plans/editar-campos-lista-dobradinhas-impl.md b/docs/plans/editar-campos-lista-dobradinhas-impl.md new file mode 100644 index 00000000..924cb836 --- /dev/null +++ b/docs/plans/editar-campos-lista-dobradinhas-impl.md @@ -0,0 +1,108 @@ +# Impl: Editar nome, partido, e-mail e telefone na lista de dobradinhas + +Status: aprovado +Atualizado em: 2026-08-06 +Issue: #391 +Intenção: docs/plans/editar-campos-lista-dobradinhas.md +Appetite restante: ~1–1,5 dia de engenharia; um vertical slice de Contact + edição inline, sem remodelar as relações da lista + +## Leitura da intenção + +- **Outcome:** staff corrige Nome, Partido, E-mail e Telefone na própria lista de `/campanha/dobradinhas`, com feedback de salvamento, sem perder o link da ficha no texto do nome. +- **O que NÃO negociar:** `Contact` é a fonte de nome/e-mail/telefone; dobradinha nova já nasce com Contact; nome no texto abre a ficha; clique na célula fora do texto edita; partido continua em `stateDeputy`; ID é a chave canônica da ficha; `leader` não acessa a superfície; conflito de telefone falha sem merge. +- **O que reavaliar:** a hipótese de manter `stateDeputy.name` e a rota `[slug]`. Manter nome duplicado criaria drift entre a lista e Contact; manter slug como chave canônica faria rename continuar perigoso. + +## Abordagem recomendada + +```mermaid +flowchart LR + M[Migration: Contact obrigatório + backfill] --> D[StateDeputy: contact, party, notes, slug legado] + C[Create dobradinha] --> T1[Transação: cria Contact + StateDeputy] + D --> L[Loader depth 1: nome, e-mail, telefone, partido] + L --> U[Controle inline compartilhado] + U --> T2[Action: revalida staff + locks de telefone + transação] + T2 --> P[Atualiza Contact ou StateDeputy] + R[URL /dobradinhas/:id ou slug antigo] --> Q[Resolve por id/slug] + Q --> X[Redirect para /dobradinhas/:id] +``` + +**Opções consideradas:** + +- **A — manter `stateDeputy.name` e espelhar o valor no Contact:** menor diff imediato, mas cria duas fontes de verdade para a pessoa e torna qualquer edição futura um problema de sincronização. +- **B — `Contact` obrigatório, backfill dos existentes, `slug` preservado somente como alias legado:** mantém o padrão de `Leadership.contact`, satisfaz a criação atômica e torna rename independente da URL. +- **C — criar Contact somente no primeiro e-mail/telefone editado:** parece reduzir a migration, mas viola o aceite de Contact na criação e deixa a lista com duas formas de registro. + +**Recomendação:** B — o custo irreversível é concentrado numa migration explícita e o restante passa a ter uma única fonte de verdade, sem criar uma collection paralela de pessoa. + +**Alternativas rejeitadas:** A porque perpetua drift e o `name` paralelo explicitamente proibido pela intenção; C porque deixa dobradinhas recém-criadas sem Contact e complica o vínculo posterior. + +### Decisões de engenharia + +- **Schema:** remover `StateDeputy.name`; adicionar `contact` obrigatório e único, seguindo `Leadership.contact`; manter `slug` único e imutável como alias de compatibilidade, nunca como identidade canônica. A ação de criação recebe o nome, cria o Contact e o hook deriva o slug do Contact. +- **Migration:** gerar a migration com `pnpm migrate:create add_state_deputy_contact`, revisar o snapshot JSON e hand-write apenas o backfill necessário. A subida adiciona a coluna nullable, cria um Contact name-only (`state: 'BA'`) por dobradinha existente, liga cada linha, aplica FK/índice único/not-null e remove `state_deputy.name`/índice antigo. A descida restaura `name` a partir do Contact antes de remover a relação, sem apagar Contacts que podem ter sido compartilhados; deve ser protegida contra perda de dados alterados. +- **URL:** renomear o segmento interno para `[id]`, mas o resolver aceita número ou slug legado. Toda leitura por slug redireciona para `/campanha/dobradinhas/`; novos links de dados já resolvidos usam ID. O slug permanece consultável para não quebrar links antigos. +- **Escrita:** criar Contact + StateDeputy numa única `withPayloadTransaction`, passando `req` a todas as operações. A edição de Contact primeiro lê a dobradinha com `user` + `overrideAccess: false`; somente depois usa o bypass administrativo justificado para atualizar o Contact na mesma transação. +- **Telefone:** mover a checagem de lock/ambiguidade hoje privada de `leadership.ts` para o dono da invariável (`contactPhoneInvariant.ts`) e reutilizá-la em Leadership e StateDeputy. `CONTACT_PHONE_CONFLICT_MESSAGE` e `CONTACT_PHONE_AMBIGUOUS_MESSAGE` continuam mensagens seguras e allowlisted. +- **UI:** extrair a máquina de edição de `LeadershipContactFieldControl` para um controle compartilhado de célula. Ele preserva o modo `lápis` de lideranças e adiciona o modo `célula` das dobradinhas; aceita link no valor do nome, formato de telefone, action e nome do campo sem copiar a lógica de autosave. Partido usa a mesma máquina, mas grava somente `stateDeputy.party`. +- **Admin/access:** `contact` pode ser preenchido na criação, mas não alterado por update comum da dobradinha; a edição de pessoa fica nas actions autorizadas. `canReadContacts` passa a incluir Contacts ligados a `stateDeputy` para assessores, sem abrir Contacts de lideranças/apoiadores fora do escopo; coordenador/candidato já são irrestritos e líder continua negado. + +### Componentes / mudanças + +- **`src/collections/StateDeputy.ts`:** usar `contact` como título/admin column; remover hook/validação dependente de `name`; derivar o alias `slug` do Contact na criação; proteger `contact` e `slug` contra updates de campanha. +- **`src/migrations/_add_state_deputy_contact.{ts,json}` + `src/migrations/index.ts`:** migration de schema e backfill idempotente, sem editar migrations congeladas. +- **`src/lib/schemas/stateDeputy.ts`:** separar nome de criação (destinado ao Contact), update de partido/notas e update inline de partido; manter limites de Contact e mensagens de conflito. +- **`src/lib/schemas/contact.ts` (ou owner equivalente após o depth check):** contrato puro de edição por campo compartilhado por Leadership e StateDeputy; eliminar o contrato duplicado em `leadership.ts` se a extração confirmar os mesmos limites. +- **`src/app/(campaign)/campanha/actions/stateDeputy.ts`:** criação transacional, `updateStateDeputyContactRecord`, update inline de partido e revalidação dos caminhos por ID; nenhum caminho aceita `leader` ou grava Contact sem a checagem da dobradinha. +- **`src/app/(campaign)/campanha/actions/leadership.ts` + `src/utilities/contactPhoneInvariant.ts`:** reutilizar o helper genérico de telefone sem alterar a política B153. +- **`src/utilities/access/contacts.ts`:** incluir Contacts de dobradinhas no conjunto de leitura do assessor, com query administrativa somente para resolver IDs de linhas que o acesso de StateDeputy já permite. +- **`src/utilities/stateDeputyData.ts` e `src/utilities/stateDeputyListUrl.ts`:** buscar/sortear/filtrar por `contact.name`, montar `name/email/phone` a partir de `depth: 1` e manter view models mínimos. +- **`src/utilities/campaignRelationOptions.ts`, `src/utilities/homeSearch/searchHomeStateDeputies.ts`, `src/lib/campaignHomeSearchHits.ts` e ferramentas AI:** trocar leituras de `stateDeputy.name` por `contact.name`; carregar ID; manter slug somente onde o contrato legado ainda precisa aceitá-lo. +- **`src/components/campaign/shared/CampaignInlineEditableCell.tsx` (nome final a confirmar no código):** máquina client-safe de edição, foco, blur, debounce existente, Escape, pending/erro e refresh; preservar copy-on-click/lápis de B153 e adicionar trigger por célula. +- **`src/components/campaign/leadership/*`:** usar o controle compartilhado e manter o comportamento visual/funcional já entregue em B153. +- **`src/app/(campaign)/campanha/(app)/dobradinhas/page.tsx`:** colunas Nome, Partido, E-mail e Telefone; nome como link somente no texto, célula fora do link como editor; placeholders legíveis; relações existentes permanecem intactas. +- **`src/app/(campaign)/campanha/(app)/dobradinhas/[id]/`:** resolver ID/slug legado, redirecionar para ID, exibir bloco Contact editável e deixar o form longo responsável apenas por partido/notas. +- **Links de primeira parte em listas, chips, busca, relação e redirect de criação:** usar ID; contratos AI que ainda recebem slug ficam protegidos pelo fallback/redirect até uma mudança explícita do contrato de ferramenta. + +### Dados → forma + +- **Forma escolhida:** células de leitura com um único editor ativo por gesto; Nome tem texto-link e área restante editável; Partido/E-mail/Telefone entram em input ao clicar na célula; pending/erro ficam no próprio controle e sucesso chama `router.refresh()`. +- **Por quê:** corresponde ao gesto travado de “edit where you see”, preserva a varredura da lista e evita uma tabela-planilha com inputs sempre montados. +- **Rejeitadas:** toggle global de edição, inputs permanentes e lápis obrigatório nas dobradinhas; todos aumentam o modo mental e divergem do canvas/aceite. + +## Fases verificáveis + +1. **Tracer / schema + dados:** adicionar `contact` no StateDeputy, escrever/revisar migration e backfill, gerar types, adaptar fixture de StateDeputy para criar Contact name-only e provar que create/rollback não deixa órfão de dobradinha. +2. **Server / access:** implementar criação transacional, edição de Contact/partido com schema e locks, atualizar loader/list/search/AI e cobrir coordenador, assessor, líder negado, telefone conflitante e slug legado. +3. **UI:** extrair o controle compartilhado B153; adicionar as quatro colunas e bloco Contact da ficha; alinhar links para ID e feedback de blur. Validar teclado, foco, placeholder, Escape e responsividade da tabela. +4. **Gates:** rodar focused unit/int após cada slice; depois `pnpm generate:types`, `pnpm gate:fast`, `pnpm format:check`, `pnpm exec knip`, `pnpm check:cycles`, `pnpm test`, build local/test DB e `pnpm push` conforme o fechamento do pipeline. + +## Rabbit holes / Não escopo (engenharia) + +- Unificar Contact entre dobradinha, liderança e assessor; a relação fica compatível com esse futuro, mas não haverá merge/reuso automático por nome ou telefone. +- Alterar a política de acesso ou criar Consent para dado interno; não há novo opt-in LGPD nesta superfície. +- Migrar a API de ferramentas AI de slug para ID como um redesign separado; basta que o resolver legado redirecione e que novas superfícies próprias usem ID. +- Criar uma tabela/collection de aliases, editar em massa, debounce novo ou mecanismo de autosave paralelo. +- Reorganizar chips de municípios/lideranças/assessores ou mudar filtros/ordenação além da troca do campo de nome para `contact.name`. + +## Riscos e mitigação + +- **Backfill destrutivo ou parcialmente aplicado:** migration idempotente, contador/`RAISE NOTICE` de linhas afetadas, teste de fonte e rehearsal no banco local; nunca rodar contra produção manualmente. +- **Contact de assessor não consegue ler a nova coluna:** incluir o vínculo StateDeputy no access de Contact e testar leitura como advisor e negação como leader. +- **Telefone duplicado/ambíguo:** lock advisory dos telefones antigo/novo e consulta administrativa limitada; testar corrida/conflict e não oferecer merge. +- **Nome linkável perde o gesto de célula:** teste de componente para clique no texto versus área externa, com `stopPropagation`, foco e Escape. +- **Rename deixa link antigo quebrado:** slug permanece imutável como alias e a rota aceita slug antes de redirecionar ao ID. +- **View model vaza documento inteiro:** selects explícitos e resumo de Contact; nenhuma nova leitura usa `depth` sem necessidade. +- **Refactor B153 regrede copy/lápis:** testes existentes e novos testes do controle compartilhado devem cobrir ambos os modos antes de trocar a implementação. + +## Aceite de engenharia + +- [x] Aceite de produto da intenção ainda coberto +- [x] `stateDeputy.name` não permanece como segunda fonte; nome/e-mail/telefone vêm de Contact +- [x] Create e Contact update multi-collection usam transação e `req` +- [x] Local API com actor usa `overrideAccess: false`; bypass do Contact está justificado após row access +- [x] `leader` continua fora e assessor só vê Contacts permitidos +- [x] Telefone usa lock/invariante existente e mensagens seguras +- [x] URLs novas usam ID; slug antigo resolve e redireciona +- [x] Testes de migration, domínio/access e UI previstos + +**Self-score decision-quality: 5/5** — decisões caras têm alternativas rejeitadas; o appetite permanece bounded; rabbit holes estão nomeados; o controle B153 e a invariável de telefone são reutilizados; o outcome da intenção não foi alterado. diff --git a/scripts/import-projecao.mjs b/scripts/import-projecao.mjs index 50ed6cb2..dea1a9f5 100644 --- a/scripts/import-projecao.mjs +++ b/scripts/import-projecao.mjs @@ -789,11 +789,11 @@ for (const [slug, entry] of staffRoster) { const existingDeputies = await payload.find({ collection: 'stateDeputy', - depth: 0, + depth: 1, limit: 5000, pagination: false, overrideAccess: true, - select: { name: true, slug: true }, + select: { contact: true, slug: true }, }) const deputiesBySlug = new Map(existingDeputies.docs.map((deputy) => [deputy.slug, deputy])) @@ -1124,9 +1124,20 @@ try { let deputiesCreated = 0 for (const plan of deputyPlans.values()) { if (plan.deputyId != null) continue + const contact = await payload.create({ + collection: 'contact', + data: { + name: plan.name, + state: 'BA', + city: null, + }, + depth: 0, + overrideAccess: true, + req, + }) const created = await payload.create({ collection: 'stateDeputy', - data: { name: plan.name }, + data: { contact: contact.id }, depth: 0, overrideAccess: true, req, diff --git a/scripts/seed-minimal.mjs b/scripts/seed-minimal.mjs index eb1e4789..60d97938 100644 --- a/scripts/seed-minimal.mjs +++ b/scripts/seed-minimal.mjs @@ -44,6 +44,7 @@ assertLocalDatabase( const config = (await import('../src/payload.config.ts')).default const { provisionOnda0ConsentAndPrivacy } = await import('../src/utilities/onda0Provision.ts') const { hashConsentContent } = await import('../src/utilities/consentContentHash.ts') +const { withPayloadTransaction } = await import('../src/utilities/payloadTransaction.ts') const { WHATSAPP_SUBSCRIPTION_CONSENT_KEY } = await import('../src/lib/campaignConsentKeys.ts') const payload = await getPayload({ config }) @@ -151,12 +152,48 @@ try { }, `organization ${MINIMAL_ORGANIZATION.slug}`, ) - const stateDeputy = await upsert( - 'stateDeputy', - { slug: { equals: MINIMAL_STATE_DEPUTY.slug } }, - { ...MINIMAL_STATE_DEPUTY }, - `stateDeputy ${MINIMAL_STATE_DEPUTY.slug}`, - ) + let stateDeputy = await findOne('stateDeputy', { + slug: { equals: MINIMAL_STATE_DEPUTY.slug }, + }) + if (!stateDeputy) { + stateDeputy = await withPayloadTransaction(payload, async ({ req }) => { + const contactResult = await payload.find({ + collection: 'contact', + where: { email: { equals: `${MINIMAL_STATE_DEPUTY.slug}@planilha.invalid` } }, + depth: 0, + limit: 1, + pagination: false, + overrideAccess: true, + req, + }) + const contact = + contactResult.docs[0] ?? + (await payload.create({ + collection: 'contact', + data: { + name: MINIMAL_STATE_DEPUTY.name, + email: `${MINIMAL_STATE_DEPUTY.slug}@planilha.invalid`, + state: 'BA', + city: null, + }, + depth: 0, + overrideAccess: true, + req, + })) + + return payload.create({ + collection: 'stateDeputy', + data: { contact: contact.id, party: MINIMAL_STATE_DEPUTY.party }, + depth: 0, + overrideAccess: true, + req, + }) + }) + console.log(`[seed:minimal] created contact ${MINIMAL_STATE_DEPUTY.name}`) + console.log(`[seed:minimal] created stateDeputy ${MINIMAL_STATE_DEPUTY.slug}`) + } else { + console.log(`[seed:minimal] ok (exists) stateDeputy ${MINIMAL_STATE_DEPUTY.slug}`) + } // 5. Leaderships (contact upserted by phone; leadership unique by contact). for (const entry of MINIMAL_LEADERSHIPS) { diff --git a/src/app/(campaign)/campanha/(app)/dobradinhas/[slug]/formActions.ts b/src/app/(campaign)/campanha/(app)/dobradinhas/[id]/formActions.ts similarity index 96% rename from src/app/(campaign)/campanha/(app)/dobradinhas/[slug]/formActions.ts rename to src/app/(campaign)/campanha/(app)/dobradinhas/[id]/formActions.ts index fac6c17b..89fed8dc 100644 --- a/src/app/(campaign)/campanha/(app)/dobradinhas/[slug]/formActions.ts +++ b/src/app/(campaign)/campanha/(app)/dobradinhas/[id]/formActions.ts @@ -31,7 +31,7 @@ export const updateStateDeputyFormAction = async ( party: nullableFormText(formData, 'party'), notes: nullableFormText(formData, 'notes'), }) - revalidatePath('/campanha/dobradinhas/[slug]', 'page') + revalidatePath('/campanha/dobradinhas/[id]', 'page') return { message: 'Dobradinha atualizada.' } }, safeMessages: [STATE_DEPUTY_STAFF_MESSAGE], diff --git a/src/app/(campaign)/campanha/(app)/dobradinhas/[slug]/page.tsx b/src/app/(campaign)/campanha/(app)/dobradinhas/[id]/page.tsx similarity index 92% rename from src/app/(campaign)/campanha/(app)/dobradinhas/[slug]/page.tsx rename to src/app/(campaign)/campanha/(app)/dobradinhas/[id]/page.tsx index eafc0648..ed5f5538 100644 --- a/src/app/(campaign)/campanha/(app)/dobradinhas/[slug]/page.tsx +++ b/src/app/(campaign)/campanha/(app)/dobradinhas/[id]/page.tsx @@ -1,12 +1,13 @@ import config from '@payload-config' import { HandshakeIcon, MapPinIcon, UserCogIcon } from 'lucide-react' import Link from 'next/link' -import { notFound } from 'next/navigation' +import { notFound, redirect } from 'next/navigation' import { getPayload } from 'payload' import { SetCampaignPageChrome } from '@/components/campaign/shell/CampaignPageChromeContext' import { CampaignPageShell } from '@/components/campaign/shell/CampaignPageShell' import { StateDeputyAdvisorRelationCell } from '@/components/campaign/stateDeputy/StateDeputyAdvisorRelationCell' +import { StateDeputyContactSection } from '@/components/campaign/stateDeputy/StateDeputyContactSection' import { StateDeputyForm } from '@/components/campaign/stateDeputy/StateDeputyForm' import { Badge } from '@/components/ui/Badge' import { campaignPageMetadata } from '@/lib/campaignPageChrome' @@ -20,13 +21,13 @@ import { } from './formActions' export async function generateMetadata({ params }: StateDeputyDetailPageProps) { - const { slug } = await params + const { id } = await params const [user, payload] = await Promise.all([ requireCampaignPageActor({ gate: 'staff' }), getPayload({ config }), ]) - const stateDeputy = await loadStateDeputyDetail(payload, user, slug) + const stateDeputy = await loadStateDeputyDetail(payload, user, id) if (!stateDeputy) return campaignPageMetadata({ title: 'Dobradinha' }) return campaignPageMetadata( @@ -37,18 +38,19 @@ export async function generateMetadata({ params }: StateDeputyDetailPageProps) { } type StateDeputyDetailPageProps = { - params: Promise<{ slug: string }> + params: Promise<{ id: string }> } export default async function StateDeputyDetailPage({ params }: StateDeputyDetailPageProps) { - const { slug } = await params + const { id } = await params const [user, payload] = await Promise.all([ requireCampaignPageActor({ gate: 'staff' }), getPayload({ config }), ]) - const stateDeputy = await loadStateDeputyDetail(payload, user, slug) + const stateDeputy = await loadStateDeputyDetail(payload, user, id) if (!stateDeputy) notFound() + if (String(stateDeputy.id) !== id) redirect(`/campanha/dobradinhas/${stateDeputy.id}`) // B156 — only coordinator/candidate assign advisors; the rest of staff reads. const canEditAdvisors = isCampaignUnrestricted(user) @@ -63,6 +65,12 @@ export default async function StateDeputyDetailPage({ params }: StateDeputyDetai : { title: stateDeputy.name } } /> + {stateDeputy.notes ? (

{stateDeputy.notes}

) : null} diff --git a/src/app/(campaign)/campanha/(app)/dobradinhas/formActions.ts b/src/app/(campaign)/campanha/(app)/dobradinhas/formActions.ts index f658d595..0b3bd2b0 100644 --- a/src/app/(campaign)/campanha/(app)/dobradinhas/formActions.ts +++ b/src/app/(campaign)/campanha/(app)/dobradinhas/formActions.ts @@ -4,10 +4,14 @@ import { setLeadershipStateDeputyMembership } from '@/app/(campaign)/campanha/ac import { setStateDeputyAdvisorMembership, setStateDeputyMunicipalitiesBatch, + updateStateDeputyContact, + updateStateDeputyParty, } from '@/app/(campaign)/campanha/actions/stateDeputy' import { + nullableFormText, repeatedRelationshipFormValues, requiredFormBoolean, + requiredFormText, requiredRelationshipFormValue, } from '@/lib/formData' import { @@ -16,15 +20,88 @@ import { } from '@/lib/schemas/leadership' import { STATE_DEPUTY_ADVISOR_SAFE_MESSAGES, + STATE_DEPUTY_CONFLICT_MESSAGE, + STATE_DEPUTY_INVALID_CONTACT_MESSAGE, + STATE_DEPUTY_INVALID_FIELD_MESSAGE, STATE_DEPUTY_MUNICIPALITIES_SAFE_MESSAGES, + STATE_DEPUTY_STAFF_MESSAGE, } from '@/lib/schemas/stateDeputy' import { runCampaignFormAction, type CampaignFormActionState, } from '@/utilities/campaignFormActionError' +import { + CONTACT_PHONE_AMBIGUOUS_MESSAGE, + CONTACT_PHONE_CONFLICT_MESSAGE, +} from '@/utilities/contactPhoneInvariant' const safeMessages = [LEADERSHIP_STAFF_MESSAGE, LEADERSHIP_STATE_DEPUTIES_CAP_MESSAGE] as const +const stateDeputyContactSafeMessages = [ + STATE_DEPUTY_STAFF_MESSAGE, + STATE_DEPUTY_CONFLICT_MESSAGE, + STATE_DEPUTY_INVALID_CONTACT_MESSAGE, + CONTACT_PHONE_AMBIGUOUS_MESSAGE, + CONTACT_PHONE_CONFLICT_MESSAGE, +] as const + +const stateDeputyPartySafeMessages = [STATE_DEPUTY_STAFF_MESSAGE] as const + +/** Per-field Contact edit for B163 (lista + ficha). */ +export const updateStateDeputyContactFormAction = async ( + _state: CampaignFormActionState, + formData: FormData, +): Promise => + runCampaignFormAction({ + execute: async () => { + const id = requiredRelationshipFormValue(formData, 'stateDeputyId') + const field = requiredFormText(formData, 'field') + + if (field === 'name') { + await updateStateDeputyContact({ + id, + field: 'name', + name: requiredFormText(formData, 'name'), + }) + } else if (field === 'email') { + await updateStateDeputyContact({ + id, + field: 'email', + email: nullableFormText(formData, 'email') ?? undefined, + }) + } else if (field === 'phone') { + await updateStateDeputyContact({ + id, + field: 'phone', + phone: nullableFormText(formData, 'phone'), + }) + } else { + throw new Error(STATE_DEPUTY_INVALID_FIELD_MESSAGE) + } + + return { message: 'Salvo.' } + }, + safeMessages: stateDeputyContactSafeMessages, + genericMessage: 'Não foi possível salvar. Verifique os dados e tente novamente.', + }) + +/** Inline party edit for B163; notes are deliberately left untouched. */ +export const updateStateDeputyPartyFormAction = async ( + _state: CampaignFormActionState, + formData: FormData, +): Promise => + runCampaignFormAction({ + execute: async () => { + await updateStateDeputyParty({ + id: requiredRelationshipFormValue(formData, 'stateDeputyId'), + party: nullableFormText(formData, 'party'), + }) + return { message: 'Salvo.' } + }, + safeMessages: stateDeputyPartySafeMessages, + genericMessage: 'Não foi possível salvar o partido. Verifique os dados e tente novamente.', + }) + /** One chip toggle in the "Lideranças" column of `/campanha/dobradinhas` (B36). */ export const setLeadershipStateDeputyMembershipFormAction = async ( _state: CampaignFormActionState, diff --git a/src/app/(campaign)/campanha/(app)/dobradinhas/nova/formActions.ts b/src/app/(campaign)/campanha/(app)/dobradinhas/nova/formActions.ts index 59f8558f..06d143cd 100644 --- a/src/app/(campaign)/campanha/(app)/dobradinhas/nova/formActions.ts +++ b/src/app/(campaign)/campanha/(app)/dobradinhas/nova/formActions.ts @@ -22,7 +22,7 @@ export const createStateDeputyFormAction = async ( party: optionalFormText(formData, 'party'), notes: optionalFormText(formData, 'notes'), }), - redirectTo: (stateDeputy) => `/campanha/dobradinhas/${stateDeputy.slug}`, + redirectTo: (stateDeputy) => `/campanha/dobradinhas/${stateDeputy.id}`, safeMessages: [STATE_DEPUTY_CONFLICT_MESSAGE, STATE_DEPUTY_STAFF_MESSAGE], genericMessage: 'Não foi possível cadastrar a dobradinha. Verifique os dados e tente novamente.', diff --git a/src/app/(campaign)/campanha/(app)/dobradinhas/page.tsx b/src/app/(campaign)/campanha/(app)/dobradinhas/page.tsx index e27ea4f3..eab9ab55 100644 --- a/src/app/(campaign)/campanha/(app)/dobradinhas/page.tsx +++ b/src/app/(campaign)/campanha/(app)/dobradinhas/page.tsx @@ -5,6 +5,7 @@ import { redirect } from 'next/navigation' import { getPayload } from 'payload' import { CampaignColumnPickerTrailing } from '@/components/campaign/shared/CampaignColumnPickerTrailing' +import { CampaignInlineEditableCell } from '@/components/campaign/shared/CampaignInlineEditableCell' import { CampaignListFooter } from '@/components/campaign/shared/CampaignListFooter' import { CampaignListPendingBoundary, @@ -69,6 +70,8 @@ import { setLeadershipStateDeputyMembershipFormAction, setStateDeputyAdvisorMembershipFormAction, setStateDeputyMunicipalitiesFormAction, + updateStateDeputyContactFormAction, + updateStateDeputyPartyFormAction, } from './formActions' export const metadata = campaignPageMetadataFromCatalog('dobradinhas') @@ -118,12 +121,17 @@ const stateDeputyColumns = ( mandatory: true, head: , cell: (row) => ( - - {row.name} - + ), }, { @@ -137,8 +145,53 @@ const stateDeputyColumns = ( hasNoPartyOption={hasNoPartyOption} /> ), - cellClassName: 'text-muted-foreground', - cell: (row) => row.party ?? '—', + cellClassName: 'min-w-32', + cell: (row) => ( + + ), + }, + { + id: 'email', + label: 'E-mail', + cellClassName: 'max-w-56', + cell: (row) => ( + + ), + }, + { + id: 'phone', + label: 'Telefone', + cellClassName: 'min-w-40', + cell: (row) => ( + + ), }, { id: 'municipalities', diff --git a/src/app/(campaign)/campanha/(app)/liderancas/page.tsx b/src/app/(campaign)/campanha/(app)/liderancas/page.tsx index 4bd5950a..bc84be00 100644 --- a/src/app/(campaign)/campanha/(app)/liderancas/page.tsx +++ b/src/app/(campaign)/campanha/(app)/liderancas/page.tsx @@ -5,7 +5,6 @@ import { redirect } from 'next/navigation' import { getPayload } from 'payload' import { LeadershipInviteRowAction } from '@/components/campaign/invite/LeadershipInviteRowAction' -import { LeadershipContactFieldControl } from '@/components/campaign/leadership/LeadershipContactFieldControl' import { LeadershipFilters } from '@/components/campaign/leadership/LeadershipFilters' import { LeadershipListSupportStatusControl } from '@/components/campaign/leadership/LeadershipListSupportStatusControl' import { @@ -13,6 +12,7 @@ import { LeadershipSortableHead, } from '@/components/campaign/leadership/LeadershipSortableHead' import { CampaignColumnPickerTrailing } from '@/components/campaign/shared/CampaignColumnPickerTrailing' +import { CampaignInlineEditableCell } from '@/components/campaign/shared/CampaignInlineEditableCell' import { CampaignListFooter } from '@/components/campaign/shared/CampaignListFooter' import { CampaignListPendingBoundary, @@ -81,6 +81,7 @@ import { loadStateDeputySummaries } from '@/utilities/stateDeputyData' import { setLeadershipMunicipalitiesFormAction, setLeadershipStateDeputyMembershipFormAction, + updateLeadershipContactFormAction, } from './formActions' export const metadata = campaignPageMetadataFromCatalog('liderancas') @@ -144,7 +145,15 @@ const leadershipColumns = ({ mandatory: true, head: , cell: (row) => ( - + ), }, { @@ -152,14 +161,30 @@ const leadershipColumns = ({ label: 'E-mail', cellClassName: 'max-w-56', cell: (row) => ( - + ), }, { id: 'phone', label: 'Celular', cell: (row) => ( - + ), }, { @@ -224,7 +249,7 @@ const leadershipColumns = ({ items={row.stateDeputies.map((deputy) => ({ id: deputy.id, label: deputy.name, - href: `/campanha/dobradinhas/${deputy.slug}`, + href: `/campanha/dobradinhas/${deputy.id}`, ...(deputy.party ? { party: deputy.party } : {}), }))} options={stateDeputyOptions} @@ -380,7 +405,7 @@ export default async function LeadershipsPage({ searchParams }: LeadershipsPageP item: { id: option.id, label: option.plainName, - href: `/campanha/dobradinhas/${option.slug}`, + href: `/campanha/dobradinhas/${option.id}`, ...(option.party ? { party: option.party } : {}), }, })), diff --git a/src/app/(campaign)/campanha/actions/leadership.ts b/src/app/(campaign)/campanha/actions/leadership.ts index 66e4072a..b3d494db 100644 --- a/src/app/(campaign)/campanha/actions/leadership.ts +++ b/src/app/(campaign)/campanha/actions/leadership.ts @@ -32,7 +32,7 @@ import { getAdvisorMunicipalityIds } from '@/utilities/campaignAccess' import { getCampaignActionContext, reloadStaffActor } from '@/utilities/campaignActionContext' import { acquireContactPhoneLocks, - assertContactPhoneAvailable, + assertContactPhoneWritable, CONTACT_PHONE_AMBIGUOUS_MESSAGE, } from '@/utilities/contactPhoneInvariant' import { loadMunicipalityLeadershipSummaries } from '@/utilities/municipality/municipalityViewModels' @@ -49,40 +49,6 @@ const getFreshStaffActor = ( req?: PayloadTransactionRequest, ): Promise => reloadStaffActor(payload, actor, LEADERSHIP_STAFF_MESSAGE, req) -const assertLeadershipContactPhoneWritable = async ( - payload: Payload, - req: PayloadTransactionRequest, - contactID: number, - phone: string, -) => { - if (payload.db.name !== 'postgres') { - throw new Error(POSTGRES_DEDUP_LOCK_MESSAGE) - } - - await acquireContactPhoneLocks(payload, req, [phone]) - - // Intentional admin bypass: staff scope was checked on the leadership; phone - // uniqueness must see every contact with the number, not only visible rows. - const contactsWithPhone = await payload.find({ - collection: 'contact', - where: { phone: { equals: phone } }, - depth: 0, - limit: 2, - pagination: false, - overrideAccess: true, - req, - }) - - if (contactsWithPhone.totalDocs > 1) { - throw new Error(CONTACT_PHONE_AMBIGUOUS_MESSAGE) - } - - const phoneOwner = contactsWithPhone.docs[0] - if (phoneOwner && phoneOwner.id !== contactID) { - await assertContactPhoneAvailable(payload, req, phone, contactID) - } -} - /** Advisors may only link leaderships to municipalities they administer. */ const assertMunicipalitiesWithinScope = async ( payload: Payload, @@ -318,7 +284,7 @@ export const updateLeadershipContactRecord = async ( contactData.email = data.email ?? null } else if (data.field === 'phone') { if (data.phone) { - await assertLeadershipContactPhoneWritable(payload, req, contactID, data.phone) + await assertContactPhoneWritable(payload, req, contactID, data.phone) contactData.phone = data.phone } else { contactData.phone = null @@ -387,7 +353,7 @@ const updateLeadershipWizardRecord = async ( throw new Error(LEADERSHIP_INVALID_CONTACT_MESSAGE) } - await assertLeadershipContactPhoneWritable(payload, req, contactID, data.phone) + await assertContactPhoneWritable(payload, req, contactID, data.phone) // bypass: contact write is staff-scoped via leadership access check above. await payload.update({ @@ -556,11 +522,11 @@ export const setMunicipalityLeadershipMembership = async (input: { * `/campanha/liderancas` — the list the chip was toggled ON — is deliberately * NOT here: see `revalidateLeadershipMunicipalityPaths` below for the reason. */ -const revalidateLeadershipStateDeputyPaths = (leadershipId: number, stateDeputySlug?: string) => { +const revalidateLeadershipStateDeputyPaths = (leadershipId: number, stateDeputyID?: number) => { revalidatePath(`/campanha/liderancas/${leadershipId}`, 'page') revalidatePath('/campanha/dobradinhas', 'page') - if (stateDeputySlug) { - revalidatePath(`/campanha/dobradinhas/${stateDeputySlug}`, 'page') + if (stateDeputyID) { + revalidatePath(`/campanha/dobradinhas/${stateDeputyID}`, 'page') } } @@ -578,7 +544,7 @@ const LEADERSHIP_RELATION_LOCK_KEYS = { * The pairing is the point: relation and lock key used to be two independent * string literals a few lines apart, so a third relation could quietly take a * lock that guards a different column. The rest of the two delta writes — the - * delta shape, the scope assertion, the slug lookup, the return — is genuinely + * delta shape, the scope assertion, the return — is genuinely * different in each, and hoisting it would cost six callbacks to share ~24 lines. */ const openLeadershipForRelationDelta = async < @@ -642,26 +608,11 @@ export const setLeadershipStateDeputyMembershipRecord = async ( assigned, ) - // No-op: nothing to write, and nothing for the caller to revalidate — - // skip the slug lookup below, which exists only to target that revalidate. + // No-op: nothing to write, and nothing for the caller to revalidate. if (nextStateDeputyIDs === null) { - return { leadership: current, stateDeputySlug: undefined } + return { leadership: current, stateDeputyID: undefined } } - // Intentional admin bypass: only used to resolve the slug of the - // touched deputy for a targeted revalidate; existence is otherwise - // enforced by Payload's relationship validation on `update`. - const stateDeputySlug = ( - await payload.findByID({ - collection: 'stateDeputy', - id: stateDeputyId, - depth: 0, - select: { slug: true }, - overrideAccess: true, - req, - }) - ).slug - const updated = await payload.update({ collection: 'leadership', id: leadershipId, @@ -672,7 +623,7 @@ export const setLeadershipStateDeputyMembershipRecord = async ( req, }) - return { leadership: updated, stateDeputySlug } + return { leadership: updated, stateDeputyID: stateDeputyId } }, { beginFailureMessage: 'Não foi possível atualizar as dobradinhas.' }, ) @@ -682,13 +633,13 @@ export const setLeadershipStateDeputyMembership = async ( input: LeadershipStateDeputyMembershipInput, ) => { const { payload, actor } = await getCampaignActionContext() - const { leadership, stateDeputySlug } = await setLeadershipStateDeputyMembershipRecord( + const { leadership, stateDeputyID } = await setLeadershipStateDeputyMembershipRecord( payload, actor, input, ) // No-op writes nothing, so there is nothing to revalidate. - if (stateDeputySlug) revalidateLeadershipStateDeputyPaths(input.leadershipId, stateDeputySlug) + if (stateDeputyID) revalidateLeadershipStateDeputyPaths(input.leadershipId, stateDeputyID) return leadership } @@ -749,8 +700,7 @@ export const setLeadershipMunicipalitiesMembershipRecord = async ( assigned, ) - // No-op: nothing to write, and nothing for the caller to revalidate — - // skip the slug lookup below, which exists only to target that revalidate. + // No-op: nothing to write, and nothing for the caller to revalidate. if (change === null) { return { leadership: current, municipalitySlugs: [] } } diff --git a/src/app/(campaign)/campanha/actions/stateDeputy.ts b/src/app/(campaign)/campanha/actions/stateDeputy.ts index a5b2e207..64f41ba8 100644 --- a/src/app/(campaign)/campanha/actions/stateDeputy.ts +++ b/src/app/(campaign)/campanha/actions/stateDeputy.ts @@ -4,24 +4,28 @@ import { revalidatePath } from 'next/cache' import type { Payload } from 'payload' import { nextStateDeputyIdsAfterMunicipalityMembership } from '@/lib/municipalityStateDeputyMembership' -import { uniqueRelationshipIds } from '@/lib/relationship' +import { relationshipId, uniqueRelationshipIds } from '@/lib/relationship' +import { contactFieldUpdateSchema, type ContactFieldUpdateInput } from '@/lib/schemas/contact' import { STATE_DEPUTY_ADVISORS_UNRESTRICTED_MESSAGE, STATE_DEPUTY_CONFLICT_MESSAGE, + STATE_DEPUTY_INVALID_CONTACT_MESSAGE, STATE_DEPUTY_STAFF_MESSAGE, municipalityStateDeputyCreateSchema, stateDeputyAdvisorMembershipSchema, stateDeputyCreateSchema, stateDeputyMunicipalitiesBatchSchema, + stateDeputyPartyUpdateSchema, stateDeputyUpdateSchema, type MunicipalityStateDeputyCreateInput, type StateDeputyAdvisorMembershipInput, type StateDeputyCreateInput, type StateDeputyMunicipalitiesBatchInput, + type StateDeputyPartyUpdateInput, type StateDeputyUpdateInput, } from '@/lib/schemas/stateDeputy' import { nextStateDeputyAdvisorIdsAfterMembership } from '@/lib/stateDeputyAdvisorMembership' -import type { CampaignUser } from '@/payload-types' +import type { CampaignUser, Contact } from '@/payload-types' import { getCampaignActionContext, reloadStaffActor, @@ -32,32 +36,84 @@ import { runStaffEntityMutation, type StaffEntityPolicy, } from '@/utilities/campaignEntityActions' +import { assertContactPhoneWritable } from '@/utilities/contactPhoneInvariant' import { hookFilledCreateData } from '@/utilities/hookFilledData' import { revalidateMunicipalityListPaths } from '@/utilities/municipality/municipalityRevalidation' import { withPayloadTransaction } from '@/utilities/payloadTransaction' import { acquireTextAdvisoryLocks } from '@/utilities/postgresTransactionLocks' +import { assertStateDeputyNameAvailable } from '@/utilities/stateDeputy/nameInvariant' const stateDeputyPolicy: StaffEntityPolicy = { staffMessage: STATE_DEPUTY_STAFF_MESSAGE, - conflictPattern: /state_deputy_(name|slug)|duplicate key/i, + conflictPattern: /state_deputy_(contact|slug)|duplicate key/i, conflictMessage: STATE_DEPUTY_CONFLICT_MESSAGE, } +type StateDeputyCreationData = { + name: string + party?: string | null + notes?: string | null +} + +const createStateDeputyWithContact = async ( + payload: Payload, + actor: CampaignUser, + data: StateDeputyCreationData, + req: { transactionID: number | string }, +) => { + // Intentional admin bypass: Contact is admin-managed, while this staff action + // owns the atomic Contact ↔ StateDeputy creation. + await assertStateDeputyNameAvailable(payload, req, data.name) + + const contact = await payload.create({ + collection: 'contact', + data: { + name: data.name, + email: null, + phone: null, + state: 'BA' as Contact['state'], + city: null, + }, + depth: 0, + // Intentional admin bypass: this Contact write is owned by the atomic staff action. + overrideAccess: true, + req, + }) + + return payload.create({ + collection: 'stateDeputy', + data: hookFilledCreateData<'stateDeputy'>({ + contact: contact.id, + ...(data.party === undefined ? {} : { party: data.party }), + ...(data.notes === undefined ? {} : { notes: data.notes }), + }), + depth: 0, + user: actor, + overrideAccess: false, + req, + }) +} + const createStateDeputyRecord = async ( payload: Payload, actor: CampaignUser, input: StateDeputyCreateInput, ) => { const data = stateDeputyCreateSchema.parse(input) - return runStaffEntityMutation(payload, actor, stateDeputyPolicy, (currentActor) => - payload.create({ - collection: 'stateDeputy', - data: hookFilledCreateData<'stateDeputy'>(data), - depth: 0, - user: currentActor, - overrideAccess: false, - }), - ) + try { + return await withPayloadTransaction( + payload, + async ({ req }) => { + const currentActor = await reloadStaffActor(payload, actor, STATE_DEPUTY_STAFF_MESSAGE, req) + return createStateDeputyWithContact(payload, currentActor, data, req) + }, + { beginFailureMessage: 'Não foi possível iniciar a transação da dobradinha.' }, + ) + } catch (error) { + const conflict = mapStaffEntityConflict(error, stateDeputyPolicy) + if (conflict) throw conflict + throw error + } } const updateStateDeputyRecord = async ( @@ -78,14 +134,111 @@ const updateStateDeputyRecord = async ( ) } +export const updateStateDeputyPartyRecord = async ( + payload: Payload, + actor: CampaignUser, + input: StateDeputyPartyUpdateInput, +) => { + const { id, party } = stateDeputyPartyUpdateSchema.parse(input) + return runStaffEntityMutation(payload, actor, stateDeputyPolicy, (currentActor) => + payload.update({ + collection: 'stateDeputy', + id, + data: { party }, + depth: 0, + user: currentActor, + overrideAccess: false, + }), + ) +} + +export const updateStateDeputyContactRecord = async ( + payload: Payload, + actor: CampaignUser, + input: ContactFieldUpdateInput, +) => { + const data = contactFieldUpdateSchema.parse(input) + + return withPayloadTransaction( + payload, + async ({ req }) => { + const currentActor = await reloadStaffActor(payload, actor, STATE_DEPUTY_STAFF_MESSAGE, req) + const current = await payload.findByID({ + collection: 'stateDeputy', + id: data.id, + depth: 0, + select: { contact: true }, + user: currentActor, + overrideAccess: false, + req, + }) + + const contactID = relationshipId(current.contact) + if (contactID === null) throw new Error(STATE_DEPUTY_INVALID_CONTACT_MESSAGE) + + const contactData: Partial> = {} + if (data.field === 'name') { + await assertStateDeputyNameAvailable(payload, req, data.name, data.id) + contactData.name = data.name + } else if (data.field === 'email') { + contactData.email = data.email ?? null + } else if (data.field === 'phone') { + if (data.phone) { + await assertContactPhoneWritable(payload, req, contactID, data.phone) + contactData.phone = data.phone + } else { + contactData.phone = null + } + } + + // Bypass: the StateDeputy row access above established staff scope. + await payload.update({ + collection: 'contact', + id: contactID, + data: contactData, + depth: 0, + overrideAccess: true, + req, + }) + + return current + }, + { beginFailureMessage: 'Não foi possível atualizar o contato da dobradinha.' }, + ) +} + export const createStateDeputy = async (input: StateDeputyCreateInput) => { const { payload, actor } = await getCampaignActionContext() return createStateDeputyRecord(payload, actor, input) } +const revalidateStateDeputyPaths = (stateDeputyID: number) => { + revalidatePath('/campanha/dobradinhas', 'page') + revalidatePath(`/campanha/dobradinhas/${stateDeputyID}`, 'page') + revalidatePath('/campanha/liderancas', 'page') + revalidatePath('/campanha/liderancas/[id]', 'page') + revalidateMunicipalityListPaths({ scope: 'both' }) +} + export const updateStateDeputy = async (input: StateDeputyUpdateInput) => { const { payload, actor } = await getCampaignActionContext() - return updateStateDeputyRecord(payload, actor, input) + const updated = await updateStateDeputyRecord(payload, actor, input) + revalidateStateDeputyPaths(updated.id) + return updated +} + +export const updateStateDeputyParty = async (input: StateDeputyPartyUpdateInput) => { + const { payload, actor } = await getCampaignActionContext() + const updated = await updateStateDeputyPartyRecord(payload, actor, input) + revalidateStateDeputyPaths(updated.id) + return updated +} + +export const updateStateDeputyContact = async (input: ContactFieldUpdateInput) => { + const { payload, actor } = await getCampaignActionContext() + const updated = await updateStateDeputyContactRecord(payload, actor, input) + revalidateStateDeputyPaths(updated.id) + return updated } /** @@ -95,11 +248,11 @@ export const updateStateDeputy = async (input: StateDeputyUpdateInput) => { * and the município index for a change already on screen. */ const revalidateStateDeputyMunicipalityPaths = ( - stateDeputySlug: string | undefined, + stateDeputyID: number | undefined, municipalitySlugs: readonly string[], ) => { - if (stateDeputySlug) { - revalidatePath(`/campanha/dobradinhas/${stateDeputySlug}`, 'page') + if (stateDeputyID) { + revalidatePath(`/campanha/dobradinhas/${stateDeputyID}`, 'page') } revalidateMunicipalityListPaths({ scope: 'list' }) for (const slug of municipalitySlugs) { @@ -139,7 +292,7 @@ export const setStateDeputyAdvisorMembershipRecord = async ( collection: 'stateDeputy', id: stateDeputyId, depth: 0, - select: { advisors: true, slug: true }, + select: { advisors: true }, // Intentional admin bypass: the unrestricted role was verified above; // the `advisors` field's update access is admin-only in the collection // config, same contract as `setAdvisorMunicipalitiesBatchRecord`. The @@ -155,10 +308,9 @@ export const setStateDeputyAdvisorMembershipRecord = async ( assigned, ) - // No-op: nothing to write, and nothing for the caller to revalidate — - // the slug lookup below exists only to target that revalidate. + // No-op: nothing to write, and nothing for the caller to revalidate. if (nextAdvisorIDs === null) { - return { stateDeputySlug: undefined } + return { stateDeputyID: undefined } } // Same intentional bypass as the read: the unrestricted role was verified @@ -172,7 +324,7 @@ export const setStateDeputyAdvisorMembershipRecord = async ( req, }) - return { stateDeputySlug: stateDeputy.slug } + return { stateDeputyID: stateDeputyId } }, { beginFailureMessage: 'Não foi possível atualizar os assessores da dobradinha.' }, ) @@ -180,12 +332,12 @@ export const setStateDeputyAdvisorMembershipRecord = async ( export const setStateDeputyAdvisorMembership = async (input: StateDeputyAdvisorMembershipInput) => { const { payload, actor } = await getCampaignActionContext() - const { stateDeputySlug } = await setStateDeputyAdvisorMembershipRecord(payload, actor, input) + const { stateDeputyID } = await setStateDeputyAdvisorMembershipRecord(payload, actor, input) // The list is deliberately absent: the chip cell that calls this IS on it and // already shows the toggle. The detail page is the route an actor cannot see // from here — same reasoning as the leadership twin (B31). - if (stateDeputySlug) revalidatePath(`/campanha/dobradinhas/${stateDeputySlug}`, 'page') - return stateDeputySlug + if (stateDeputyID) revalidatePath(`/campanha/dobradinhas/${stateDeputyID}`, 'page') + return stateDeputyID } export const setStateDeputyMunicipalitiesBatchRecord = async ( @@ -251,28 +403,10 @@ export const setStateDeputyMunicipalitiesBatchRecord = async ( if (typeof municipality.slug === 'string') changedSlugs.push(municipality.slug) } - // Resolved AFTER the loop, and only when something changed: the slug - // exists solely to target the revalidate, which the caller skips on a - // no-op batch — so on a no-op this read was pure waste (the guard the - // `setLeadershipMunicipalitiesMembershipRecord` twin already had). - // Intentional admin bypass: existence is otherwise enforced by Payload's - // relationship validation on `update` (precedent: - // `setLeadershipStateDeputyMembershipRecord`, B31). - const stateDeputySlug = - changedSlugs.length > 0 - ? ( - await payload.findByID({ - collection: 'stateDeputy', - id: stateDeputyId, - depth: 0, - select: { slug: true }, - overrideAccess: true, - req, - }) - ).slug - : undefined - - return { stateDeputySlug, slugs: changedSlugs } + return { + stateDeputyID: changedSlugs.length > 0 ? stateDeputyId : undefined, + slugs: changedSlugs, + } }, { beginFailureMessage: 'Não foi possível atualizar os municípios da dobradinha.' }, ) @@ -284,7 +418,7 @@ export const setStateDeputyMunicipalitiesBatch = async ( const { payload, actor } = await getCampaignActionContext() const result = await setStateDeputyMunicipalitiesBatchRecord(payload, actor, input) if (result.slugs.length > 0) { - revalidateStateDeputyMunicipalityPaths(result.stateDeputySlug, result.slugs) + revalidateStateDeputyMunicipalityPaths(result.stateDeputyID, result.slugs) } return result } @@ -295,7 +429,7 @@ export const setStateDeputyMunicipalitiesBatch = async ( * and assigns it to the município in the SAME transaction: if the assign * fails (out-of-scope município, cap), the create rolls back with it — no * orphan, same contract as `createMunicipalityAdvisorRecord` (B154). The - * unique `name`/`slug` violation is mapped to the safe conflict message; the + * unique `slug` violation is mapped to the safe conflict message; the * município write re-checks `canUpdateMunicipality` through the same * `user`-threaded read as the B37 batch. */ @@ -317,19 +451,12 @@ export const createMunicipalityStateDeputyRecord = async ( let created try { - created = await payload.create({ - collection: 'stateDeputy', - data: hookFilledCreateData<'stateDeputy'>({ name, ...(party ? { party } : {}) }), - depth: 0, - user: currentActor, - overrideAccess: false, - req, - }) + created = await createStateDeputyWithContact(payload, currentActor, { name, party }, req) } catch (error) { // Same translation as the shared owner (`runStaffEntityMutation`): the // pattern covers the insert race, `ValidationError` covers Payload's // pre-insert unique check — in this create path the only validation - // left after zod IS the unique name/slug check. + // left after zod IS the unique slug check. const conflict = mapStaffEntityConflict(error, stateDeputyPolicy) if (conflict) throw conflict throw error @@ -367,7 +494,7 @@ export const createMunicipalityStateDeputyRecord = async ( return { stateDeputy: { id: created.id, - name: created.name, + name, slug: created.slug, party: created.party ?? null, }, @@ -382,7 +509,7 @@ export const createMunicipalityStateDeputy = async (input: MunicipalityStateDepu const { payload, actor } = await getCampaignActionContext() const result = await createMunicipalityStateDeputyRecord(payload, actor, input) if (result.municipalitySlug) { - revalidateStateDeputyMunicipalityPaths(result.stateDeputy.slug, [result.municipalitySlug]) + revalidateStateDeputyMunicipalityPaths(result.stateDeputy.id, [result.municipalitySlug]) } return result } diff --git a/src/collections/Contact.ts b/src/collections/Contact.ts index 387f5c06..fd61a1c9 100644 --- a/src/collections/Contact.ts +++ b/src/collections/Contact.ts @@ -1,10 +1,12 @@ import { CitiesByState } from '@/lib/cities' import { BRAZILIAN_PHONE_INVALID_MESSAGE, normalizeBrazilianPhone } from '@/lib/phone' +import { trimmedText } from '@/lib/text' import { canManageContacts, canReadContacts } from '@/utilities/campaignAccess' import { acquireContactPhoneLocks, assertContactPhoneAvailable, } from '@/utilities/contactPhoneInvariant' +import { assertStateDeputyNameAvailable } from '@/utilities/stateDeputy/nameInvariant' import type { CollectionBeforeChangeHook, CollectionConfig } from 'payload' import { APIError } from 'payload' @@ -48,6 +50,38 @@ const enforceUniqueContactPhone: CollectionBeforeChangeHook = async ({ return data } +const enforceStateDeputyName: CollectionBeforeChangeHook = async ({ + data, + operation, + originalDoc, + req, +}) => { + if (operation !== 'update' || !data || data.name === undefined || !originalDoc) return data + + const currentName = trimmedText(originalDoc.name) + const nextName = trimmedText(data.name) + if (nextName === currentName) return data + + const linkedDeputy = await req.payload.find({ + collection: 'stateDeputy', + where: { contact: { equals: originalDoc.id } }, + depth: 0, + limit: 1, + pagination: false, + // Intentional bypass: Contact access is broader than the deputy's scope; this + // lookup only determines whether the name invariant applies. + overrideAccess: true, + req, + }) + + const stateDeputyID = linkedDeputy.docs[0]?.id + if (stateDeputyID !== undefined) { + await assertStateDeputyNameAvailable(req.payload, req, nextName, stateDeputyID) + } + + return data +} + export const Contact: CollectionConfig = { slug: 'contact', labels: { @@ -91,7 +125,7 @@ export const Contact: CollectionConfig = { return data }, ], - beforeChange: [enforceUniqueContactPhone], + beforeChange: [enforceUniqueContactPhone, enforceStateDeputyName], }, fields: [ { diff --git a/src/collections/StateDeputy.ts b/src/collections/StateDeputy.ts index dc022607..fe7e28f1 100644 --- a/src/collections/StateDeputy.ts +++ b/src/collections/StateDeputy.ts @@ -1,7 +1,7 @@ import type { CollectionBeforeValidateHook, CollectionConfig } from 'payload' import { APIError } from 'payload' -import { uniqueRelationshipIds } from '@/lib/relationship' +import { relationshipId, uniqueRelationshipIds } from '@/lib/relationship' import { STATE_DEPUTY_NAME_REQUIRED_MESSAGE } from '@/lib/schemas/stateDeputy' import { slugify } from '@/lib/slug' import { trimmedText } from '@/lib/text' @@ -16,24 +16,63 @@ import { eligibleCampaignStaffWhere, } from '@/utilities/campaignAccess' import { stampCampaignCreatedBy, systemStampedActorField } from '@/utilities/campaignAuditFields' +import { assertStateDeputyNameAvailable } from '@/utilities/stateDeputy/nameInvariant' -const setCanonicalStateDeputySlug: CollectionBeforeValidateHook = ({ +const setCanonicalStateDeputySlug: CollectionBeforeValidateHook = async ({ data, operation, originalDoc, + req, }) => { if (!data) return data - const name = trimmedText(data.name ?? originalDoc?.name) - const slug = slugify(name) - if (!slug) { - throw new APIError(STATE_DEPUTY_NAME_REQUIRED_MESSAGE, 400) + + if (operation === 'update') { + const contactID = relationshipId(data.contact ?? originalDoc?.contact) + if (contactID !== null) { + const contact = await req.payload.findByID({ + collection: 'contact', + id: contactID, + depth: 0, + select: { name: true }, + // Intentional bypass: this read derives the invariant name after the + // authorized StateDeputy relation update. + overrideAccess: true, + req, + }) + await assertStateDeputyNameAvailable(req.payload, req, contact.name, originalDoc?.id) + } + data.slug = originalDoc?.slug ?? data.slug + return data } - if (operation === 'update' && data.name !== undefined && name !== originalDoc?.name) { - throw new APIError('O nome da dobradinha não pode ser alterado após a criação.', 409) + + const contactID = relationshipId(data.contact) + if (contactID === null) { + throw new APIError('O contato da dobradinha é obrigatório.', 400) } - data.name = name - data.slug = operation === 'create' ? slug : originalDoc?.slug - return data + + return req.payload + .findByID({ + collection: 'contact', + id: contactID, + depth: 0, + select: { name: true }, + // Intentional bypass: the create action has already authorized the staff + // actor; this lookup only derives the immutable legacy alias from Contact. + overrideAccess: true, + req, + }) + .then((contact) => { + const name = trimmedText(contact.name) + const slug = slugify(name) + if (!slug) { + throw new APIError(STATE_DEPUTY_NAME_REQUIRED_MESSAGE, 400) + } + + return assertStateDeputyNameAvailable(req.payload, req, name).then(() => { + data.slug = slug + return data + }) + }) } const validateStateDeputyAdvisors: CollectionBeforeValidateHook = async ({ data, req }) => { @@ -75,8 +114,8 @@ export const StateDeputy: CollectionConfig = { }, admin: { group: 'Campanha', - useAsTitle: 'name', - defaultColumns: ['name', 'party', 'updatedAt'], + useAsTitle: 'contact', + defaultColumns: ['contact', 'party', 'updatedAt'], description: 'Deputados estaduais com quem a campanha dobra. Vincule a municípios e lideranças nas fichas correspondentes.', }, @@ -92,14 +131,16 @@ export const StateDeputy: CollectionConfig = { }, fields: [ { - name: 'name', - type: 'text', - label: 'Nome', + name: 'contact', + type: 'relationship', + relationTo: 'contact', + label: 'Contato', required: true, - minLength: 2, - maxLength: 160, unique: true, index: true, + access: { + update: canSetCampaignSystemField, + }, }, { name: 'slug', diff --git a/src/components/campaign/dashboard/HomeSearchStateDeputyGroup.tsx b/src/components/campaign/dashboard/HomeSearchStateDeputyGroup.tsx index 49768053..83ae64a7 100644 --- a/src/components/campaign/dashboard/HomeSearchStateDeputyGroup.tsx +++ b/src/components/campaign/dashboard/HomeSearchStateDeputyGroup.tsx @@ -37,9 +37,9 @@ export const HomeSearchStateDeputyGroup = () => {

Dobradinhas

    {visibleStateDeputies.map((hit) => ( -
  • +
  • diff --git a/src/components/campaign/leadership/LeadershipContactFieldControl.tsx b/src/components/campaign/leadership/LeadershipContactFieldControl.tsx deleted file mode 100644 index aab5660e..00000000 --- a/src/components/campaign/leadership/LeadershipContactFieldControl.tsx +++ /dev/null @@ -1,202 +0,0 @@ -'use client' - -import { PencilIcon } from 'lucide-react' -import Link from 'next/link' -import { useRouter } from 'next/navigation' -import { useEffect, useRef, useState, type ChangeEvent } from 'react' -import { toast } from 'sonner' - -import { updateLeadershipContactFormAction } from '@/app/(campaign)/campanha/(app)/liderancas/formActions' -import { - CampaignCopyableCell, - campaignReadCellClassName, -} from '@/components/campaign/shared/CampaignCopyableCell' -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { Spinner } from '@/components/ui/Spinner' -import { campaignInlineInputClassName } from '@/lib/campaignInlineInput' -import { formatBrazilianPhoneInput, sanitizeBrazilianPhoneInput } from '@/lib/phone' -import { cn } from '@/lib/utils' -import type { CampaignFormActionState } from '@/utilities/campaignFormActionError' - -const SAVE_DEBOUNCE_MS = 500 - -type LeadershipContactField = 'name' | 'email' | 'phone' - -type LeadershipContactFieldControlProps = { - leadershipId: number - field: LeadershipContactField - value: string | null - className?: string -} - -const fieldLabels: Record = { - name: 'Nome', - email: 'E-mail', - phone: 'Celular', -} - -const inputTypeForField = (field: LeadershipContactField): 'text' | 'email' | 'tel' => { - if (field === 'email') return 'email' - if (field === 'phone') return 'tel' - return 'text' -} - -export const LeadershipContactFieldControl = ({ - leadershipId, - field, - value, - className, -}: LeadershipContactFieldControlProps) => { - const label = fieldLabels[field] - const phoneDisplayValue = - field === 'phone' && value ? formatBrazilianPhoneInput(value) : undefined - const router = useRouter() - const [isEditing, setIsEditing] = useState(false) - const [draft, setDraft] = useState(value ?? '') - const [isPending, setIsPending] = useState(false) - const lastSaved = useRef((value ?? '').trim()) - const timerRef = useRef | null>(null) - const requestId = useRef(0) - const inputRef = useRef(null) - - useEffect(() => { - if (inputRef.current && document.activeElement === inputRef.current) return - const next = value ?? '' - setDraft(next) - lastSaved.current = next.trim() - }, [value, leadershipId, field]) - - useEffect(() => { - if (!isEditing) return - inputRef.current?.focus() - inputRef.current?.select() - }, [isEditing]) - - useEffect(() => { - return () => { - if (timerRef.current) clearTimeout(timerRef.current) - } - }, []) - - const save = (nextRaw: string) => { - const displayNext = nextRaw.trim() - if (displayNext === lastSaved.current) { - setIsEditing(false) - return - } - - const formData = new FormData() - formData.set('leadershipId', String(leadershipId)) - formData.set('field', field) - formData.set(field, nextRaw) - - const id = ++requestId.current - setIsPending(true) - void updateLeadershipContactFormAction({}, formData).then((result: CampaignFormActionState) => { - if (id !== requestId.current) return - setIsPending(false) - if (result.status === 'success') { - lastSaved.current = displayNext - setIsEditing(false) - router.refresh() - return - } - toast.error(result.message ?? 'Não foi possível salvar.') - }) - } - - const scheduleSave = (nextRaw: string) => { - if (timerRef.current) clearTimeout(timerRef.current) - timerRef.current = setTimeout(() => { - timerRef.current = null - save(nextRaw) - }, SAVE_DEBOUNCE_MS) - } - - const handleBlur = () => { - if (timerRef.current) { - clearTimeout(timerRef.current) - timerRef.current = null - } - save(draft) - } - - const handleChange = (event: ChangeEvent) => { - const next = - field === 'phone' - ? formatBrazilianPhoneInput(sanitizeBrazilianPhoneInput(event.currentTarget.value)) - : event.currentTarget.value - setDraft(next) - scheduleSave(next) - } - - if (isEditing) { - return ( -
    - { - if (event.key === 'Escape') { - event.preventDefault() - setDraft(value ?? '') - setIsEditing(false) - } - }} - {...(field === 'phone' ? { inputMode: 'numeric' as const } : {})} - /> - {isPending ? ( - - ) : null} -
    - ) - } - - return ( -
    -
    - {field === 'name' ? ( - value ? ( - - {value} - - ) : ( - - — - - ) - ) : ( - - )} -
    - -
    - ) -} diff --git a/src/components/campaign/leadership/LeadershipContactSection.tsx b/src/components/campaign/leadership/LeadershipContactSection.tsx index 24b9ff08..232ac990 100644 --- a/src/components/campaign/leadership/LeadershipContactSection.tsx +++ b/src/components/campaign/leadership/LeadershipContactSection.tsx @@ -1,6 +1,7 @@ 'use client' -import { LeadershipContactFieldControl } from '@/components/campaign/leadership/LeadershipContactFieldControl' +import { updateLeadershipContactFormAction } from '@/app/(campaign)/campanha/(app)/liderancas/formActions' +import { CampaignInlineEditableCell } from '@/components/campaign/shared/CampaignInlineEditableCell' type LeadershipContactSectionProps = { leadershipId: number @@ -23,19 +24,43 @@ export const LeadershipContactSection = ({
    Nome
    - +
    E-mail
    - +
    Celular
    - +
    diff --git a/src/components/campaign/shared/CampaignInlineEditableCell.tsx b/src/components/campaign/shared/CampaignInlineEditableCell.tsx new file mode 100644 index 00000000..9d23ee13 --- /dev/null +++ b/src/components/campaign/shared/CampaignInlineEditableCell.tsx @@ -0,0 +1,309 @@ +'use client' + +import { PencilIcon } from 'lucide-react' +import Link from 'next/link' +import { useRouter } from 'next/navigation' +import { useEffect, useRef, useState, type ChangeEvent, type KeyboardEvent } from 'react' +import { toast } from 'sonner' + +import { + CampaignCopyableCell, + campaignReadCellClassName, +} from '@/components/campaign/shared/CampaignCopyableCell' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Spinner } from '@/components/ui/Spinner' +import { campaignInlineInputClassName } from '@/lib/campaignInlineInput' +import { formatBrazilianPhoneInput, sanitizeBrazilianPhoneInput } from '@/lib/phone' +import { cn } from '@/lib/utils' +import type { CampaignFormActionState } from '@/utilities/campaignFormActionError' + +const SAVE_DEBOUNCE_MS = 500 + +export type CampaignInlineEditableField = 'name' | 'email' | 'phone' | 'party' + +type CampaignInlineEditableCellProps = { + recordId: number + recordIdField: string + field: CampaignInlineEditableField + value: string | null + label: string + formAction: ( + state: CampaignFormActionState, + formData: FormData, + ) => Promise + href?: string + editTrigger?: 'pencil' | 'cell' + readBehavior?: 'text' | 'copy' + saveOnChange?: boolean + className?: string +} + +const inputTypeForField = (field: CampaignInlineEditableField): 'text' | 'email' | 'tel' => { + if (field === 'email') return 'email' + if (field === 'phone') return 'tel' + return 'text' +} + +export const CampaignInlineEditableCell = ({ + recordId, + recordIdField, + field, + value, + label, + formAction, + href, + editTrigger = 'pencil', + readBehavior = 'text', + saveOnChange = true, + className, +}: CampaignInlineEditableCellProps) => { + const router = useRouter() + const [isEditing, setIsEditing] = useState(false) + const [draft, setDraft] = useState(value ?? '') + const [isPending, setIsPending] = useState(false) + const [errorMessage, setErrorMessage] = useState(null) + const [showSavedFeedback, setShowSavedFeedback] = useState(false) + const lastSaved = useRef((value ?? '').trim()) + const timerRef = useRef | null>(null) + const savedFeedbackTimerRef = useRef | null>(null) + const requestId = useRef(0) + const inputRef = useRef(null) + + useEffect(() => { + if (inputRef.current && document.activeElement === inputRef.current) return + const next = value ?? '' + setDraft(next) + lastSaved.current = next.trim() + }, [field, recordId, value]) + + useEffect(() => { + if (!isEditing) return + inputRef.current?.focus() + inputRef.current?.select() + }, [isEditing]) + + useEffect(() => { + return () => { + if (timerRef.current) clearTimeout(timerRef.current) + if (savedFeedbackTimerRef.current) clearTimeout(savedFeedbackTimerRef.current) + } + }, []) + + const startEditing = () => { + setShowSavedFeedback(false) + setErrorMessage(null) + setIsEditing(true) + } + + const save = (nextRaw: string) => { + const displayNext = nextRaw.trim() + if (displayNext === lastSaved.current) { + setErrorMessage(null) + setIsEditing(false) + return + } + + const formData = new FormData() + formData.set(recordIdField, String(recordId)) + formData.set('field', field) + formData.set(field, nextRaw) + + const id = ++requestId.current + setIsPending(true) + setShowSavedFeedback(false) + setErrorMessage(null) + void formAction({}, formData) + .then((result) => { + if (id !== requestId.current) return + setIsPending(false) + if (result.status === 'success') { + lastSaved.current = displayNext + setErrorMessage(null) + setIsEditing(false) + setShowSavedFeedback(true) + if (savedFeedbackTimerRef.current) clearTimeout(savedFeedbackTimerRef.current) + savedFeedbackTimerRef.current = setTimeout(() => { + savedFeedbackTimerRef.current = null + setShowSavedFeedback(false) + }, 1800) + router.refresh() + return + } + const message = + result.fieldErrors?.[field]?.[0] ?? result.message ?? 'Não foi possível salvar.' + setErrorMessage(message) + toast.error(message) + }) + .catch(() => { + if (id !== requestId.current) return + const message = 'Não foi possível salvar. Tente novamente.' + setIsPending(false) + setShowSavedFeedback(false) + setErrorMessage(message) + toast.error(message) + }) + } + + const scheduleSave = (nextRaw: string) => { + if (timerRef.current) clearTimeout(timerRef.current) + timerRef.current = setTimeout(() => { + timerRef.current = null + save(nextRaw) + }, SAVE_DEBOUNCE_MS) + } + + const handleBlur = () => { + if (timerRef.current) { + clearTimeout(timerRef.current) + timerRef.current = null + } + save(draft) + } + + const handleChange = (event: ChangeEvent) => { + const next = + field === 'phone' + ? formatBrazilianPhoneInput(sanitizeBrazilianPhoneInput(event.currentTarget.value)) + : event.currentTarget.value + setDraft(next) + if (saveOnChange) scheduleSave(next) + } + + const handleReadKeyDown = (event: KeyboardEvent) => { + if (event.key !== 'Enter' && event.key !== ' ') return + event.preventDefault() + startEditing() + } + + if (isEditing) { + return ( +
    + { + if (event.key === 'Escape') { + event.preventDefault() + if (timerRef.current) clearTimeout(timerRef.current) + timerRef.current = null + setDraft(value ?? '') + setShowSavedFeedback(false) + setErrorMessage(null) + setIsEditing(false) + } + }} + {...(field === 'phone' ? { inputMode: 'numeric' as const } : {})} + /> + {isPending ? ( + + ) : null} + {errorMessage ? ( +

    + {errorMessage} +

    + ) : null} +
    + ) + } + + const displayValue = field === 'phone' && value ? formatBrazilianPhoneInput(value) : undefined + const readValue = + href && field === 'name' && value ? ( + event.stopPropagation()} + onKeyDown={(event) => event.stopPropagation()} + > + {value} + + ) : readBehavior === 'copy' ? ( + + ) : ( + + {displayValue ?? value ?? '—'} + + ) + + const savedFeedback = showSavedFeedback ? ( + + Salvo. + + ) : null + + if (editTrigger === 'cell') { + if (href && field === 'name') { + return ( +
    +
    + ) + } + + return ( +
    + {readValue} + {savedFeedback} +
    + ) + } + + return ( +
    +
    {readValue}
    + {savedFeedback} + +
    + ) +} diff --git a/src/components/campaign/shared/MunicipalityStateDeputyRelationCell.tsx b/src/components/campaign/shared/MunicipalityStateDeputyRelationCell.tsx index f66764e2..cabd2658 100644 --- a/src/components/campaign/shared/MunicipalityStateDeputyRelationCell.tsx +++ b/src/components/campaign/shared/MunicipalityStateDeputyRelationCell.tsx @@ -45,7 +45,7 @@ const entryOf = (option: StateDeputyRelationOption): MunicipalityRelationEntry = label: option.name, initialsLabel: option.plainName, searchText: `${option.plainName} ${option.party ?? ''}`, - href: `/campanha/dobradinhas/${option.slug}`, + href: `/campanha/dobradinhas/${option.id}`, }) const entriesFor = (options: readonly StateDeputyRelationOption[]) => { @@ -98,7 +98,7 @@ export const MunicipalityStateDeputyRelationCell = ({ label: stateDeputyDisplayName(created.name, created.party), initialsLabel: created.name, searchText: `${created.name} ${created.party ?? ''}`, - href: `/campanha/dobradinhas/${created.slug}`, + href: `/campanha/dobradinhas/${created.id}`, }, } } @@ -123,7 +123,7 @@ export const MunicipalityStateDeputyRelationCell = ({ } emptyState={—} createLabel={(name) => `Criar dobradinha “${name}”`} - createMaxLength={160} + createMaxLength={120} sortSelected={false} onToggle={toggle} onCreate={create} diff --git a/src/components/campaign/stateDeputy/StateDeputyChips.tsx b/src/components/campaign/stateDeputy/StateDeputyChips.tsx index 6e784c19..1290337e 100644 --- a/src/components/campaign/stateDeputy/StateDeputyChips.tsx +++ b/src/components/campaign/stateDeputy/StateDeputyChips.tsx @@ -14,7 +14,7 @@ export const StateDeputyChips = ({ deputies }: StateDeputyChipsProps) => {
    {deputies.map((deputy) => ( - + {deputy.name} {deputy.party ? ` (${deputy.party})` : ''} diff --git a/src/components/campaign/stateDeputy/StateDeputyContactSection.tsx b/src/components/campaign/stateDeputy/StateDeputyContactSection.tsx new file mode 100644 index 00000000..3ca19172 --- /dev/null +++ b/src/components/campaign/stateDeputy/StateDeputyContactSection.tsx @@ -0,0 +1,65 @@ +'use client' + +import { updateStateDeputyContactFormAction } from '@/app/(campaign)/campanha/(app)/dobradinhas/formActions' +import { CampaignInlineEditableCell } from '@/components/campaign/shared/CampaignInlineEditableCell' + +type StateDeputyContactSectionProps = { + stateDeputyId: number + name: string + email: string | null + phone: string | null +} + +export const StateDeputyContactSection = ({ + stateDeputyId, + name, + email, + phone, +}: StateDeputyContactSectionProps) => ( +
    +

    + Contato +

    +
    +
    +
    Nome
    +
    + +
    +
    +
    +
    E-mail
    +
    + +
    +
    +
    +
    Telefone
    +
    + +
    +
    +
    +
    +) diff --git a/src/components/campaign/stateDeputy/StateDeputyForm.tsx b/src/components/campaign/stateDeputy/StateDeputyForm.tsx index 91df0113..934030d4 100644 --- a/src/components/campaign/stateDeputy/StateDeputyForm.tsx +++ b/src/components/campaign/stateDeputy/StateDeputyForm.tsx @@ -31,11 +31,7 @@ export const StateDeputyForm = ({ formAction, initial, initialName }: StateDeput return (
    {initial ? : null} - {isEdit ? ( -

    - O nome da dobradinha não pode ser alterado após a criação. -

    - ) : ( + {!isEdit ? ( Nome @@ -51,7 +47,7 @@ export const StateDeputyForm = ({ formAction, initial, initialName }: StateDeput {fieldError(state.fieldErrors, 'name')} ) : null} - )} + ) : null} Partido (v === '' ? undefined : v)) .optional(), }) + +const contactPhoneInput = z.union([brazilianMobile, z.literal(''), z.null()]).optional() + +const nullableContactPhone = contactPhoneInput.transform((value) => + value === '' || value === undefined ? null : value, +) + +/** Per-field Contact write shared by campaign person joins. */ +export const contactFieldUpdateSchema = z.discriminatedUnion('field', [ + z.object({ + id: positiveRelationshipId, + field: z.literal('name'), + name: z.string().trim().min(2).max(120), + }), + z.object({ + id: positiveRelationshipId, + field: z.literal('email'), + email: optionalPersistedEmail, + }), + z.object({ + id: positiveRelationshipId, + field: z.literal('phone'), + phone: nullableContactPhone, + }), +]) + +export type ContactFieldUpdateInput = z.input diff --git a/src/lib/schemas/leadership.ts b/src/lib/schemas/leadership.ts index 13391937..a19309e8 100644 --- a/src/lib/schemas/leadership.ts +++ b/src/lib/schemas/leadership.ts @@ -1,5 +1,6 @@ import { z } from 'zod' +import { contactFieldUpdateSchema } from '@/lib/schemas/contact' import { brazilianMobile, optionalPersistedEmail, @@ -152,29 +153,7 @@ export type LeadershipMunicipalitiesMembershipInput = z.input< typeof leadershipMunicipalitiesMembershipSchema > -const leadershipContactPhoneInput = z.union([brazilianMobile, z.literal(''), z.null()]).optional() - -const nullableLeadershipContactPhone = leadershipContactPhoneInput.transform((value) => - value === '' || value === undefined ? null : value, -) - /** Per-field Contact write for B153 inline edit (lista + detalhe). */ -export const leadershipContactUpdateSchema = z.discriminatedUnion('field', [ - z.object({ - id: positiveRelationshipId, - field: z.literal('name'), - name: z.string().trim().min(2).max(120), - }), - z.object({ - id: positiveRelationshipId, - field: z.literal('email'), - email: optionalPersistedEmail, - }), - z.object({ - id: positiveRelationshipId, - field: z.literal('phone'), - phone: nullableLeadershipContactPhone, - }), -]) +export const leadershipContactUpdateSchema = contactFieldUpdateSchema export type LeadershipContactUpdateInput = z.input diff --git a/src/lib/schemas/stateDeputy.ts b/src/lib/schemas/stateDeputy.ts index e2a3f482..febc22ba 100644 --- a/src/lib/schemas/stateDeputy.ts +++ b/src/lib/schemas/stateDeputy.ts @@ -12,7 +12,7 @@ import { import { parseStateDeputyNameParty } from '@/lib/stateDeputyNameParty' export const stateDeputyCreateSchema = z.object({ - name: z.string().trim().min(2).max(160), + name: z.string().trim().min(2).max(120), party: trimmedOptionalText(32), notes: trimmedOptionalText(4000), }) @@ -23,6 +23,14 @@ export const stateDeputyUpdateSchema = z.object({ notes: trimmedNullableText(4000), }) +export const stateDeputyPartyUpdateSchema = z.object({ + id: positiveRelationshipId, + party: trimmedNullableText(32), +}) + +export const STATE_DEPUTY_INVALID_CONTACT_MESSAGE = 'Contato da dobradinha inválido.' +export const STATE_DEPUTY_INVALID_FIELD_MESSAGE = 'Campo de dobradinha inválido.' + /** * Refused staff-only write on any dobradinha — thrown by the create/update * policy AND by the município batch. Named once because every route's @@ -81,8 +89,8 @@ export const municipalityStateDeputyCreateSchema = z const { name, party } = parseStateDeputyNameParty(input.rawName) return { municipalityId: input.municipalityId, name, party } }) - .refine((value) => value.name.length >= 2 && value.name.length <= 160, { - message: 'O nome precisa ter entre 2 e 160 caracteres.', + .refine((value) => value.name.length >= 2 && value.name.length <= 120, { + message: 'O nome precisa ter entre 2 e 120 caracteres.', }) .refine((value) => (value.party ?? '').length <= 32, { message: 'O partido pode ter no máximo 32 caracteres.', @@ -119,6 +127,7 @@ export const STATE_DEPUTY_ADVISOR_SAFE_MESSAGES = [ export type StateDeputyCreateInput = z.input export type StateDeputyUpdateInput = z.input +export type StateDeputyPartyUpdateInput = z.input export type StateDeputyMunicipalitiesBatchInput = z.input< typeof stateDeputyMunicipalitiesBatchSchema > diff --git a/src/migrations/20260806_082110_add_state_deputy_contact.json b/src/migrations/20260806_082110_add_state_deputy_contact.json new file mode 100644 index 00000000..c86de7fc --- /dev/null +++ b/src/migrations/20260806_082110_add_state_deputy_contact.json @@ -0,0 +1,12908 @@ +{ + "version": "7", + "dialect": "postgresql", + "tables": { + "public.users_roles": { + "name": "users_roles", + "schema": "", + "columns": { + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "enum_users_roles", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + } + }, + "indexes": { + "users_roles_order_idx": { + "name": "users_roles_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_roles_parent_idx": { + "name": "users_roles_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_roles_parent_fk": { + "name": "users_roles_parent_fk", + "tableFrom": "users_roles", + "tableTo": "users", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "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 + }, + "avatar_id": { + "name": "avatar_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "enum_campaign_user_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'leader'" + }, + "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_avatar_idx": { + "name": "campaign_user_avatar_idx", + "columns": [ + { + "expression": "avatar_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "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": { + "campaign_user_avatar_id_media_id_fk": { + "name": "campaign_user_avatar_id_media_id_fk", + "tableFrom": "campaign_user", + "tableTo": "media", + "columnsFrom": [ + "avatar_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.campaign_web_authn_credential": { + "name": "campaign_web_authn_credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "transports": { + "name": "transports", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "device_label": { + "name": "device_label", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp(3) with time zone", + "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": { + "campaign_web_authn_credential_user_idx": { + "name": "campaign_web_authn_credential_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaign_web_authn_credential_credential_id_idx": { + "name": "campaign_web_authn_credential_credential_id_idx", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaign_web_authn_credential_updated_at_idx": { + "name": "campaign_web_authn_credential_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaign_web_authn_credential_created_at_idx": { + "name": "campaign_web_authn_credential_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "campaign_web_authn_credential_user_id_campaign_user_id_fk": { + "name": "campaign_web_authn_credential_user_id_campaign_user_id_fk", + "tableFrom": "campaign_web_authn_credential", + "tableTo": "campaign_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification": { + "name": "notification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "recipient_id": { + "name": "recipient_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "enum_notification_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "municipality_id": { + "name": "municipality_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "read_at": { + "name": "read_at", + "type": "timestamp(3) with time zone", + "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": { + "notification_recipient_idx": { + "name": "notification_recipient_idx", + "columns": [ + { + "expression": "recipient_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notification_type_idx": { + "name": "notification_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notification_municipality_idx": { + "name": "notification_municipality_idx", + "columns": [ + { + "expression": "municipality_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notification_read_at_idx": { + "name": "notification_read_at_idx", + "columns": [ + { + "expression": "read_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notification_updated_at_idx": { + "name": "notification_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notification_created_at_idx": { + "name": "notification_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notification_recipient_id_campaign_user_id_fk": { + "name": "notification_recipient_id_campaign_user_id_fk", + "tableFrom": "notification", + "tableTo": "campaign_user", + "columnsFrom": [ + "recipient_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "notification_municipality_id_municipality_id_fk": { + "name": "notification_municipality_id_municipality_id_fk", + "tableFrom": "notification", + "tableTo": "municipality", + "columnsFrom": [ + "municipality_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.push_subscription": { + "name": "push_subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "p256dh": { + "name": "p256dh", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "auth": { + "name": "auth", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "expiration_time": { + "name": "expiration_time", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "consent_id": { + "name": "consent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "consent_content_hash": { + "name": "consent_content_hash", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "consented_at": { + "name": "consented_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + }, + "user_agent": { + "name": "user_agent", + "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": { + "push_subscription_user_idx": { + "name": "push_subscription_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "push_subscription_endpoint_idx": { + "name": "push_subscription_endpoint_idx", + "columns": [ + { + "expression": "endpoint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "push_subscription_consent_idx": { + "name": "push_subscription_consent_idx", + "columns": [ + { + "expression": "consent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "push_subscription_updated_at_idx": { + "name": "push_subscription_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "push_subscription_created_at_idx": { + "name": "push_subscription_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "push_subscription_user_id_campaign_user_id_fk": { + "name": "push_subscription_user_id_campaign_user_id_fk", + "tableFrom": "push_subscription", + "tableTo": "campaign_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "push_subscription_consent_id_consent_id_fk": { + "name": "push_subscription_consent_id_consent_id_fk", + "tableFrom": "push_subscription", + "tableTo": "consent", + "columnsFrom": [ + "consent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "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.campaign_vote_summary_snapshot": { + "name": "campaign_vote_summary_snapshot", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "day": { + "name": "day", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + }, + "scope_key": { + "name": "scope_key", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'statewide'" + }, + "staff_vote_total_central": { + "name": "staff_vote_total_central", + "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": { + "campaign_vote_summary_snapshot_day_idx": { + "name": "campaign_vote_summary_snapshot_day_idx", + "columns": [ + { + "expression": "day", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaign_vote_summary_snapshot_scope_key_idx": { + "name": "campaign_vote_summary_snapshot_scope_key_idx", + "columns": [ + { + "expression": "scope_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaign_vote_summary_snapshot_updated_at_idx": { + "name": "campaign_vote_summary_snapshot_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaign_vote_summary_snapshot_created_at_idx": { + "name": "campaign_vote_summary_snapshot_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "day_scopeKey_idx": { + "name": "day_scopeKey_idx", + "columns": [ + { + "expression": "day", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.municipality_strengths": { + "name": "municipality_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": { + "municipality_strengths_order_idx": { + "name": "municipality_strengths_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "municipality_strengths_parent_id_idx": { + "name": "municipality_strengths_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "municipality_strengths_parent_id_fk": { + "name": "municipality_strengths_parent_id_fk", + "tableFrom": "municipality_strengths", + "tableTo": "municipality", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.municipality_risks": { + "name": "municipality_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": { + "municipality_risks_order_idx": { + "name": "municipality_risks_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "municipality_risks_parent_id_idx": { + "name": "municipality_risks_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "municipality_risks_parent_id_fk": { + "name": "municipality_risks_parent_id_fk", + "tableFrom": "municipality_risks", + "tableTo": "municipality", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.municipality": { + "name": "municipality", + "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 + }, + "kind": { + "name": "kind", + "type": "enum_municipality_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "city": { + "name": "city", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "region": { + "name": "region", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "ibge_code": { + "name": "ibge_code", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "tse_city_code": { + "name": "tse_city_code", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "zone_number": { + "name": "zone_number", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "enum_municipality_priority", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'normal'" + }, + "engagement_level": { + "name": "engagement_level", + "type": "enum_municipality_engagement_level", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "level_note": { + "name": "level_note", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "level_changed_at": { + "name": "level_changed_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "expected_votes_pessimistic": { + "name": "expected_votes_pessimistic", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "expected_votes_central": { + "name": "expected_votes_central", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "expected_votes_optimistic": { + "name": "expected_votes_optimistic", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "political_trend_status": { + "name": "political_trend_status", + "type": "enum_municipality_political_trend_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "political_trend_note": { + "name": "political_trend_note", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "political_trend_recorded_by_id": { + "name": "political_trend_recorded_by_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "political_trend_recorded_at": { + "name": "political_trend_recorded_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "dobradinha_notes": { + "name": "dobradinha_notes", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "next_steps": { + "name": "next_steps", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "budget_notes": { + "name": "budget_notes", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "last_update_at": { + "name": "last_update_at", + "type": "timestamp(3) with time zone", + "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": { + "municipality_name_idx": { + "name": "municipality_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "municipality_slug_idx": { + "name": "municipality_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "municipality_kind_idx": { + "name": "municipality_kind_idx", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "municipality_city_idx": { + "name": "municipality_city_idx", + "columns": [ + { + "expression": "city", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "municipality_region_idx": { + "name": "municipality_region_idx", + "columns": [ + { + "expression": "region", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "municipality_ibge_code_idx": { + "name": "municipality_ibge_code_idx", + "columns": [ + { + "expression": "ibge_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "municipality_tse_city_code_idx": { + "name": "municipality_tse_city_code_idx", + "columns": [ + { + "expression": "tse_city_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "municipality_zone_number_idx": { + "name": "municipality_zone_number_idx", + "columns": [ + { + "expression": "zone_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "municipality_priority_idx": { + "name": "municipality_priority_idx", + "columns": [ + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "municipality_engagement_level_idx": { + "name": "municipality_engagement_level_idx", + "columns": [ + { + "expression": "engagement_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "municipality_level_changed_at_idx": { + "name": "municipality_level_changed_at_idx", + "columns": [ + { + "expression": "level_changed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "municipality_political_trend_political_trend_recorded_by_idx": { + "name": "municipality_political_trend_political_trend_recorded_by_idx", + "columns": [ + { + "expression": "political_trend_recorded_by_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "municipality_last_update_at_idx": { + "name": "municipality_last_update_at_idx", + "columns": [ + { + "expression": "last_update_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "municipality_updated_at_idx": { + "name": "municipality_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "municipality_created_at_idx": { + "name": "municipality_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "municipality_political_trend_recorded_by_id_campaign_user_id_fk": { + "name": "municipality_political_trend_recorded_by_id_campaign_user_id_fk", + "tableFrom": "municipality", + "tableTo": "campaign_user", + "columnsFrom": [ + "political_trend_recorded_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.municipality_rels": { + "name": "municipality_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 + }, + "state_deputy_id": { + "name": "state_deputy_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "municipality_rels_order_idx": { + "name": "municipality_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "municipality_rels_parent_idx": { + "name": "municipality_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "municipality_rels_path_idx": { + "name": "municipality_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "municipality_rels_campaign_user_id_idx": { + "name": "municipality_rels_campaign_user_id_idx", + "columns": [ + { + "expression": "campaign_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "municipality_rels_state_deputy_id_idx": { + "name": "municipality_rels_state_deputy_id_idx", + "columns": [ + { + "expression": "state_deputy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "municipality_rels_parent_fk": { + "name": "municipality_rels_parent_fk", + "tableFrom": "municipality_rels", + "tableTo": "municipality", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "municipality_rels_campaign_user_fk": { + "name": "municipality_rels_campaign_user_fk", + "tableFrom": "municipality_rels", + "tableTo": "campaign_user", + "columnsFrom": [ + "campaign_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "municipality_rels_state_deputy_fk": { + "name": "municipality_rels_state_deputy_fk", + "tableFrom": "municipality_rels", + "tableTo": "state_deputy", + "columnsFrom": [ + "state_deputy_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 + }, + "exclusive": { + "name": "exclusive", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "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 + }, + "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": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "leadership_exclusive_idx": { + "name": "leadership_exclusive_idx", + "columns": [ + { + "expression": "exclusive", + "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": {} + } + }, + "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_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.leadership_rels": { + "name": "leadership_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 + }, + "municipality_id": { + "name": "municipality_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "state_deputy_id": { + "name": "state_deputy_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "leadership_rels_order_idx": { + "name": "leadership_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "leadership_rels_parent_idx": { + "name": "leadership_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "leadership_rels_path_idx": { + "name": "leadership_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "leadership_rels_municipality_id_idx": { + "name": "leadership_rels_municipality_id_idx", + "columns": [ + { + "expression": "municipality_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "leadership_rels_organization_id_idx": { + "name": "leadership_rels_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "leadership_rels_state_deputy_id_idx": { + "name": "leadership_rels_state_deputy_id_idx", + "columns": [ + { + "expression": "state_deputy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "leadership_rels_parent_fk": { + "name": "leadership_rels_parent_fk", + "tableFrom": "leadership_rels", + "tableTo": "leadership", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "leadership_rels_municipality_fk": { + "name": "leadership_rels_municipality_fk", + "tableFrom": "leadership_rels", + "tableTo": "municipality", + "columnsFrom": [ + "municipality_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "leadership_rels_organization_fk": { + "name": "leadership_rels_organization_fk", + "tableFrom": "leadership_rels", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "leadership_rels_state_deputy_fk": { + "name": "leadership_rels_state_deputy_fk", + "tableFrom": "leadership_rels", + "tableTo": "state_deputy", + "columnsFrom": [ + "state_deputy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "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 + }, + "kind": { + "name": "kind", + "type": "enum_organization_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "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": { + "organization_name_idx": { + "name": "organization_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "organization_slug_idx": { + "name": "organization_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "organization_kind_idx": { + "name": "organization_kind_idx", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "organization_created_by_idx": { + "name": "organization_created_by_idx", + "columns": [ + { + "expression": "created_by_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "organization_updated_at_idx": { + "name": "organization_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "organization_created_at_idx": { + "name": "organization_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_created_by_id_campaign_user_id_fk": { + "name": "organization_created_by_id_campaign_user_id_fk", + "tableFrom": "organization", + "tableTo": "campaign_user", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_rels": { + "name": "organization_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 + }, + "municipality_id": { + "name": "municipality_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "organization_rels_order_idx": { + "name": "organization_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "organization_rels_parent_idx": { + "name": "organization_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "organization_rels_path_idx": { + "name": "organization_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "organization_rels_municipality_id_idx": { + "name": "organization_rels_municipality_id_idx", + "columns": [ + { + "expression": "municipality_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_rels_parent_fk": { + "name": "organization_rels_parent_fk", + "tableFrom": "organization_rels", + "tableTo": "organization", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_rels_municipality_fk": { + "name": "organization_rels_municipality_fk", + "tableFrom": "organization_rels", + "tableTo": "municipality", + "columnsFrom": [ + "municipality_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.state_deputy": { + "name": "state_deputy", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "party": { + "name": "party", + "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": { + "state_deputy_contact_idx": { + "name": "state_deputy_contact_idx", + "columns": [ + { + "expression": "contact_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "state_deputy_slug_idx": { + "name": "state_deputy_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "state_deputy_party_idx": { + "name": "state_deputy_party_idx", + "columns": [ + { + "expression": "party", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "state_deputy_created_by_idx": { + "name": "state_deputy_created_by_idx", + "columns": [ + { + "expression": "created_by_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "state_deputy_updated_at_idx": { + "name": "state_deputy_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "state_deputy_created_at_idx": { + "name": "state_deputy_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "state_deputy_contact_id_contact_id_fk": { + "name": "state_deputy_contact_id_contact_id_fk", + "tableFrom": "state_deputy", + "tableTo": "contact", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "state_deputy_created_by_id_campaign_user_id_fk": { + "name": "state_deputy_created_by_id_campaign_user_id_fk", + "tableFrom": "state_deputy", + "tableTo": "campaign_user", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.state_deputy_rels": { + "name": "state_deputy_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": { + "state_deputy_rels_order_idx": { + "name": "state_deputy_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "state_deputy_rels_parent_idx": { + "name": "state_deputy_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "state_deputy_rels_path_idx": { + "name": "state_deputy_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "state_deputy_rels_campaign_user_id_idx": { + "name": "state_deputy_rels_campaign_user_id_idx", + "columns": [ + { + "expression": "campaign_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "state_deputy_rels_parent_fk": { + "name": "state_deputy_rels_parent_fk", + "tableFrom": "state_deputy_rels", + "tableTo": "state_deputy", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "state_deputy_rels_campaign_user_fk": { + "name": "state_deputy_rels_campaign_user_fk", + "tableFrom": "state_deputy_rels", + "tableTo": "campaign_user", + "columnsFrom": [ + "campaign_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vote_pledge": { + "name": "vote_pledge", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "leadership_id": { + "name": "leadership_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "municipality_id": { + "name": "municipality_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "declared_votes": { + "name": "declared_votes", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "declared_at": { + "name": "declared_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "declared_by_id": { + "name": "declared_by_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "estimated_votes_pessimistic": { + "name": "estimated_votes_pessimistic", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "estimated_votes_central": { + "name": "estimated_votes_central", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "estimated_votes_optimistic": { + "name": "estimated_votes_optimistic", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "estimate_note": { + "name": "estimate_note", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "estimated_by_id": { + "name": "estimated_by_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "estimated_at": { + "name": "estimated_at", + "type": "timestamp(3) with time zone", + "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": { + "vote_pledge_leadership_idx": { + "name": "vote_pledge_leadership_idx", + "columns": [ + { + "expression": "leadership_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vote_pledge_municipality_idx": { + "name": "vote_pledge_municipality_idx", + "columns": [ + { + "expression": "municipality_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vote_pledge_declared_votes_idx": { + "name": "vote_pledge_declared_votes_idx", + "columns": [ + { + "expression": "declared_votes", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vote_pledge_declared_by_idx": { + "name": "vote_pledge_declared_by_idx", + "columns": [ + { + "expression": "declared_by_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vote_pledge_estimated_by_idx": { + "name": "vote_pledge_estimated_by_idx", + "columns": [ + { + "expression": "estimated_by_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vote_pledge_updated_at_idx": { + "name": "vote_pledge_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vote_pledge_created_at_idx": { + "name": "vote_pledge_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "leadership_municipality_idx": { + "name": "leadership_municipality_idx", + "columns": [ + { + "expression": "leadership_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "municipality_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vote_pledge_leadership_id_leadership_id_fk": { + "name": "vote_pledge_leadership_id_leadership_id_fk", + "tableFrom": "vote_pledge", + "tableTo": "leadership", + "columnsFrom": [ + "leadership_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "vote_pledge_municipality_id_municipality_id_fk": { + "name": "vote_pledge_municipality_id_municipality_id_fk", + "tableFrom": "vote_pledge", + "tableTo": "municipality", + "columnsFrom": [ + "municipality_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "vote_pledge_declared_by_id_campaign_user_id_fk": { + "name": "vote_pledge_declared_by_id_campaign_user_id_fk", + "tableFrom": "vote_pledge", + "tableTo": "campaign_user", + "columnsFrom": [ + "declared_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "vote_pledge_estimated_by_id_campaign_user_id_fk": { + "name": "vote_pledge_estimated_by_id_campaign_user_id_fk", + "tableFrom": "vote_pledge", + "tableTo": "campaign_user", + "columnsFrom": [ + "estimated_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._vote_pledge_v": { + "name": "_vote_pledge_v", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_leadership_id": { + "name": "version_leadership_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "version_municipality_id": { + "name": "version_municipality_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "version_declared_votes": { + "name": "version_declared_votes", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "version_declared_at": { + "name": "version_declared_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_declared_by_id": { + "name": "version_declared_by_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_estimated_votes_pessimistic": { + "name": "version_estimated_votes_pessimistic", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "version_estimated_votes_central": { + "name": "version_estimated_votes_central", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "version_estimated_votes_optimistic": { + "name": "version_estimated_votes_optimistic", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "version_estimate_note": { + "name": "version_estimate_note", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_estimated_by_id": { + "name": "version_estimated_by_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_estimated_at": { + "name": "version_estimated_at", + "type": "timestamp(3) with time zone", + "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 + }, + "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()" + } + }, + "indexes": { + "_vote_pledge_v_parent_idx": { + "name": "_vote_pledge_v_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_vote_pledge_v_version_version_leadership_idx": { + "name": "_vote_pledge_v_version_version_leadership_idx", + "columns": [ + { + "expression": "version_leadership_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_vote_pledge_v_version_version_municipality_idx": { + "name": "_vote_pledge_v_version_version_municipality_idx", + "columns": [ + { + "expression": "version_municipality_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_vote_pledge_v_version_version_declared_votes_idx": { + "name": "_vote_pledge_v_version_version_declared_votes_idx", + "columns": [ + { + "expression": "version_declared_votes", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_vote_pledge_v_version_version_declared_by_idx": { + "name": "_vote_pledge_v_version_version_declared_by_idx", + "columns": [ + { + "expression": "version_declared_by_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_vote_pledge_v_version_version_estimated_by_idx": { + "name": "_vote_pledge_v_version_version_estimated_by_idx", + "columns": [ + { + "expression": "version_estimated_by_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_vote_pledge_v_version_version_updated_at_idx": { + "name": "_vote_pledge_v_version_version_updated_at_idx", + "columns": [ + { + "expression": "version_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_vote_pledge_v_version_version_created_at_idx": { + "name": "_vote_pledge_v_version_version_created_at_idx", + "columns": [ + { + "expression": "version_created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_vote_pledge_v_created_at_idx": { + "name": "_vote_pledge_v_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_vote_pledge_v_updated_at_idx": { + "name": "_vote_pledge_v_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "version_leadership_version_municipality_idx": { + "name": "version_leadership_version_municipality_idx", + "columns": [ + { + "expression": "version_leadership_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version_municipality_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_vote_pledge_v_parent_id_vote_pledge_id_fk": { + "name": "_vote_pledge_v_parent_id_vote_pledge_id_fk", + "tableFrom": "_vote_pledge_v", + "tableTo": "vote_pledge", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "_vote_pledge_v_version_leadership_id_leadership_id_fk": { + "name": "_vote_pledge_v_version_leadership_id_leadership_id_fk", + "tableFrom": "_vote_pledge_v", + "tableTo": "leadership", + "columnsFrom": [ + "version_leadership_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "_vote_pledge_v_version_municipality_id_municipality_id_fk": { + "name": "_vote_pledge_v_version_municipality_id_municipality_id_fk", + "tableFrom": "_vote_pledge_v", + "tableTo": "municipality", + "columnsFrom": [ + "version_municipality_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "_vote_pledge_v_version_declared_by_id_campaign_user_id_fk": { + "name": "_vote_pledge_v_version_declared_by_id_campaign_user_id_fk", + "tableFrom": "_vote_pledge_v", + "tableTo": "campaign_user", + "columnsFrom": [ + "version_declared_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "_vote_pledge_v_version_estimated_by_id_campaign_user_id_fk": { + "name": "_vote_pledge_v_version_estimated_by_id_campaign_user_id_fk", + "tableFrom": "_vote_pledge_v", + "tableTo": "campaign_user", + "columnsFrom": [ + "version_estimated_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.allocation_decision": { + "name": "allocation_decision", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "municipality_id": { + "name": "municipality_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pattern_id": { + "name": "pattern_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "enum_allocation_decision_outcome", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "rationale": { + "name": "rationale", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "alternative_reading": { + "name": "alternative_reading", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "decided_by_id": { + "name": "decided_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": { + "allocation_decision_municipality_idx": { + "name": "allocation_decision_municipality_idx", + "columns": [ + { + "expression": "municipality_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "allocation_decision_pattern_id_idx": { + "name": "allocation_decision_pattern_id_idx", + "columns": [ + { + "expression": "pattern_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "allocation_decision_decided_by_idx": { + "name": "allocation_decision_decided_by_idx", + "columns": [ + { + "expression": "decided_by_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "allocation_decision_updated_at_idx": { + "name": "allocation_decision_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "allocation_decision_created_at_idx": { + "name": "allocation_decision_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "allocation_decision_municipality_id_municipality_id_fk": { + "name": "allocation_decision_municipality_id_municipality_id_fk", + "tableFrom": "allocation_decision", + "tableTo": "municipality", + "columnsFrom": [ + "municipality_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "allocation_decision_decided_by_id_campaign_user_id_fk": { + "name": "allocation_decision_decided_by_id_campaign_user_id_fk", + "tableFrom": "allocation_decision", + "tableTo": "campaign_user", + "columnsFrom": [ + "decided_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.campaign_demand_status_history": { + "name": "campaign_demand_status_history", + "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 + }, + "status": { + "name": "status", + "type": "enum_campaign_demand_status_history_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "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": { + "campaign_demand_status_history_order_idx": { + "name": "campaign_demand_status_history_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaign_demand_status_history_parent_id_idx": { + "name": "campaign_demand_status_history_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaign_demand_status_history_author_idx": { + "name": "campaign_demand_status_history_author_idx", + "columns": [ + { + "expression": "author_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "campaign_demand_status_history_author_id_campaign_user_id_fk": { + "name": "campaign_demand_status_history_author_id_campaign_user_id_fk", + "tableFrom": "campaign_demand_status_history", + "tableTo": "campaign_user", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "campaign_demand_status_history_parent_id_fk": { + "name": "campaign_demand_status_history_parent_id_fk", + "tableFrom": "campaign_demand_status_history", + "tableTo": "campaign_demand", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.campaign_demand": { + "name": "campaign_demand", + "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_campaign_demand_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "municipality_id": { + "name": "municipality_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "activity_id": { + "name": "activity_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "leadership_id": { + "name": "leadership_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "enum_campaign_demand_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'aberta'" + }, + "decision_note": { + "name": "decision_note", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "decided_by_id": { + "name": "decided_by_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "decided_at": { + "name": "decided_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "numeric", + "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": { + "campaign_demand_title_idx": { + "name": "campaign_demand_title_idx", + "columns": [ + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaign_demand_slug_idx": { + "name": "campaign_demand_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaign_demand_kind_idx": { + "name": "campaign_demand_kind_idx", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaign_demand_municipality_idx": { + "name": "campaign_demand_municipality_idx", + "columns": [ + { + "expression": "municipality_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaign_demand_activity_idx": { + "name": "campaign_demand_activity_idx", + "columns": [ + { + "expression": "activity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaign_demand_leadership_idx": { + "name": "campaign_demand_leadership_idx", + "columns": [ + { + "expression": "leadership_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaign_demand_status_idx": { + "name": "campaign_demand_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaign_demand_decided_by_idx": { + "name": "campaign_demand_decided_by_idx", + "columns": [ + { + "expression": "decided_by_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaign_demand_created_by_idx": { + "name": "campaign_demand_created_by_idx", + "columns": [ + { + "expression": "created_by_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaign_demand_updated_at_idx": { + "name": "campaign_demand_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaign_demand_created_at_idx": { + "name": "campaign_demand_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "campaign_demand_municipality_id_municipality_id_fk": { + "name": "campaign_demand_municipality_id_municipality_id_fk", + "tableFrom": "campaign_demand", + "tableTo": "municipality", + "columnsFrom": [ + "municipality_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "campaign_demand_activity_id_activity_id_fk": { + "name": "campaign_demand_activity_id_activity_id_fk", + "tableFrom": "campaign_demand", + "tableTo": "activity", + "columnsFrom": [ + "activity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "campaign_demand_leadership_id_leadership_id_fk": { + "name": "campaign_demand_leadership_id_leadership_id_fk", + "tableFrom": "campaign_demand", + "tableTo": "leadership", + "columnsFrom": [ + "leadership_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "campaign_demand_decided_by_id_campaign_user_id_fk": { + "name": "campaign_demand_decided_by_id_campaign_user_id_fk", + "tableFrom": "campaign_demand", + "tableTo": "campaign_user", + "columnsFrom": [ + "decided_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "campaign_demand_created_by_id_campaign_user_id_fk": { + "name": "campaign_demand_created_by_id_campaign_user_id_fk", + "tableFrom": "campaign_demand", + "tableTo": "campaign_user", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.campaign_demand_rels": { + "name": "campaign_demand_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 + }, + "media_id": { + "name": "media_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "campaign_demand_rels_order_idx": { + "name": "campaign_demand_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaign_demand_rels_parent_idx": { + "name": "campaign_demand_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaign_demand_rels_path_idx": { + "name": "campaign_demand_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaign_demand_rels_media_id_idx": { + "name": "campaign_demand_rels_media_id_idx", + "columns": [ + { + "expression": "media_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "campaign_demand_rels_parent_fk": { + "name": "campaign_demand_rels_parent_fk", + "tableFrom": "campaign_demand_rels", + "tableTo": "campaign_demand", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "campaign_demand_rels_media_fk": { + "name": "campaign_demand_rels_media_fk", + "tableFrom": "campaign_demand_rels", + "tableTo": "media", + "columnsFrom": [ + "media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "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 + }, + "municipality_id": { + "name": "municipality_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_municipality_idx": { + "name": "supporter_municipality_idx", + "columns": [ + { + "expression": "municipality_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_municipality_id_municipality_id_fk": { + "name": "supporter_municipality_id_municipality_id_fk", + "tableFrom": "supporter", + "tableTo": "municipality", + "columnsFrom": [ + "municipality_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.municipality_update": { + "name": "municipality_update", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "municipality_id": { + "name": "municipality_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "enum_municipality_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 + }, + "signal_type": { + "name": "signal_type", + "type": "enum_municipality_update_signal_type", + "typeSchema": "public", + "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": { + "municipality_update_municipality_idx": { + "name": "municipality_update_municipality_idx", + "columns": [ + { + "expression": "municipality_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "municipality_update_author_idx": { + "name": "municipality_update_author_idx", + "columns": [ + { + "expression": "author_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "municipality_update_kind_idx": { + "name": "municipality_update_kind_idx", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "municipality_update_updated_at_idx": { + "name": "municipality_update_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "municipality_update_created_at_idx": { + "name": "municipality_update_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "municipality_update_municipality_id_municipality_id_fk": { + "name": "municipality_update_municipality_id_municipality_id_fk", + "tableFrom": "municipality_update", + "tableTo": "municipality", + "columnsFrom": [ + "municipality_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "municipality_update_author_id_campaign_user_id_fk": { + "name": "municipality_update_author_id_campaign_user_id_fk", + "tableFrom": "municipality_update", + "tableTo": "campaign_user", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.activity_tasks": { + "name": "activity_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": { + "activity_tasks_order_idx": { + "name": "activity_tasks_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_tasks_parent_id_idx": { + "name": "activity_tasks_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_tasks_responsible_idx": { + "name": "activity_tasks_responsible_idx", + "columns": [ + { + "expression": "responsible_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "activity_tasks_responsible_id_contact_id_fk": { + "name": "activity_tasks_responsible_id_contact_id_fk", + "tableFrom": "activity_tasks", + "tableTo": "contact", + "columnsFrom": [ + "responsible_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "activity_tasks_parent_id_fk": { + "name": "activity_tasks_parent_id_fk", + "tableFrom": "activity_tasks", + "tableTo": "activity", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.activity_updates": { + "name": "activity_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": { + "activity_updates_order_idx": { + "name": "activity_updates_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_updates_parent_id_idx": { + "name": "activity_updates_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_updates_author_idx": { + "name": "activity_updates_author_idx", + "columns": [ + { + "expression": "author_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "activity_updates_author_id_campaign_user_id_fk": { + "name": "activity_updates_author_id_campaign_user_id_fk", + "tableFrom": "activity_updates", + "tableTo": "campaign_user", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "activity_updates_parent_id_fk": { + "name": "activity_updates_parent_id_fk", + "tableFrom": "activity_updates", + "tableTo": "activity", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.activity": { + "name": "activity", + "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_activity_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "enum_activity_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'rascunho'" + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "origin": { + "name": "origin", + "type": "enum_activity_origin", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'dado'" + }, + "deputy_present": { + "name": "deputy_present", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": 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 + }, + "municipality_id": { + "name": "municipality_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "locality": { + "name": "locality", + "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 + }, + "task_total": { + "name": "task_total", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "task_done_count": { + "name": "task_done_count", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "result_summary": { + "name": "result_summary", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "result_recorded_by_id": { + "name": "result_recorded_by_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "result_recorded_at": { + "name": "result_recorded_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": { + "activity_title_idx": { + "name": "activity_title_idx", + "columns": [ + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_slug_idx": { + "name": "activity_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_kind_idx": { + "name": "activity_kind_idx", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_status_idx": { + "name": "activity_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_deputy_present_idx": { + "name": "activity_deputy_present_idx", + "columns": [ + { + "expression": "deputy_present", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_start_at_idx": { + "name": "activity_start_at_idx", + "columns": [ + { + "expression": "start_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_municipality_idx": { + "name": "activity_municipality_idx", + "columns": [ + { + "expression": "municipality_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_responsible_idx": { + "name": "activity_responsible_idx", + "columns": [ + { + "expression": "responsible_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_leadership_idx": { + "name": "activity_leadership_idx", + "columns": [ + { + "expression": "leadership_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_result_recorded_by_idx": { + "name": "activity_result_recorded_by_idx", + "columns": [ + { + "expression": "result_recorded_by_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_created_by_idx": { + "name": "activity_created_by_idx", + "columns": [ + { + "expression": "created_by_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_updated_at_idx": { + "name": "activity_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_created_at_idx": { + "name": "activity_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "activity_municipality_id_municipality_id_fk": { + "name": "activity_municipality_id_municipality_id_fk", + "tableFrom": "activity", + "tableTo": "municipality", + "columnsFrom": [ + "municipality_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "activity_responsible_id_contact_id_fk": { + "name": "activity_responsible_id_contact_id_fk", + "tableFrom": "activity", + "tableTo": "contact", + "columnsFrom": [ + "responsible_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "activity_leadership_id_leadership_id_fk": { + "name": "activity_leadership_id_leadership_id_fk", + "tableFrom": "activity", + "tableTo": "leadership", + "columnsFrom": [ + "leadership_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "activity_result_recorded_by_id_campaign_user_id_fk": { + "name": "activity_result_recorded_by_id_campaign_user_id_fk", + "tableFrom": "activity", + "tableTo": "campaign_user", + "columnsFrom": [ + "result_recorded_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "activity_created_by_id_campaign_user_id_fk": { + "name": "activity_created_by_id_campaign_user_id_fk", + "tableFrom": "activity", + "tableTo": "campaign_user", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.activity_rels": { + "name": "activity_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 + }, + "organization_id": { + "name": "organization_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "campaign_user_id": { + "name": "campaign_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "media_id": { + "name": "media_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "activity_rels_order_idx": { + "name": "activity_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_rels_parent_idx": { + "name": "activity_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_rels_path_idx": { + "name": "activity_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_rels_organization_id_idx": { + "name": "activity_rels_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_rels_campaign_user_id_idx": { + "name": "activity_rels_campaign_user_id_idx", + "columns": [ + { + "expression": "campaign_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_rels_media_id_idx": { + "name": "activity_rels_media_id_idx", + "columns": [ + { + "expression": "media_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "activity_rels_parent_fk": { + "name": "activity_rels_parent_fk", + "tableFrom": "activity_rels", + "tableTo": "activity", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "activity_rels_organization_fk": { + "name": "activity_rels_organization_fk", + "tableFrom": "activity_rels", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "activity_rels_campaign_user_fk": { + "name": "activity_rels_campaign_user_fk", + "tableFrom": "activity_rels", + "tableTo": "campaign_user", + "columnsFrom": [ + "campaign_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "activity_rels_media_fk": { + "name": "activity_rels_media_fk", + "tableFrom": "activity_rels", + "tableTo": "media", + "columnsFrom": [ + "media_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 + }, + "tracking_facebook_pixel_id": { + "name": "tracking_facebook_pixel_id", + "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()" + }, + "_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_tracking_facebook_pixel_id": { + "name": "version_tracking_facebook_pixel_id", + "type": "varchar", + "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": false + }, + "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.exports": { + "name": "exports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "format": { + "name": "format", + "type": "enum_exports_format", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'csv'" + }, + "limit": { + "name": "limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "page": { + "name": "page", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "sort": { + "name": "sort", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "enum_exports_sort_order", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "drafts": { + "name": "drafts", + "type": "enum_exports_drafts", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'yes'" + }, + "collection_slug": { + "name": "collection_slug", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'signature'" + }, + "where": { + "name": "where", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "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": { + "exports_updated_at_idx": { + "name": "exports_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "exports_created_at_idx": { + "name": "exports_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "exports_filename_idx": { + "name": "exports_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.exports_texts": { + "name": "exports_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": { + "exports_texts_order_parent": { + "name": "exports_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": {} + } + }, + "foreignKeys": { + "exports_texts_parent_fk": { + "name": "exports_texts_parent_fk", + "tableFrom": "exports_texts", + "tableTo": "exports", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.imports": { + "name": "imports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "collection_slug": { + "name": "collection_slug", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "import_mode": { + "name": "import_mode", + "type": "enum_imports_import_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "match_field": { + "name": "match_field", + "type": "varchar", + "primaryKey": false, + "notNull": false, + "default": "'id'" + }, + "status": { + "name": "status", + "type": "enum_imports_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'pending'" + }, + "summary_imported": { + "name": "summary_imported", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "summary_updated": { + "name": "summary_updated", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "summary_total": { + "name": "summary_total", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "summary_issues": { + "name": "summary_issues", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "summary_issue_details": { + "name": "summary_issue_details", + "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()" + }, + "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": { + "imports_updated_at_idx": { + "name": "imports_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "imports_created_at_idx": { + "name": "imports_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "imports_filename_idx": { + "name": "imports_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.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_web_authn_credential_id": { + "name": "campaign_web_authn_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "notification_id": { + "name": "notification_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "push_subscription_id": { + "name": "push_subscription_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "campaign_invite_id": { + "name": "campaign_invite_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "campaign_vote_summary_snapshot_id": { + "name": "campaign_vote_summary_snapshot_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "municipality_id": { + "name": "municipality_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "leadership_id": { + "name": "leadership_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "state_deputy_id": { + "name": "state_deputy_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vote_pledge_id": { + "name": "vote_pledge_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "allocation_decision_id": { + "name": "allocation_decision_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "campaign_demand_id": { + "name": "campaign_demand_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 + }, + "municipality_update_id": { + "name": "municipality_update_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "activity_id": { + "name": "activity_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_web_authn_credent_idx": { + "name": "payload_locked_documents_rels_campaign_web_authn_credent_idx", + "columns": [ + { + "expression": "campaign_web_authn_credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_notification_id_idx": { + "name": "payload_locked_documents_rels_notification_id_idx", + "columns": [ + { + "expression": "notification_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_push_subscription_id_idx": { + "name": "payload_locked_documents_rels_push_subscription_id_idx", + "columns": [ + { + "expression": "push_subscription_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_campaign_vote_summary_snap_idx": { + "name": "payload_locked_documents_rels_campaign_vote_summary_snap_idx", + "columns": [ + { + "expression": "campaign_vote_summary_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_municipality_id_idx": { + "name": "payload_locked_documents_rels_municipality_id_idx", + "columns": [ + { + "expression": "municipality_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_organization_id_idx": { + "name": "payload_locked_documents_rels_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_state_deputy_id_idx": { + "name": "payload_locked_documents_rels_state_deputy_id_idx", + "columns": [ + { + "expression": "state_deputy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_vote_pledge_id_idx": { + "name": "payload_locked_documents_rels_vote_pledge_id_idx", + "columns": [ + { + "expression": "vote_pledge_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_allocation_decision_id_idx": { + "name": "payload_locked_documents_rels_allocation_decision_id_idx", + "columns": [ + { + "expression": "allocation_decision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_campaign_demand_id_idx": { + "name": "payload_locked_documents_rels_campaign_demand_id_idx", + "columns": [ + { + "expression": "campaign_demand_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_municipality_update_id_idx": { + "name": "payload_locked_documents_rels_municipality_update_id_idx", + "columns": [ + { + "expression": "municipality_update_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_activity_id_idx": { + "name": "payload_locked_documents_rels_activity_id_idx", + "columns": [ + { + "expression": "activity_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_web_authn_credenti_fk": { + "name": "payload_locked_documents_rels_campaign_web_authn_credenti_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "campaign_web_authn_credential", + "columnsFrom": [ + "campaign_web_authn_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_notification_fk": { + "name": "payload_locked_documents_rels_notification_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "notification", + "columnsFrom": [ + "notification_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_push_subscription_fk": { + "name": "payload_locked_documents_rels_push_subscription_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "push_subscription", + "columnsFrom": [ + "push_subscription_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_campaign_vote_summary_snaps_fk": { + "name": "payload_locked_documents_rels_campaign_vote_summary_snaps_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "campaign_vote_summary_snapshot", + "columnsFrom": [ + "campaign_vote_summary_snapshot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_municipality_fk": { + "name": "payload_locked_documents_rels_municipality_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "municipality", + "columnsFrom": [ + "municipality_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_organization_fk": { + "name": "payload_locked_documents_rels_organization_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_state_deputy_fk": { + "name": "payload_locked_documents_rels_state_deputy_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "state_deputy", + "columnsFrom": [ + "state_deputy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_vote_pledge_fk": { + "name": "payload_locked_documents_rels_vote_pledge_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "vote_pledge", + "columnsFrom": [ + "vote_pledge_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_allocation_decision_fk": { + "name": "payload_locked_documents_rels_allocation_decision_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "allocation_decision", + "columnsFrom": [ + "allocation_decision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_campaign_demand_fk": { + "name": "payload_locked_documents_rels_campaign_demand_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "campaign_demand", + "columnsFrom": [ + "campaign_demand_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_municipality_update_fk": { + "name": "payload_locked_documents_rels_municipality_update_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "municipality_update", + "columnsFrom": [ + "municipality_update_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_activity_fk": { + "name": "payload_locked_documents_rels_activity_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "activity", + "columnsFrom": [ + "activity_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 + }, + "public.privacy_policy": { + "name": "privacy_policy", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "published": { + "name": "published", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "body": { + "name": "body", + "type": "jsonb", + "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": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.campaign_goals": { + "name": "campaign_goals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "state_goal": { + "name": "state_goal", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 150000 + }, + "margin": { + "name": "margin", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "base_year": { + "name": "base_year", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "varchar", + "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": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.enum_users_roles": { + "name": "enum_users_roles", + "schema": "public", + "values": [ + "admin", + "editor" + ] + }, + "public.enum_campaign_user_role": { + "name": "enum_campaign_user_role", + "schema": "public", + "values": [ + "coordinator", + "advisor", + "candidate", + "leader" + ] + }, + "public.enum_notification_type": { + "name": "enum_notification_type", + "schema": "public", + "values": [ + "municipality_update", + "new_supporter", + "activity_attention", + "invite_accepted" + ] + }, + "public.enum_campaign_invite_kind": { + "name": "enum_campaign_invite_kind", + "schema": "public", + "values": [ + "login", + "autopreenchimento" + ] + }, + "public.enum_municipality_kind": { + "name": "enum_municipality_kind", + "schema": "public", + "values": [ + "municipio", + "zona" + ] + }, + "public.enum_municipality_priority": { + "name": "enum_municipality_priority", + "schema": "public", + "values": [ + "alta", + "normal" + ] + }, + "public.enum_municipality_engagement_level": { + "name": "enum_municipality_engagement_level", + "schema": "public", + "values": [ + "n0", + "n1", + "n2", + "n3", + "n4" + ] + }, + "public.enum_municipality_political_trend_status": { + "name": "enum_municipality_political_trend_status", + "schema": "public", + "values": [ + "favoravel", + "neutra", + "desfavoravel" + ] + }, + "public.enum_leadership_support_status": { + "name": "enum_leadership_support_status", + "schema": "public", + "values": [ + "engajado", + "a_abordar", + "em_disputa", + "negativo" + ] + }, + "public.enum_organization_kind": { + "name": "enum_organization_kind", + "schema": "public", + "values": [ + "sindicato", + "associacao", + "religioso", + "movimento", + "categoria_profissional", + "outro" + ] + }, + "public.enum_allocation_decision_outcome": { + "name": "enum_allocation_decision_outcome", + "schema": "public", + "values": [ + "aceita", + "descarta", + "movimento", + "adiada" + ] + }, + "public.enum_campaign_demand_status_history_status": { + "name": "enum_campaign_demand_status_history_status", + "schema": "public", + "values": [ + "aberta", + "em_analise", + "escalada", + "aprovada", + "rejeitada" + ] + }, + "public.enum_campaign_demand_kind": { + "name": "enum_campaign_demand_kind", + "schema": "public", + "values": [ + "material", + "servico", + "transporte", + "alimentacao", + "infraestrutura", + "espaco", + "equipamento", + "pessoal_apoio", + "outro" + ] + }, + "public.enum_campaign_demand_status": { + "name": "enum_campaign_demand_status", + "schema": "public", + "values": [ + "aberta", + "em_analise", + "escalada", + "aprovada", + "rejeitada" + ] + }, + "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", + "lideranca", + "convite", + "evento" + ] + }, + "public.enum_municipality_update_kind": { + "name": "enum_municipality_update_kind", + "schema": "public", + "values": [ + "semanal", + "urgente", + "nota", + "sinal" + ] + }, + "public.enum_municipality_update_signal_type": { + "name": "enum_municipality_update_signal_type", + "schema": "public", + "values": [ + "invasao", + "esfriamento", + "visita_adversario", + "proposta_broker", + "outro" + ] + }, + "public.enum_activity_kind": { + "name": "enum_activity_kind", + "schema": "public", + "values": [ + "caminhada", + "comicio", + "carreata", + "panfletagem", + "porta_a_porta", + "reuniao_apoio", + "lancamento", + "convencao", + "ato", + "entrevista", + "producao_conteudo", + "digital", + "outro" + ] + }, + "public.enum_activity_status": { + "name": "enum_activity_status", + "schema": "public", + "values": [ + "rascunho", + "planejado", + "confirmado", + "realizado", + "cancelado" + ] + }, + "public.enum_activity_origin": { + "name": "enum_activity_origin", + "schema": "public", + "values": [ + "dado", + "pedido_broker", + "obrigacao_politica" + ] + }, + "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_exports_format": { + "name": "enum_exports_format", + "schema": "public", + "values": [ + "csv", + "json" + ] + }, + "public.enum_exports_sort_order": { + "name": "enum_exports_sort_order", + "schema": "public", + "values": [ + "asc", + "desc" + ] + }, + "public.enum_exports_drafts": { + "name": "enum_exports_drafts", + "schema": "public", + "values": [ + "yes", + "no" + ] + }, + "public.enum_imports_import_mode": { + "name": "enum_imports_import_mode", + "schema": "public", + "values": [ + "create", + "update", + "upsert" + ] + }, + "public.enum_imports_status": { + "name": "enum_imports_status", + "schema": "public", + "values": [ + "pending", + "completed", + "partial", + "failed" + ] + }, + "public.enum_payload_jobs_log_task_slug": { + "name": "enum_payload_jobs_log_task_slug", + "schema": "public", + "values": [ + "inline", + "createCollectionExport", + "createCollectionImport", + "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", + "createCollectionExport", + "createCollectionImport", + "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": "4e858b4e-9663-4250-aeb2-83bda5ed4dc1", + "prevId": "00000000-0000-0000-0000-000000000000" +} diff --git a/src/migrations/20260806_082110_add_state_deputy_contact.ts b/src/migrations/20260806_082110_add_state_deputy_contact.ts new file mode 100644 index 00000000..d04c34cc --- /dev/null +++ b/src/migrations/20260806_082110_add_state_deputy_contact.ts @@ -0,0 +1,159 @@ +import { sql, type MigrateDownArgs, type MigrateUpArgs } from '@payloadcms/db-postgres' + +export async function up({ db }: MigrateUpArgs): Promise { + await db.execute(sql` + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'state_deputy' + AND column_name = 'contact_id' + ) THEN + ALTER TABLE "state_deputy" ADD COLUMN "contact_id" integer; + END IF; + END $$; + + DO $$ + DECLARE + state_deputy_row RECORD; + new_contact_id integer; + created_count integer := 0; + has_legacy_name boolean; + BEGIN + SELECT EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'state_deputy' + AND column_name = 'name' + ) INTO has_legacy_name; + + IF has_legacy_name THEN + FOR state_deputy_row IN + SELECT "id", "name" + FROM "state_deputy" + WHERE "contact_id" IS NULL + ORDER BY "id" + LOOP + INSERT INTO "contact" ("name", "state") + VALUES (state_deputy_row."name", 'BA') + RETURNING "id" INTO new_contact_id; + + UPDATE "state_deputy" + SET "contact_id" = new_contact_id + WHERE "id" = state_deputy_row."id"; + + created_count := created_count + 1; + END LOOP; + END IF; + + RAISE NOTICE 'StateDeputy Contact backfill created % contacts.', created_count; + END $$; + + DO $$ + BEGIN + IF EXISTS (SELECT 1 FROM "state_deputy" WHERE "contact_id" IS NULL) THEN + RAISE EXCEPTION 'StateDeputy Contact backfill left rows without contact_id'; + END IF; + END $$; + + DO $$ + DECLARE + names_mismatch boolean; + BEGIN + IF EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'state_deputy' + AND column_name = 'name' + ) THEN + EXECUTE $check$ + SELECT EXISTS ( + SELECT 1 + FROM "state_deputy" AS state_deputy + JOIN "contact" AS contact ON contact."id" = state_deputy."contact_id" + WHERE contact."name" IS DISTINCT FROM state_deputy."name" + ) + $check$ INTO names_mismatch; + + IF names_mismatch THEN + RAISE EXCEPTION 'StateDeputy Contact backfill changed a legacy name'; + END IF; + END IF; + END $$; + + ALTER TABLE "state_deputy" ALTER COLUMN "contact_id" SET NOT NULL; + + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint WHERE conname = 'state_deputy_contact_id_contact_id_fk' + ) THEN + ALTER TABLE "state_deputy" + ADD CONSTRAINT "state_deputy_contact_id_contact_id_fk" + FOREIGN KEY ("contact_id") REFERENCES "public"."contact"("id") + ON DELETE restrict ON UPDATE no action; + END IF; + END $$; + + CREATE UNIQUE INDEX IF NOT EXISTS "state_deputy_contact_idx" + ON "state_deputy" USING btree ("contact_id"); + DROP INDEX IF EXISTS "state_deputy_name_idx"; + ALTER TABLE "state_deputy" DROP COLUMN IF EXISTS "name";`) +} + +export async function down({ db }: MigrateDownArgs): Promise { + await db.execute(sql` + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'state_deputy' + AND column_name = 'contact_id' + ) AND EXISTS ( + SELECT 1 + FROM "state_deputy" AS state_deputy + JOIN "contact" AS contact ON contact."id" = state_deputy."contact_id" + GROUP BY contact."name" + HAVING COUNT(*) > 1 + ) THEN + RAISE EXCEPTION 'Cannot roll back StateDeputy Contact migration: Contact names are not unique'; + END IF; + END $$; + + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'state_deputy' + AND column_name = 'contact_id' + ) THEN + ALTER TABLE "state_deputy" ADD COLUMN IF NOT EXISTS "name" varchar; + + UPDATE "state_deputy" AS state_deputy + SET "name" = contact."name" + FROM "contact" AS contact + WHERE contact."id" = state_deputy."contact_id"; + + IF EXISTS (SELECT 1 FROM "state_deputy" WHERE "name" IS NULL) THEN + RAISE EXCEPTION 'Cannot roll back StateDeputy Contact migration: a name could not be restored'; + END IF; + + ALTER TABLE "state_deputy" ALTER COLUMN "name" SET NOT NULL; + DROP INDEX IF EXISTS "state_deputy_contact_idx"; + ALTER TABLE "state_deputy" + DROP CONSTRAINT IF EXISTS "state_deputy_contact_id_contact_id_fk"; + ALTER TABLE "state_deputy" DROP COLUMN "contact_id"; + END IF; + END $$; + + CREATE UNIQUE INDEX IF NOT EXISTS "state_deputy_name_idx" + ON "state_deputy" USING btree ("name");`) +} diff --git a/src/migrations/index.ts b/src/migrations/index.ts index 671d8f7b..2f307559 100644 --- a/src/migrations/index.ts +++ b/src/migrations/index.ts @@ -40,6 +40,7 @@ import * as migration_20260731_014319_add_campaign_notifications from './2026073 import * as migration_20260801_062558_add_campaign_vote_summary_snapshot from './20260801_062558_add_campaign_vote_summary_snapshot'; import * as migration_20260802_230000_strip_engagement_reversal_signals from './20260802_230000_strip_engagement_reversal_signals'; import * as migration_20260804_061017_add_state_deputy_advisors from './20260804_061017_add_state_deputy_advisors'; +import * as migration_20260806_082110_add_state_deputy_contact from './20260806_082110_add_state_deputy_contact'; import * as migration_20260806_120000_c14_remodel_activity_agenda from './20260806_120000_c14_remodel_activity_agenda'; export const migrations = [ @@ -251,11 +252,16 @@ export const migrations = [ { up: migration_20260804_061017_add_state_deputy_advisors.up, down: migration_20260804_061017_add_state_deputy_advisors.down, - name: '20260804_061017_add_state_deputy_advisors' + name: '20260804_061017_add_state_deputy_advisors', + }, + { + up: migration_20260806_082110_add_state_deputy_contact.up, + down: migration_20260806_082110_add_state_deputy_contact.down, + name: '20260806_082110_add_state_deputy_contact', }, { up: migration_20260806_120000_c14_remodel_activity_agenda.up, down: migration_20260806_120000_c14_remodel_activity_agenda.down, - name: '20260806_120000_c14_remodel_activity_agenda' + name: '20260806_120000_c14_remodel_activity_agenda', }, ]; diff --git a/src/payload-types.ts b/src/payload-types.ts index a4a0c756..94dd495e 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -452,7 +452,7 @@ export interface Municipality { */ export interface StateDeputy { id: number; - name: string; + contact: number | Contact; slug: string; party?: string | null; notes?: string | null; @@ -461,6 +461,49 @@ export interface StateDeputy { updatedAt: string; createdAt: string; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "contact". + */ +export interface Contact { + id: number; + name: string; + email?: string | null; + phone?: string | null; + gender?: ('feminino' | 'masculino' | 'outro' | 'nao_informado') | null; + state: + | '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'; + city?: string | null; + postalCode?: string | null; + updatedAt: string; + createdAt: string; +} /** * Endpoints Web Push por dispositivo. Não armazena dados biométricos. * @@ -546,49 +589,6 @@ export interface Leadership { updatedAt: string; createdAt: string; } -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "contact". - */ -export interface Contact { - id: number; - name: string; - email?: string | null; - phone?: string | null; - gender?: ('feminino' | 'masculino' | 'outro' | 'nao_informado') | null; - state: - | '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'; - city?: string | null; - postalCode?: string | null; - updatedAt: string; - createdAt: string; -} /** * Sindicatos, associações, movimentos e afins. Concentra lideranças associadas e atividades apoiadas. * @@ -1615,7 +1615,7 @@ export interface OrganizationSelect { * via the `definition` "stateDeputy_select". */ export interface StateDeputySelect { - name?: T; + contact?: T; slug?: T; party?: T; notes?: T; diff --git a/src/utilities/access/contacts.ts b/src/utilities/access/contacts.ts index 45578dc5..e8f852b3 100644 --- a/src/utilities/access/contacts.ts +++ b/src/utilities/access/contacts.ts @@ -53,6 +53,26 @@ const getAccessibleContactIds = ( } } + // Advisors already read every dobradinha. Include the normalized Contacts + // backing those rows without widening access to other Contacts. + if (currentUser.role === 'advisor' && collections.stateDeputy) { + // Intentional admin bypass: StateDeputy access already scopes the advisor's rows. + const stateDeputyResult = await find({ + collection: 'stateDeputy', + depth: 0, + limit: 0, + overrideAccess: true, + pagination: false, + req, + select: { contact: true }, + where: {}, + }) + for (const doc of stateDeputyResult.docs) { + const id = relationshipId(doc.contact) + if (id !== null) contactIDs.push(id) + } + } + // Advisors read contacts of supporters in their municipalities; leaders read // contacts of the supporters they created — mirroring `canReadSupporter`. const supporterWhere = diff --git a/src/utilities/ai/tools/getDobradinhas.ts b/src/utilities/ai/tools/getDobradinhas.ts index 596fd1c3..cb7699c0 100644 --- a/src/utilities/ai/tools/getDobradinhas.ts +++ b/src/utilities/ai/tools/getDobradinhas.ts @@ -3,6 +3,7 @@ import type { Where } from 'payload' import { z } from 'zod' import type { AIToolContext } from '@/lib/ai/types' +import { populatedContactName, relationshipId, uniqueRelationshipIds } from '@/lib/relationship' type Dobradinha = { name: string @@ -29,8 +30,39 @@ export const getDobradinhas = (ctx: AIToolContext) => const { payload } = ctx const andClauses: Where[] = [] + let municipalityContext: { name: string; slug: string } | undefined + + if (municipality) { + const munResult = await payload.find({ + collection: 'municipality', + where: { + or: [ + { name: { equals: municipality } }, + { slug: { equals: municipality.toLowerCase().replace(/\s+/g, '-') } }, + ], + }, + depth: 0, + limit: 1, + pagination: false, + select: { name: true, slug: true, stateDeputies: true }, + overrideAccess: false, + user: ctx.user, + }) + + const mun = munResult.docs[0] + if (!mun) return { message: `Nenhuma dobradinha encontrada em "${municipality}".` } + + const stateDeputyIDs = uniqueRelationshipIds(mun.stateDeputies) + if (stateDeputyIDs.length === 0) { + return { message: `Nenhuma dobradinha encontrada em "${municipality}".` } + } + + municipalityContext = { name: mun.name, slug: mun.slug } + andClauses.push({ id: { in: stateDeputyIDs } }) + } + if (deputyName) { - andClauses.push({ name: { like: deputyName } }) + andClauses.push({ 'contact.name': { like: deputyName } }) } const where: Where = andClauses.length > 0 ? { and: andClauses } : {} @@ -41,57 +73,41 @@ export const getDobradinhas = (ctx: AIToolContext) => depth: 1, limit: 20, pagination: false, - select: { name: true, party: true, slug: true, notes: true }, - sort: 'name', + select: { contact: true, party: true, slug: true, notes: true }, + sort: 'contact.name', overrideAccess: false, user: ctx.user, }) - let deputies: Dobradinha[] = result.docs.map((doc: Record) => ({ - name: doc.name as string, - party: (doc.party as string) ?? null, - slug: doc.slug as string, - municipalities: [], - notes: (doc.notes as string) ?? null, - })) - - // If municipality filter is provided, query the municipality to get its dobradinhas - if (municipality) { - const munResult = await payload.find({ - collection: 'municipality', - where: { - or: [ - { name: { equals: municipality } }, - { slug: { equals: municipality.toLowerCase().replace(/\s+/g, '-') } }, - ], - }, - depth: 1, - limit: 1, + const contactIDs = result.docs + .map((doc) => relationshipId(doc.contact)) + .filter((id): id is number => id !== null) + const contactNames = new Map() + if (contactIDs.length > 0) { + const contacts = await payload.find({ + collection: 'contact', + where: { id: { in: contactIDs } }, + depth: 0, + limit: 0, pagination: false, - select: { name: true, slug: true, stateDeputies: true }, + select: { name: true }, overrideAccess: false, user: ctx.user, }) - - if (munResult.docs.length > 0) { - const mun = munResult.docs[0] as Record - const linkedDeputies = (mun.stateDeputies as Array<{ name: string }>) ?? [] - const linkedNames = new Set(linkedDeputies.map((d) => d.name)) - - // Enrich deputies with municipality info - for (const deputy of deputies) { - if (linkedNames.has(deputy.name)) { - deputy.municipalities.push({ - name: mun.name as string, - slug: mun.slug as string, - }) - } - } - - deputies = deputies.filter((d) => linkedNames.has(d.name)) - } + for (const contact of contacts.docs) contactNames.set(contact.id, contact.name) } + const deputies: Dobradinha[] = result.docs.map((doc: Record) => ({ + name: populatedContactName( + doc.contact, + contactNames.get(relationshipId(doc.contact) ?? -1) ?? 'Contato', + ), + party: (doc.party as string) ?? null, + slug: doc.slug as string, + municipalities: municipalityContext ? [municipalityContext] : [], + notes: (doc.notes as string) ?? null, + })) + if (deputies.length === 0) { if (municipality) { return { message: `Nenhuma dobradinha encontrada em "${municipality}".` } diff --git a/src/utilities/ai/tools/searchEntities.ts b/src/utilities/ai/tools/searchEntities.ts index 0eee7972..ec411d8d 100644 --- a/src/utilities/ai/tools/searchEntities.ts +++ b/src/utilities/ai/tools/searchEntities.ts @@ -2,6 +2,7 @@ import { tool } from 'ai' import { z } from 'zod' import type { AIToolContext } from '@/lib/ai/types' +import { populatedContactName } from '@/lib/relationship' type SearchHit = { collection: string @@ -102,22 +103,24 @@ export const searchEntities = (ctx: AIToolContext) => const depResult = await payload.find({ collection: 'stateDeputy', where: { - name: { like: query }, + 'contact.name': { like: query }, }, - depth: 0, + depth: 1, limit: 5, pagination: false, - select: { name: true, party: true }, - sort: 'name', + select: { contact: true, party: true }, + sort: 'contact.name', overrideAccess: false, user: ctx.user, }) for (const doc of depResult.docs as Array>) { + const name = populatedContactName(doc.contact) hits.push({ collection: 'stateDeputy', - label: doc.name as string, + label: name, description: `Dobradinha — ${(doc.party as string) ?? 'sem partido'}`, + href: `/campanha/dobradinhas/${doc.id as number}`, }) } diff --git a/src/utilities/campaignRelationOptions.ts b/src/utilities/campaignRelationOptions.ts index ea15e838..f8d4ce7e 100644 --- a/src/utilities/campaignRelationOptions.ts +++ b/src/utilities/campaignRelationOptions.ts @@ -48,7 +48,7 @@ export const loadOrganizationOptions = async ( * `name` folds in the party for select/typeahead disambiguation (e.g. * `RelationMultiSelect`); `plainName`/`party`/`slug` ride along so cell-level * consumers (B31's "Dobradinhas" column) can build a `StateDeputySummary` - * chip for an optimistic add and link to `/campanha/dobradinhas/` + * chip for an optimistic add and link to `/campanha/dobradinhas/` * without a second catalog fetch. */ export type StateDeputyRelationOption = RelationOption & { @@ -63,22 +63,25 @@ export const loadStateDeputyOptions = async ( ): Promise => { const result = await payload.find({ collection: 'stateDeputy', - depth: 0, + depth: 1, limit: 0, pagination: false, - sort: 'name', - select: { name: true, party: true, slug: true }, + sort: 'contact.name', + select: { contact: true, party: true, slug: true }, where: {}, user, overrideAccess: false, }) - return result.docs.map((stateDeputy) => ({ - id: stateDeputy.id, - name: stateDeputy.party ? `${stateDeputy.name} (${stateDeputy.party})` : stateDeputy.name, - plainName: stateDeputy.name, - party: stateDeputy.party ?? null, - slug: stateDeputy.slug, - })) + return result.docs.map((stateDeputy) => { + const plainName = populatedContactName(stateDeputy.contact) + return { + id: stateDeputy.id, + name: stateDeputy.party ? `${plainName} (${stateDeputy.party})` : plainName, + plainName, + party: stateDeputy.party ?? null, + slug: stateDeputy.slug, + } + }) } /** diff --git a/src/utilities/contactPhoneInvariant.ts b/src/utilities/contactPhoneInvariant.ts index 8f6a65c4..0f8fbef9 100644 --- a/src/utilities/contactPhoneInvariant.ts +++ b/src/utilities/contactPhoneInvariant.ts @@ -3,7 +3,10 @@ import 'server-only' import type { Payload, PayloadRequest } from 'payload' import type { PayloadTransactionRequest } from '@/utilities/payloadTransaction' -import { acquireTextAdvisoryLocks } from '@/utilities/postgresTransactionLocks' +import { + acquireTextAdvisoryLocks, + POSTGRES_DEDUP_LOCK_MESSAGE, +} from '@/utilities/postgresTransactionLocks' export const CONTACT_PHONE_CONFLICT_MESSAGE = 'Já existe outro contato com este celular.' @@ -14,7 +17,7 @@ export const CONTACT_PHONE_CONFLICT_MESSAGE = 'Já existe outro contato com este export const CONTACT_PHONE_AMBIGUOUS_MESSAGE = 'Existe mais de um contato com este celular. Resolva a duplicidade no admin antes de continuar.' -type ContactPhonePayload = Pick +type ContactPhonePayload = Pick type ContactPhoneRequest = PayloadTransactionRequest | PayloadRequest export const contactPhoneLockKeys = (phones: string[]): string[] => @@ -52,3 +55,60 @@ export const assertContactPhoneAvailable = async ( }) if (conflicts.totalDocs > 0) throw new Error(conflictMessage) } + +/** + * Checks the phone invariant for an already-authorized Contact update. The + * caller must prove access to the owning campaign join before using the + * intentional admin-bypass reads/writes here. + */ +export const assertContactPhoneWritable = async ( + payload: ContactPhonePayload, + req: ContactPhoneRequest, + contactID: number, + phone: string, +): Promise => { + if (payload.db.name !== 'postgres') { + throw new Error(POSTGRES_DEDUP_LOCK_MESSAGE) + } + + const currentContact = await payload.findByID({ + collection: 'contact', + id: contactID, + depth: 0, + select: { phone: true }, + // Intentional admin bypass: the caller has already proved ownership through + // the campaign join; this read only obtains the old value for lock ordering. + overrideAccess: true, + req, + }) + const oldPhone = currentContact.phone ?? undefined + + // Lock both sides before checking availability. This prevents concurrent + // swaps (A -> B and B -> A) from waiting on each other's hook locks. + await acquireContactPhoneLocks( + payload, + req, + [oldPhone, phone].filter((value): value is string => Boolean(value)), + ) + + const contactsWithPhone = await payload.find({ + collection: 'contact', + where: { phone: { equals: phone } }, + depth: 0, + limit: 2, + pagination: false, + // Intentional admin bypass: phone uniqueness must see every Contact, + // including one outside the actor's campaign scope. + overrideAccess: true, + req, + }) + + if (contactsWithPhone.totalDocs > 1) { + throw new Error(CONTACT_PHONE_AMBIGUOUS_MESSAGE) + } + + const phoneOwner = contactsWithPhone.docs[0] + if (phoneOwner && phoneOwner.id !== contactID) { + await assertContactPhoneAvailable(payload, req, phone, contactID) + } +} diff --git a/src/utilities/homeSearch/searchHomeStateDeputies.ts b/src/utilities/homeSearch/searchHomeStateDeputies.ts index 09ec52e3..69ace92e 100644 --- a/src/utilities/homeSearch/searchHomeStateDeputies.ts +++ b/src/utilities/homeSearch/searchHomeStateDeputies.ts @@ -14,6 +14,7 @@ import { compareHomeSearchNameRelevance, normalizeHomeSearchName, } from '@/lib/homeSearchMunicipalityMatch' +import { populatedContactName } from '@/lib/relationship' import { matchesNormalizedAtWordStart } from '@/lib/wordStartFilter' import type { CampaignUser } from '@/payload-types' import { municipalityIdsByStateDeputyIds } from '@/utilities/stateDeputyData' @@ -39,13 +40,13 @@ export const searchHomeStateDeputies = async ( const result = await payload.find({ collection: 'stateDeputy', where: { - or: [{ name: { contains: query } }, { party: { contains: query } }], + or: [{ 'contact.name': { contains: query } }, { party: { contains: query } }], }, - depth: 0, + depth: 1, limit: 0, pagination: false, - sort: 'name', - select: { name: true, slug: true, party: true }, + sort: 'contact.name', + select: { contact: true, slug: true, party: true }, user, overrideAccess: false, }) @@ -59,7 +60,8 @@ export const searchHomeStateDeputies = async ( }> = [] for (const doc of result.docs) { - const normalizedName = normalizeHomeSearchName(doc.name) + const name = populatedContactName(doc.contact) + const normalizedName = normalizeHomeSearchName(name) const normalizedParty = doc.party ? normalizeHomeSearchName(doc.party) : '' const nameMatch = matchesNormalizedAtWordStart(normalizedName, normalizedQuery) const partyMatch = @@ -69,7 +71,7 @@ export const searchHomeStateDeputies = async ( candidates.push({ normalizedName: nameMatch ? normalizedName : normalizedParty, slug: doc.slug, - name: doc.name, + name, party: doc.party ?? null, id: doc.id, }) @@ -91,6 +93,7 @@ export const searchHomeStateDeputies = async ( normalizedName: row.normalizedName, tieBreakDesc: municipalityCount, hit: { + id: row.id, slug: row.slug, name: row.name, party: row.party, diff --git a/src/utilities/stateDeputy/nameInvariant.ts b/src/utilities/stateDeputy/nameInvariant.ts new file mode 100644 index 00000000..321255a6 --- /dev/null +++ b/src/utilities/stateDeputy/nameInvariant.ts @@ -0,0 +1,44 @@ +import 'server-only' + +import type { Payload, PayloadRequest } from 'payload' + +import { STATE_DEPUTY_CONFLICT_MESSAGE } from '@/lib/schemas/stateDeputy' +import { acquireTextAdvisoryLocks } from '@/utilities/postgresTransactionLocks' + +type StateDeputyNameRequest = Pick + +/** + * Keeps the historical unique-name rule at the StateDeputy boundary even + * though the name now lives on Contact and Contact is shared by other joins. + */ +export const assertStateDeputyNameAvailable = async ( + payload: Pick, + req: StateDeputyNameRequest, + name: string, + stateDeputyID?: number, +): Promise => { + const normalizedName = name.trim() + + if (req.transactionID !== undefined && req.transactionID !== null) { + await acquireTextAdvisoryLocks(payload, req, [`state-deputy-name:${normalizedName}`]) + } + + const conflicts = await payload.find({ + collection: 'stateDeputy', + where: { + and: [ + { 'contact.name': { equals: normalizedName } }, + ...(stateDeputyID === undefined ? [] : [{ id: { not_equals: stateDeputyID } }]), + ], + }, + depth: 0, + limit: 1, + pagination: false, + // Intentional bypass: the invariant must see rows outside the current + // advisor scope. + overrideAccess: true, + req, + }) + + if (conflicts.totalDocs > 0) throw new Error(STATE_DEPUTY_CONFLICT_MESSAGE) +} diff --git a/src/utilities/stateDeputyData.ts b/src/utilities/stateDeputyData.ts index a2acc05d..da8d0c69 100644 --- a/src/utilities/stateDeputyData.ts +++ b/src/utilities/stateDeputyData.ts @@ -22,6 +22,8 @@ type NamedRelationSummary = { export type StateDeputyRowViewModel = { id: number name: string + email: string | null + phone: string | null slug: string party: string | null municipalityIDs: number[] @@ -30,6 +32,21 @@ export type StateDeputyRowViewModel = { advisors: NamedRelationSummary[] } +const stateDeputyContactSummary = ( + contact: StateDeputy['contact'], +): { id: number; name: string; email: string | null; phone: string | null } => { + if (typeof contact === 'object' && contact !== null) { + return { + id: contact.id, + name: contact.name, + email: contact.email ?? null, + phone: contact.phone ?? null, + } + } + + return { id: Number(contact), name: 'Contato', email: null, phone: null } +} + /** Values still reachable under the OTHER active filters (the Partido popover). */ export type StateDeputyListFilterFacets = { parties: string[] @@ -125,11 +142,11 @@ export const loadStateDeputyListPageData = async ( payload.find({ collection: 'stateDeputy', where: buildStateDeputyListWhere(state), - depth: 0, + depth: 1, limit: stateDeputyPageSize, page: state.page, sort: resolveStateDeputyListPayloadSort(sort, dir), - select: { name: true, slug: true, party: true, advisors: true }, + select: { contact: true, slug: true, party: true, advisors: true }, user, overrideAccess: false, }), @@ -199,8 +216,8 @@ export const loadStateDeputyListPageData = async ( return { rows: result.docs.map((doc) => ({ + ...stateDeputyContactSummary(doc.contact), id: doc.id, - name: doc.name, slug: doc.slug, party: doc.party ?? null, municipalityIDs: municipalityIDsByDeputy.get(doc.id) ?? [], @@ -221,6 +238,8 @@ export type StateDeputySummary = { } export type StateDeputyDetailViewModel = StateDeputySummary & { + email: string | null + phone: string | null notes: string | null municipalities: Array<{ id: number; name: string; slug: string }> leaderships: Array<{ id: number; name: string }> @@ -231,18 +250,29 @@ export type StateDeputyDetailViewModel = StateDeputySummary & { export const loadStateDeputyDetail = async ( payload: Payload, user: CampaignUser, - slug: string, + identifier: string, ): Promise => { + const numericCandidate = /^[1-9]\d*$/.test(identifier) ? Number(identifier) : null + const numericID = + numericCandidate !== null && Number.isSafeInteger(numericCandidate) ? numericCandidate : null const result = await payload.find({ collection: 'stateDeputy', - where: { slug: { equals: slug } }, - depth: 0, - limit: 1, + where: + numericID === null + ? { slug: { equals: identifier } } + : { or: [{ id: { equals: numericID } }, { slug: { equals: identifier } }] }, + depth: 1, + limit: numericID === null ? 1 : 2, pagination: false, user, overrideAccess: false, }) - const stateDeputy = result.docs[0] as StateDeputy | undefined + // Numeric legacy slugs are rare but valid; an actual ID wins when both exist. + const stateDeputy = ( + numericID === null + ? result.docs[0] + : (result.docs.find((doc) => doc.id === numericID) ?? result.docs[0]) + ) as StateDeputy | undefined if (!stateDeputy) return null const [municipalities, leaderships, advisorSummaries] = await Promise.all([ @@ -271,8 +301,8 @@ export const loadStateDeputyDetail = async ( ]) return { + ...stateDeputyContactSummary(stateDeputy.contact), id: stateDeputy.id, - name: stateDeputy.name, slug: stateDeputy.slug, party: stateDeputy.party ?? null, notes: stateDeputy.notes ?? null, @@ -298,11 +328,11 @@ export const loadStateDeputySummaries = async ( const result = await payload.find({ collection: 'stateDeputy', where: { id: { in: ids } }, - depth: 0, + depth: 1, limit: 0, pagination: false, - sort: 'name', - select: { name: true, slug: true, party: true }, + sort: 'contact.name', + select: { contact: true, slug: true, party: true }, // Intentional admin bypass: id lookups for the home-search card, no // actor-scoping possible or needed (B52 precedent). overrideAccess: true, @@ -313,7 +343,7 @@ export const loadStateDeputySummaries = async ( doc.id, { id: doc.id, - name: doc.name, + name: stateDeputyContactSummary(doc.contact).name, slug: doc.slug, party: doc.party ?? null, }, diff --git a/src/utilities/stateDeputyListUrl.ts b/src/utilities/stateDeputyListUrl.ts index 3e05733c..94d86260 100644 --- a/src/utilities/stateDeputyListUrl.ts +++ b/src/utilities/stateDeputyListUrl.ts @@ -72,7 +72,10 @@ export const resolveStateDeputyListSort = ( export const resolveStateDeputyListPayloadSort = ( sort: StateDeputyListSortKey, dir: StateDeputyListSortDirection, -): string => (dir === 'desc' ? `-${sort}` : sort) +): string => { + const path = sort === 'name' ? 'contact.name' : sort + return dir === 'desc' ? `-${path}` : path +} export const formatStateDeputyListSortSummary = ( sort: StateDeputyListSortKey, @@ -122,7 +125,7 @@ export const parseStateDeputyListParams = ( export const buildStateDeputyListWhere = (state: StateDeputyListState): Where => { const filters: Where[] = [] - if (state.q) filters.push({ name: { contains: state.q } }) + if (state.q) filters.push({ 'contact.name': { contains: state.q } }) if (state.parties?.length) { const namedParties = state.parties.filter((party) => party !== NO_PARTY_FILTER_VALUE) diff --git a/tests/helpers/campaignFixtures.ts b/tests/helpers/campaignFixtures.ts index 42f27e5b..3f5ce725 100644 --- a/tests/helpers/campaignFixtures.ts +++ b/tests/helpers/campaignFixtures.ts @@ -93,7 +93,9 @@ type VotePledgeInput = Partial< type OrganizationInput = Partial< Pick > -type StateDeputyInput = Partial> +type StateDeputyInput = Partial> & { + name?: string +} type CampaignDemandInput = Partial< Pick< CampaignDemand, @@ -730,13 +732,15 @@ export class CampaignFixtures { } async createStateDeputy(input: StateDeputyInput = {}): Promise { - const name = input.name ?? this.value('Deputado') + const { contact: inputContact, name, ...stateDeputyInput } = input + const contact = + inputContact ?? (await this.createContact({ name: name ?? this.value('Deputado') })) const stateDeputy = await this.rootPayload.create({ collection: 'stateDeputy', data: { - ...input, - name, - slug: input.slug ?? this.value('deputado'), + ...stateDeputyInput, + contact: relationId(contact), + slug: stateDeputyInput.slug ?? this.value('deputado'), }, depth: 0, }) diff --git a/tests/int/campaignEntityListData.int.spec.ts b/tests/int/campaignEntityListData.int.spec.ts index 94f8a79b..d9e68ff6 100644 --- a/tests/int/campaignEntityListData.int.spec.ts +++ b/tests/int/campaignEntityListData.int.spec.ts @@ -14,7 +14,7 @@ import { loadAdvisorListPageData } from '@/utilities/advisorData' import { loadDemandListPageData } from '@/utilities/campaignDemandData' import { loadLeadershipListPageData } from '@/utilities/leadership/leadershipData' import { loadOrganizationListPageData } from '@/utilities/organizationData' -import { loadStateDeputyListPageData } from '@/utilities/stateDeputyData' +import { loadStateDeputyDetail, loadStateDeputyListPageData } from '@/utilities/stateDeputyData' import { NO_PARTY_FILTER_VALUE } from '@/utilities/stateDeputyListUrl' import { installCampaignFixtures } from '../helpers/campaignFixtures' @@ -312,11 +312,12 @@ describe('loadStateDeputyListPageData', () => { it('narrows by q with zeroed relation counts for a fresh deputy', async () => { const fixtures = campaignFixtures() const coordinator = await fixtures.createCampaignUser('coordinator') - const stateDeputy = await fixtures.createStateDeputy({ party: 'PT' }) + const deputyName = fixtures.value('Deputado') + const stateDeputy = await fixtures.createStateDeputy({ name: deputyName, party: 'PT' }) const result = await loadStateDeputyListPageData(payload, coordinator, { page: 1, - q: stateDeputy.name, + q: deputyName, }) expect(result.totalDocs).toBe(1) @@ -333,7 +334,8 @@ describe('loadStateDeputyListPageData', () => { const fixtures = campaignFixtures() const coordinator = await fixtures.createCampaignUser('coordinator') const municipality = await fixtures.getMunicipality() - const stateDeputy = await fixtures.createStateDeputy({ party: 'PT' }) + const deputyName = fixtures.value('Deputado') + const stateDeputy = await fixtures.createStateDeputy({ name: deputyName, party: 'PT' }) const contact = await fixtures.createContact() const leadership = await fixtures.createLeadership({ contact: contact.id, @@ -349,7 +351,7 @@ describe('loadStateDeputyListPageData', () => { const result = await loadStateDeputyListPageData(payload, coordinator, { page: 1, - q: stateDeputy.name, + q: deputyName, }) expect(result.rows[0]).toMatchObject({ @@ -438,6 +440,19 @@ describe('loadStateDeputyListPageData', () => { }) }) +describe('loadStateDeputyDetail', () => { + it('resolves an oversized numeric legacy slug without coercing it to an ID', async () => { + const fixtures = campaignFixtures() + const coordinator = await fixtures.createCampaignUser('coordinator') + const name = '12345678901234567890' + const stateDeputy = await fixtures.createStateDeputy({ name }) + + const result = await loadStateDeputyDetail(payload, coordinator, stateDeputy.slug) + + expect(result).toMatchObject({ id: stateDeputy.id, name }) + }) +}) + describe('loadDemandListPageData', () => { it('lists staff demands with municipality labels, open count and status filter', async () => { const fixtures = campaignFixtures() diff --git a/tests/int/campaignLeadershipStateDeputyMembership.int.spec.ts b/tests/int/campaignLeadershipStateDeputyMembership.int.spec.ts index e9fc4d18..160bab5f 100644 --- a/tests/int/campaignLeadershipStateDeputyMembership.int.spec.ts +++ b/tests/int/campaignLeadershipStateDeputyMembership.int.spec.ts @@ -39,14 +39,14 @@ describe('setLeadershipStateDeputyMembershipRecord (B31)', () => { }) const stateDeputy = await fixtures.createStateDeputy() - const { leadership: assigned, stateDeputySlug } = + const { leadership: assigned, stateDeputyID } = await setLeadershipStateDeputyMembershipRecord(payload, actor, { leadershipId: leadership.id, stateDeputyId: stateDeputy.id, assigned: true, }) expect(stateDeputyIds(assigned)).toContain(stateDeputy.id) - expect(stateDeputySlug).toBe(stateDeputy.slug) + expect(stateDeputyID).toBe(stateDeputy.id) const { leadership: removed } = await setLeadershipStateDeputyMembershipRecord( payload, diff --git a/tests/int/campaignMigrationReconciliation.int.spec.ts b/tests/int/campaignMigrationReconciliation.int.spec.ts index 64fecedf..fe0feb92 100644 --- a/tests/int/campaignMigrationReconciliation.int.spec.ts +++ b/tests/int/campaignMigrationReconciliation.int.spec.ts @@ -19,6 +19,7 @@ const testDatabaseUrl = process.env.DATABASE_URL ?? 'postgresql://teqo:teqo@localhost:5432/teqo_test' const adminUrl = testDatabaseUrl.replace(/\/[^/]+$/, '/postgres') const consolidatedName = '20260718_010733_consolidate_campaign_schema' +const stateDeputyContactMigrationName = '20260806_082110_add_state_deputy_contact' type MigrationDatabase = MigrateUpArgs['db'] let databaseSequence = 0 let databaseName = '' @@ -187,4 +188,86 @@ describe('campaign migration existing-schema reconciliation', () => { `) expect(sentinel.rows[0]?.count).toBe(1) }) + + it('backfills StateDeputy Contacts and remains idempotent', async () => { + if (!database) throw new Error('Disposable database is not initialized.') + const migration = migrations.find(({ name }) => name === stateDeputyContactMigrationName) + if (!migration) throw new Error('StateDeputy Contact migration is not registered.') + + await migration.down(stub({ db: database })) + await database.execute(sql` + INSERT INTO "state_deputy" ("name", "slug") + VALUES ('Legado Um', 'legado-um'), ('Legado Dois', 'legado-dois') + `) + const legacyRead = await database.execute(sql` + SELECT "name" AS legacy_name + FROM "state_deputy" + WHERE "slug" IN ('legado-um', 'legado-dois') + ORDER BY "id" + `) + expect(legacyRead.rows).toEqual([{ legacy_name: 'Legado Um' }, { legacy_name: 'Legado Dois' }]) + + await migration.up(stub({ db: database })) + const firstRead = await database.execute(sql` + SELECT contact."name" AS contact_name + FROM "state_deputy" AS state_deputy + JOIN "contact" AS contact ON contact."id" = state_deputy."contact_id" + ORDER BY state_deputy."id" + `) + expect(firstRead.rows).toEqual([{ contact_name: 'Legado Um' }, { contact_name: 'Legado Dois' }]) + + await migration.up(stub({ db: database })) + const contactCount = await database.execute(sql` + SELECT count(*)::integer AS count + FROM "contact" + WHERE "name" IN ('Legado Um', 'Legado Dois') + `) + expect(contactCount.rows[0]?.count).toBe(2) + }) + + it('keeps the required StateDeputy Contact relation from being nulled on delete', async () => { + if (!database) throw new Error('Disposable database is not initialized.') + + const result = await database.execute(sql` + SELECT confdeltype + FROM pg_constraint + WHERE conname = 'state_deputy_contact_id_contact_id_fk' + `) + + expect(result.rows).toEqual([{ confdeltype: 'r' }]) + }) + + it('refuses rollback after Contact names diverge into a duplicate', async () => { + if (!database) throw new Error('Disposable database is not initialized.') + const migration = migrations.find(({ name }) => name === stateDeputyContactMigrationName) + if (!migration) throw new Error('StateDeputy Contact migration is not registered.') + + const contacts = await database.execute(sql` + INSERT INTO "contact" ("name", "state") + VALUES ('Mesmo Nome', 'BA'), ('Outro Nome', 'BA') + RETURNING "id" + `) + await database.execute(sql` + INSERT INTO "state_deputy" ("contact_id", "slug") + VALUES (${contacts.rows[0]?.id}, 'mesmo-nome-um'), (${contacts.rows[1]?.id}, 'mesmo-nome-dois') + `) + await database.execute(sql` + UPDATE "contact" + SET "name" = 'Mesmo Nome' + WHERE "id" = ${contacts.rows[1]?.id} + `) + + await expect(migration.down(stub({ db: database }))).rejects.toThrow( + 'Contact names are not unique', + ) + const columns = await database.execute(sql` + SELECT column_name + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'state_deputy' + AND column_name IN ('name', 'contact_id') + ORDER BY column_name + `) + expect(columns.rows).toEqual([{ column_name: 'contact_id' }]) + }) }) diff --git a/tests/int/campaignMunicipalityStateDeputyCreate.int.spec.ts b/tests/int/campaignMunicipalityStateDeputyCreate.int.spec.ts index 9e4fe090..20c75540 100644 --- a/tests/int/campaignMunicipalityStateDeputyCreate.int.spec.ts +++ b/tests/int/campaignMunicipalityStateDeputyCreate.int.spec.ts @@ -4,9 +4,14 @@ import type { Payload } from 'payload' import { getPayload } from 'payload' import { beforeAll, describe, expect, it } from 'vitest' -import { createMunicipalityStateDeputyRecord } from '@/app/(campaign)/campanha/actions/stateDeputy' +import { + createMunicipalityStateDeputyRecord, + updateStateDeputyContactRecord, + updateStateDeputyPartyRecord, +} from '@/app/(campaign)/campanha/actions/stateDeputy' import { STATE_DEPUTY_CONFLICT_MESSAGE } from '@/lib/schemas/stateDeputy' import config from '@/payload.config' +import { CONTACT_PHONE_CONFLICT_MESSAGE } from '@/utilities/contactPhoneInvariant' import { installCampaignFixtures, relationIds } from '../helpers/campaignFixtures' @@ -32,8 +37,8 @@ const stateDeputyIdsOf = async (municipalityID: number): Promise => { const stateDeputyByName = async (name: string) => { const result = await payload.find({ collection: 'stateDeputy', - where: { name: { equals: name } }, - depth: 0, + where: { 'contact.name': { equals: name } }, + depth: 1, limit: 1, pagination: false, overrideAccess: true, @@ -62,7 +67,7 @@ describe('createMunicipalityStateDeputyRecord (B157)', () => { fixtures.touchMunicipality(municipality.id) expect(municipalitySlug).toBe(municipality.slug) - expect(stateDeputy.name).toBe(name) + expect((await stateDeputyByName(name))?.contact).toMatchObject({ name }) expect(stateDeputy.party).toBeNull() expect(await stateDeputyIdsOf(municipality.id)).toEqual([stateDeputy.id]) }, @@ -84,7 +89,7 @@ describe('createMunicipalityStateDeputyRecord (B157)', () => { }) fixtures.touchMunicipality(municipality.id) - expect(stateDeputy.name).toBe(name) + expect((await stateDeputyByName(name))?.contact).toMatchObject({ name }) expect(stateDeputy.party).toBe(party) expect((await stateDeputyByName(name))?.party).toBe(party) }) @@ -99,7 +104,7 @@ describe('createMunicipalityStateDeputyRecord (B157)', () => { municipalityId: municipality.id, rawName: '(PT)', }), - ).rejects.toThrow(/entre 2 e 160/) + ).rejects.toThrow(/entre 2 e 120/) expect(await stateDeputyIdsOf(municipality.id)).toEqual([]) }) @@ -107,12 +112,13 @@ describe('createMunicipalityStateDeputyRecord (B157)', () => { const fixtures = campaignFixtures() const coordinator = await fixtures.createCampaignUser('coordinator') const municipality = await fixtures.getMunicipality() - const existing = await fixtures.createStateDeputy() + const existingName = fixtures.value('Deputado') + await fixtures.createStateDeputy({ name: existingName }) await expect( createMunicipalityStateDeputyRecord(payload, coordinator, { municipalityId: municipality.id, - rawName: existing.name, + rawName: existingName, }), ).rejects.toThrow(STATE_DEPUTY_CONFLICT_MESSAGE) expect(await stateDeputyIdsOf(municipality.id)).toEqual([]) @@ -133,6 +139,15 @@ describe('createMunicipalityStateDeputyRecord (B157)', () => { // The create must roll back with the failed assign — no orphan deputy. expect(await stateDeputyByName(name)).toBeUndefined() + const orphanContacts = await payload.find({ + collection: 'contact', + where: { name: { equals: name } }, + depth: 0, + limit: 0, + pagination: false, + overrideAccess: true, + }) + expect(orphanContacts.totalDocs).toBe(0) expect(await stateDeputyIdsOf(outside.id)).toEqual([]) }) @@ -173,3 +188,112 @@ describe('createMunicipalityStateDeputyRecord (B157)', () => { ) }) }) + +describe('StateDeputy contact fields (B163)', () => { + it('updates Contact fields and party without changing the legacy slug', async () => { + const fixtures = campaignFixtures() + const coordinator = await fixtures.createCampaignUser('coordinator') + const stateDeputy = await fixtures.createStateDeputy({ name: 'Nome Original', party: 'PT' }) + const legacySlug = stateDeputy.slug + const phone = fixtures.phone() + + await updateStateDeputyContactRecord(payload, coordinator, { + id: stateDeputy.id, + field: 'name', + name: 'Nome Corrigido', + }) + await updateStateDeputyContactRecord(payload, coordinator, { + id: stateDeputy.id, + field: 'email', + email: 'corrigido@example.com', + }) + await updateStateDeputyContactRecord(payload, coordinator, { + id: stateDeputy.id, + field: 'phone', + phone, + }) + await updateStateDeputyPartyRecord(payload, coordinator, { + id: stateDeputy.id, + party: 'PSD', + }) + + const updated = await payload.findByID({ + collection: 'stateDeputy', + id: stateDeputy.id, + depth: 1, + overrideAccess: true, + }) + expect(updated.slug).toBe(legacySlug) + expect(updated.party).toBe('PSD') + expect(updated.contact).toMatchObject({ + name: 'Nome Corrigido', + email: 'corrigido@example.com', + phone, + }) + }) + + it('rejects a phone already owned by another Contact', async () => { + const fixtures = campaignFixtures() + const coordinator = await fixtures.createCampaignUser('coordinator') + const stateDeputy = await fixtures.createStateDeputy({ name: 'Dobradinha Principal' }) + const takenPhone = fixtures.phone() + await fixtures.createContact({ phone: takenPhone }) + + await expect( + updateStateDeputyContactRecord(payload, coordinator, { + id: stateDeputy.id, + field: 'phone', + phone: takenPhone, + }), + ).rejects.toThrow(CONTACT_PHONE_CONFLICT_MESSAGE) + }) + + it('rejects renaming a dobradinha to another dobradinha name', async () => { + const fixtures = campaignFixtures() + const coordinator = await fixtures.createCampaignUser('coordinator') + await fixtures.createStateDeputy({ name: 'Nome já usado' }) + const stateDeputy = await fixtures.createStateDeputy({ name: 'Nome atual' }) + + await expect( + updateStateDeputyContactRecord(payload, coordinator, { + id: stateDeputy.id, + field: 'name', + name: 'Nome já usado', + }), + ).rejects.toThrow(STATE_DEPUTY_CONFLICT_MESSAGE) + + const unchanged = await payload.findByID({ + collection: 'stateDeputy', + id: stateDeputy.id, + depth: 1, + overrideAccess: true, + }) + expect(unchanged.contact).toMatchObject({ name: 'Nome atual' }) + }) + + it('keeps the name invariant when an admin edits the linked Contact directly', async () => { + const fixtures = campaignFixtures() + const first = await fixtures.createStateDeputy({ name: 'Nome já usado' }) + const second = await fixtures.createStateDeputy({ name: 'Nome atual' }) + const secondContactID = typeof second.contact === 'object' ? second.contact.id : second.contact + + await expect( + payload.update({ + collection: 'contact', + id: secondContactID, + data: { name: 'Nome já usado' }, + depth: 0, + overrideAccess: true, + }), + ).rejects.toThrow(STATE_DEPUTY_CONFLICT_MESSAGE) + + const unchanged = await payload.findByID({ + collection: 'contact', + id: secondContactID, + depth: 0, + overrideAccess: true, + }) + expect(unchanged.name).toBe('Nome atual') + expect(first.id).not.toBe(second.id) + }) +}) diff --git a/tests/int/campaignStateDeputyAdvisorMembership.int.spec.ts b/tests/int/campaignStateDeputyAdvisorMembership.int.spec.ts index f1cd4b08..b077f6d1 100644 --- a/tests/int/campaignStateDeputyAdvisorMembership.int.spec.ts +++ b/tests/int/campaignStateDeputyAdvisorMembership.int.spec.ts @@ -33,12 +33,12 @@ describe('setStateDeputyAdvisorMembershipRecord (B156)', () => { const stateDeputy = await fixtures.createStateDeputy() const advisor = await fixtures.createCampaignUser('advisor') - const { stateDeputySlug } = await setStateDeputyAdvisorMembershipRecord(payload, actor, { + const { stateDeputyID } = await setStateDeputyAdvisorMembershipRecord(payload, actor, { stateDeputyId: stateDeputy.id, advisorId: advisor.id, assigned: true, }) - expect(stateDeputySlug).toBe(stateDeputy.slug) + expect(stateDeputyID).toBe(stateDeputy.id) const assigned = await payload.findByID({ collection: 'stateDeputy', @@ -117,13 +117,13 @@ describe('setStateDeputyAdvisorMembershipRecord (B156)', () => { assigned: true, }) - // No-op contract: no slug back (the caller then skips the revalidate). - const { stateDeputySlug } = await setStateDeputyAdvisorMembershipRecord(payload, coordinator, { + // No-op contract: no id back (the caller then skips the revalidate). + const { stateDeputyID } = await setStateDeputyAdvisorMembershipRecord(payload, coordinator, { stateDeputyId: stateDeputy.id, advisorId: advisor.id, assigned: true, }) - expect(stateDeputySlug).toBeUndefined() + expect(stateDeputyID).toBeUndefined() const after = await payload.findByID({ collection: 'stateDeputy', diff --git a/tests/unit/campaignInlineEditableCell.unit.spec.tsx b/tests/unit/campaignInlineEditableCell.unit.spec.tsx new file mode 100644 index 00000000..e43d8124 --- /dev/null +++ b/tests/unit/campaignInlineEditableCell.unit.spec.tsx @@ -0,0 +1,158 @@ +import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const router = vi.hoisted(() => ({ refresh: vi.fn() })) +const errorToast = vi.hoisted(() => vi.fn()) + +vi.mock('next/navigation', () => ({ useRouter: () => router })) +vi.mock('sonner', () => ({ toast: { error: errorToast } })) + +import { CampaignInlineEditableCell } from '@/components/campaign/shared/CampaignInlineEditableCell' +import type { CampaignFormActionState } from '@/utilities/campaignFormActionError' + +afterEach(cleanup) + +beforeEach(() => { + router.refresh.mockReset() + errorToast.mockReset() +}) + +describe('CampaignInlineEditableCell (B163)', () => { + it('keeps the name link separate from the cell edit trigger', () => { + const formAction = vi.fn( + async ( + _state: CampaignFormActionState, + _formData: FormData, + ): Promise => ({ + status: 'success', + message: 'Salvo.', + }), + ) + + render( + , + ) + + const link = screen.getByRole('link', { name: 'Ana Bastos' }) + expect(link.getAttribute('href')).toBe('/campanha/dobradinhas/7') + expect(link.closest('[role="button"]')).toBeNull() + expect(link.parentElement?.className).toContain('pointer-events-none') + expect(link.className).toContain('pointer-events-auto') + + expect(screen.queryByRole('textbox', { name: 'Nome' })).toBeNull() + + fireEvent.click(screen.getByRole('button', { name: 'Editar nome' })) + expect(screen.getByRole('textbox', { name: 'Nome' })).toBeTruthy() + }) + + it('keeps a failed save in the editor and exposes the error locally', async () => { + const formAction = vi.fn( + async ( + _state: CampaignFormActionState, + _formData: FormData, + ): Promise => ({ + message: 'Telefone já usado.', + }), + ) + + render( + , + ) + + fireEvent.click(screen.getByRole('button', { name: 'Editar telefone' })) + const input = screen.getByRole('textbox', { name: 'Telefone' }) + fireEvent.change(input, { target: { value: '71999990000' } }) + fireEvent.blur(input) + + await waitFor(() => + expect(screen.getByRole('alert').textContent).toContain('Telefone já usado.'), + ) + expect(screen.getByRole('textbox', { name: 'Telefone' })).toBeTruthy() + expect(errorToast).toHaveBeenCalledWith('Telefone já usado.') + }) + + it('renders field validation errors from the action beside the active editor', async () => { + const formAction = vi.fn( + async ( + _state: CampaignFormActionState, + _formData: FormData, + ): Promise => ({ + fieldErrors: { email: ['E-mail inválido.'] }, + }), + ) + + render( + , + ) + + fireEvent.click(screen.getByRole('button', { name: 'Editar e-mail' })) + const input = screen.getByRole('textbox', { name: 'E-mail' }) + fireEvent.change(input, { target: { value: 'invalid' } }) + fireEvent.blur(input) + + await waitFor(() => expect(screen.getByRole('alert').textContent).toContain('E-mail inválido.')) + expect(screen.getByRole('textbox', { name: 'E-mail' })).toBeTruthy() + }) + + it('shows local success feedback after a saved edit', async () => { + const formAction = vi.fn( + async ( + _state: CampaignFormActionState, + _formData: FormData, + ): Promise => ({ + status: 'success', + message: 'Salvo.', + }), + ) + + render( + , + ) + + fireEvent.click(screen.getByRole('button', { name: 'Editar partido' })) + const input = screen.getByRole('textbox', { name: 'Partido' }) + fireEvent.change(input, { target: { value: 'PSD' } }) + fireEvent.blur(input) + + await waitFor(() => expect(screen.getByRole('status').textContent).toContain('Salvo.')) + expect(screen.queryByRole('textbox', { name: 'Partido' })).toBeNull() + expect(router.refresh).toHaveBeenCalled() + }) +}) diff --git a/tests/unit/codebaseConventions.unit.spec.ts b/tests/unit/codebaseConventions.unit.spec.ts index 461b8396..c14bef9f 100644 --- a/tests/unit/codebaseConventions.unit.spec.ts +++ b/tests/unit/codebaseConventions.unit.spec.ts @@ -574,7 +574,7 @@ describe('admin bypasses carry a justification comment', () => { ['src/app/(campaign)/campanha/actions/municipality.ts', 1], ['src/app/(campaign)/campanha/actions/password.ts', 2], ['src/app/(campaign)/campanha/actions/profile.ts', 4], - ['src/app/(campaign)/campanha/actions/stateDeputy.ts', 1], + ['src/app/(campaign)/campanha/actions/stateDeputy.ts', 0], ['src/app/(campaign)/campanha/actions/supporter.ts', 9], ['src/app/(campaign)/campanha/actions/supporterImport.ts', 2], ['src/app/(campaign)/campanha/actions/votePledge.ts', 1], diff --git a/tests/unit/municipalityStateDeputyRelationCell.unit.spec.tsx b/tests/unit/municipalityStateDeputyRelationCell.unit.spec.tsx index f7e30478..ed15b943 100644 --- a/tests/unit/municipalityStateDeputyRelationCell.unit.spec.tsx +++ b/tests/unit/municipalityStateDeputyRelationCell.unit.spec.tsx @@ -69,7 +69,7 @@ describe('MunicipalityStateDeputyRelationCell shared editor (B159)', () => { screen.getByRole('button', { name: 'Editar dobradinhas em Seabra — Beltrana (PSB)' }), ).toBeTruthy() expect(screen.getByRole('link', { name: 'Beltrana (PSB)' }).getAttribute('href')).toBe( - '/campanha/dobradinhas/beltrana', + '/campanha/dobradinhas/7', ) }) }) diff --git a/tests/unit/stateDeputyListUrl.unit.spec.ts b/tests/unit/stateDeputyListUrl.unit.spec.ts index eea785d3..5fa30610 100644 --- a/tests/unit/stateDeputyListUrl.unit.spec.ts +++ b/tests/unit/stateDeputyListUrl.unit.spec.ts @@ -80,7 +80,7 @@ describe('state deputy list URL contract', () => { }) it('resolves the payload sort string with the descending prefix', () => { - expect(resolveStateDeputyListPayloadSort('name', 'asc')).toBe('name') + expect(resolveStateDeputyListPayloadSort('name', 'asc')).toBe('contact.name') expect(resolveStateDeputyListPayloadSort('party', 'desc')).toBe('-party') })