From 7750c43201e51f4b8d72ebab545a7bc8f445b530 Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Tue, 15 Sep 2026 11:08:28 -0300 Subject: [PATCH] =?UTF-8?q?feat(campaign):=20C165=20=E2=80=94=20importa=20?= =?UTF-8?q?eventos=20do=20Google=20como=20atividades=20sem=20munic=C3=ADpi?= =?UTF-8?q?o=20(Closes=20#1010)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/changelog/2026-09-15-c165.md | 1 + .../c165-f1-update-access-municipality.md | 68 + .../c165-importar-eventos-google-impl.md | 238 + scripts/lib/e2e-affected-manifest.mjs | 10 + .../(campaign)/campanha/actions/activity.ts | 99 +- src/collections/Activity.ts | 47 +- .../campaign/activity/ActivityAgenda.css | 10 + .../campaign/activity/ActivityAgenda.tsx | 12 + .../campaign/activity/ActivityCard.tsx | 24 +- .../campaign/activity/ActivityOverlay.tsx | 23 +- src/lib/googleCalendarEventMapping.ts | 262 +- src/lib/googleCalendarReverseEdit.ts | 75 +- src/lib/schemas/activity.ts | 17 + ...15_142058_activity_google_import_link.json | 15423 ++++++++++++++++ ...0915_142058_activity_google_import_link.ts | 20 + src/migrations/index.ts | 8 +- src/payload-types.ts | 6 +- src/utilities/access/activities.ts | 44 +- src/utilities/activityFormData.ts | 5 +- src/utilities/activityOmnibox.ts | 38 +- src/utilities/activityOverlayErrors.ts | 10 + src/utilities/activityRelationOptions.ts | 4 +- src/utilities/activityUi.ts | 26 +- src/utilities/activityViewModels.ts | 8 + src/utilities/calendarFeed.ts | 11 +- src/utilities/googleCalendarClient.ts | 20 + src/utilities/googleCalendarSync.ts | 281 +- tests/e2e/campaignActivity.e2e.spec.ts | 46 +- tests/int/campaignActivity.int.spec.ts | 222 +- ...eCalendarPrimaryCalendarAction.int.spec.ts | 1 + tests/int/googleCalendarSync.int.spec.ts | 416 +- tests/unit/activityAgenda.unit.spec.ts | 1 + .../activityAgendaSwipePreview.unit.spec.tsx | 1 + tests/unit/activityOverlay.unit.spec.tsx | 26 +- tests/unit/activityUi.unit.spec.ts | 34 + tests/unit/googleCalendarClient.unit.spec.ts | 28 + .../googleCalendarEventMapping.unit.spec.ts | 246 + .../googleCalendarReverseEdit.unit.spec.ts | 6 +- .../unit/googleCalendarSyncHooks.unit.spec.ts | 1 + tests/unit/listOmniboxB128.unit.spec.ts | 42 + 40 files changed, 17656 insertions(+), 204 deletions(-) create mode 100644 docs/changelog/2026-09-15-c165.md create mode 100644 docs/plans/c165-f1-update-access-municipality.md create mode 100644 docs/plans/c165-importar-eventos-google-impl.md create mode 100644 src/migrations/20260915_142058_activity_google_import_link.json create mode 100644 src/migrations/20260915_142058_activity_google_import_link.ts diff --git a/docs/changelog/2026-09-15-c165.md b/docs/changelog/2026-09-15-c165.md new file mode 100644 index 00000000..9a604c8e --- /dev/null +++ b/docs/changelog/2026-09-15-c165.md @@ -0,0 +1 @@ +**C165 — Agenda: eventos do Google viram atividades sozinhas (sem município, a partir de 16/08/2026) (2026-09-15):** o espelho (C114/C115/C164) só enxergava eventos `teqo…`; evento criado à mão no calendário conectado era ignorado de propósito. Agora um evento estrangeiro com início na janela do espelho (90/365) e a partir do corte legal (`CALENDAR_PHASE_ANCHORS.consolidationStart` = 16/08/2026, data civil Bahia, sem backfill) vira atividade `confirmado` sozinho, com título verbatim (sem prefixo `[Município] `), `location → locality` e **descrição ignorada** (o texto vive no Google). O vínculo é system-write — `googleEventId` (unique) + `googleCalendarId` na `Activity` (migration `20260915_142058_activity_google_import_link` também derruba o `NOT NULL` de `municipality_id`) — e é ele que garante idempotência: re-sync converge e editar no Teqo usa um `patchEvent` parcial (PATCH, `sendUpdates: 'none'`, sem `description`) no MESMO evento remoto, nunca um segundo `teqo…`; cancelar no Teqo marca `status: cancelled` (nunca DELETE) e uma edição/cancelamento novo no Google volta pela regra de relógio do C115 (o relógio perde → o evento é re-afirmado); exclusão permanente cancela a atividade e atividade hard-deletada não ressuscita (o id fica no snapshot). Access: sem município, só `coordinator`/`candidate` (precedente `supporter`) — Visão/Edição "Tudo" do assessor passam a devolver `municipality exists`, criar/limpar/reapontar é barrado na action e demandas continuam exigindo município (dereferência null-safe). UI (Impeccable B): overlay com "Município (opcional)" e a opção "Sem município" (aviso de visibilidade), cards com badges "Do Google" (âmbar) e "Sem município" (tracejado) + rodapé "Importado automaticamente", marcador na agenda e filtro `unscoped=1` exclusivo com município na omnibox. Sem `Consent` novo, leader lockdown e evento estrangeiro intocados; testes unit (mapping/PATCH/overlay/omnibox), int (11 casos do motor + access/demandas) e e2e pinado; manifest e2e cobre `Activity`/schema/access/mapping. diff --git a/docs/plans/c165-f1-update-access-municipality.md b/docs/plans/c165-f1-update-access-municipality.md new file mode 100644 index 00000000..0d8a644e --- /dev/null +++ b/docs/plans/c165-f1-update-access-municipality.md @@ -0,0 +1,68 @@ +# C165-F1 — Activity: update access fecha município nulo/fora da carteira (bypass REST) + +Status: rascunho +Atualizado em: 2026-09-15 +Issue: (registrada junto do PR do C165) +Pai: #1010 (C165) — `depends: [1010]` +Priority: P2 +Kind: defect +Appetite: ~0,25 dia eng (sem UI, sem migration) +Responsável: — + +> Débito registrado na triage do `/simplify` do C165 +> ([c165-importar-eventos-google-impl.md](c165-importar-eventos-google-impl.md)). + +## Contexto + +O C165 tornou `municipality` opcional na `Activity` e decidiu (Decisão G do impl plan) que o +guard de **valor** do município vive na server action: advisor não cria sem município, não +limpa (`null`) e não reaponta para fora da carteira — cada caso com mensagem nomeada +(`ACTIVITY_UNSCOPED_ADVISOR_MESSAGE` / `ACTIVITY_OUT_OF_SCOPE_MESSAGE`). + +O access cobre a **linha** (`canUpdateActivity` devolve um `Where` que alcança a atividade), +não o **valor**: uma escrita que passe direto pelo Local API/REST com credencial de campanha +(ex.: `PATCH /api/activity/:id` com o JWT no header `Authorization`, caminho já declarado no +threat model de `src/utilities/access/shared.ts`) consegue gravar `municipality: null` ou um +id fora da carteira. Na prática o comportamento é simétrico ao lado `create` (que já valida +`data`), e o cookie `campaign-token` é path-scoped `/campanha` — o caminho exigiria JWT +copiado à mão; ainda assim é o eixo de access e fecha por construção. + +## Evidência + +- `src/utilities/access/activities.ts` — `canCreateActivity` é `data`-aware (C165); + `canUpdateActivity` devolve `Where`/`boolean` sem inspecionar `data.municipality`. +- `src/app/(campaign)/campanha/actions/activity.ts` — guard equivalente no `updateActivityRecord` + (mensagens nomeadas) cobre o caminho da UI. +- `src/utilities/access/shared.ts:100-104` — caminho REST/GraphQL com JWT de campanha no threat model. + +## Objetivo e aceite + +- `canUpdateActivity`, para advisor (staff não-unrestricted), inspeciona `data.municipality` + quando presente: `null` → `false`; número fora do write scope → `false`; ausente → o `Where` + de linha atual (nada muda para quem só edita outros campos). +- Coordenador/candidato/admin intocados; `editing: 'tudo'` continua alcançando qualquer + atividade **com** município, mas limpar para `null` segue coordenação/candidato-only. +- A action mantém o guard/UX (Decisão G do C165) — defesa em profundidade, sem mensagem nova. +- Pin int com `overrideAccess: false` e um advisor (`campaignActivity.int.spec.ts`): limpar + município e reapontar para fora da carteira são recusados pelo access; editar outro campo + da mesma atividade continua funcionando. + +## Fora de escopo + +- Field access por **valor** para `municipality` (Payload field access recebe o valor, não o + documento; a assimetria é do próprio modelo — o guard na collection access é o dono). +- Hardening equivalente nas demais collections (decisão sistêmica separada; cada domínio tem + seu dono e seu precedente). +- Mudança de UX/copy/mensagens da action. + +## Riscos + +- Advisor com `editing: 'tudo'` que hoje limpa município pela action: já era recusado + (C165) — o access só antecipa a mesma recusa em outros caminhos. +- Leitura do perfil fresco no access update (mesmo `getFreshCampaignUser` já usado) mantém + o custo por request dentro do padrão do módulo. + +## Aceite de engenharia + +- [ ] `pnpm gate:fast` verde; int pinado em `tests/int/campaignActivity.int.spec.ts`. +- [ ] Sem migration, sem Consent, sem UI; URL/slug intactos. diff --git a/docs/plans/c165-importar-eventos-google-impl.md b/docs/plans/c165-importar-eventos-google-impl.md new file mode 100644 index 00000000..9025cc91 --- /dev/null +++ b/docs/plans/c165-importar-eventos-google-impl.md @@ -0,0 +1,238 @@ +# Impl: Agenda: importar automaticamente eventos do Google como atividades (sem município, a partir de 16/08/2026) + +Status: aprovado +Atualizado em: 2026-09-15 +Issue: #1010 +Intenção: docs/plans/c165-importar-eventos-google.md +Appetite restante: herdado — ~3–4 dias eng + +## Leitura da intenção + +- **Outcome:** evento criado no Google Calendar (conta da imprensa já conectada) com início ≥ 16/08/2026 vira atividade no Teqo sozinho, com a imediatez Google→Teqo do C164 (mesmo webhook/passada, sem trigger novo). A atividade nasce `confirmado`, sem município, com título original do Google (sem prefixo `[Município] `), `location → locality`, e a coordenação tria no overlay existente quando quiser. Sem município, só `coordinator`/`candidate` veem; ao atribuir município, entram as regras normais (advisor por carteira/responsável). Vínculo estável e idempotente: re-sync não duplica; editar no Teqo atualiza o MESMO evento do Google — nunca nasce um segundo evento `teqo…`. Editar/cancelar no Google reflete (relógio C115); excluir no Google cancela a atividade. Nada antes de 16/08/2026 entra (sem backfill); evento estrangeiro nunca é apagado; leader lockdown intocado; sem `Consent` novo; Teqo inteiro se o Google cair. +- **O que NÃO negociar:** corte em `CALENDAR_PHASE_ANCHORS.consolidationStart` (`src/lib/visitPlannerAnchors.ts:22-29` — não duplicar o literal); horizonte = janela do espelho (90d/365d) com piso do corte; `singleEvents=true` (cada ocorrência vira atividade própria); descrição ignorada na v1; cancelar no Teqo cancela o MESMO evento; importado que ganha município mantém o título sem prefixo; Vínculo system-write (invisível aos user paths); sem Consent; leader lockdown e feed iCal públicos intocados; evento estrangeiro nunca `DELETE`ado pelo Teqo. +- **O que reavaliar (o reconhecimento confirmou as armadilhas):** + - Access: advisor `visibility === 'tudo'` retorna `true` em `canReadActivity` (`src/utilities/access/activities.ts:74-77`) e `editing === 'tudo'` em `canUpdateActivity` (`:99`) — fura "sem município só coordenação/candidato" sem um guard de `municipality != null`; `canCreateActivity` (`:31-39`) é booleano staff e não olha `data`. + - Ação: `src/app/(campaign)/campanha/actions/activity.ts:293-294` dereferencia `activity.municipality.id` incondicionalmente → TypeError ao salvar importado com demandas; `CampaignDemand.ts:242-279` exige município (`:253`) e compara com o da atividade (`:274-278`). + - Forward: o insert sempre nasce com id determinístico `teqo…` (`src/lib/googleCalendarEventMapping.ts:142`) e o update usa o id remoto decodificado (`googleCalendarSync.ts:675`) — sem branch por `googleEventId`, editar um importado criaria um 2º evento. + - Descrição: `googleEventContentEquals` compara `description` (`mapping:196-199`) e `buildGoogleEventPayload` sempre monta a descrição (`:104-107`) — o forward sobrescreveria a descrição do usuário mesmo "ignorando" a importação. + - Recorrência: instâncias têm id `master_2026…` (underscore) e `GoogleRemoteEvent` (`mapping:152-161`) não carrega `recurringEventId`; títulos idênticos colidem no `slug` único (`Activity.ts:71-98` + `ACTIVITY_DUPLICATE_TITLE_MESSAGE`) — a 2ª ocorrência pausaria a passada. + - Namespace: `teqo3` manual decodifica como atividade 3 (`mapping:54-67`) — o corte de importação reusa `decodeGoogleEventActivityId(...) !== null`, nunca prefixo. + - Título: summary vazio/emoji-only falha `setCanonicalActivitySlug` (`Activity.ts:82-84`) sem fallback. + - Snapshot: `lastSeenEventIds` só guarda ids `teqo…` (`googleCalendarSync.ts:442-458,602-603`); sem ids estrangeiros linkados não há como detectar "excluído no Google" (hard delete). + - e2e manifest: `src/collections/Activity.ts`, `src/lib/schemas/activity.ts` e `src/utilities/access/activities.ts` **não** estão nos prefixes de atividade (`scripts/lib/e2e-affected-manifest.mjs:100-115`) → diff cairia no smoke/`frontend+campaignNewsletter`; `src/lib/googleCalendarEventMapping.ts`/`googleCalendarReverseEdit.ts` também faltam no entry do motor (`:114-135`). + - Migration: `activity.municipality_id` é `NOT NULL`; required→nullable idempotente tem precedente em `20260824_010000_make_supporter_import_batch_actor_nullable.ts:23-45`. + +## Abordagem recomendada + +```mermaid +flowchart LR + G["Google Calendar
(conta da imprensa)"] -->|listEvents
singleEvents + showDeleted| L["runSyncPass
(motor C114/C115)"] + L -->|"id decodifica (teqo…)"| M["loops 1–2 existentes
espelho + reverse C115"] + L -->|"id estrangeiro + início ≥ corte"| I{"linkado?
googleEventId"} + I -->|não| C["create activity
confirmado, sem município
título verbatim, locality = location
slug determinístico"] + I -->|sim| R{"relógio C115"} + R -->|"Google mais novo"| RE["reverse patch Teqo
(título/horário/cancelamento + registro)"] + R -->|"Teqo mais novo/diferente"| P["patchEvent (PATCH)
MESMO id remoto; sem description"] + C --> S["snapshot lastSeenEventIds
(inclui id estrangeiro linkado)"] + RE --> S + P --> S + S --> CAS["recordLastSeenSnapshot
(CAS inalterado)"] + P -.->|"cancelado no Teqo"| CC["PATCH status=cancelled
(nunca DELETE)"] +``` + +**Opções consideradas (macro):** **A)** vínculo em 2 campos system-write na `Activity` (`googleEventId` + `googleCalendarId`) + passo de importação dentro do `runSyncPass` (mesma janela/snapshot/CAS) + `PATCH` para eventos linkados; **B)** coleção de vínculo dedicada (`googleCalendarLink`) com passada própria; **C)** "espelhar" o importado criando um evento `teqo…` e deixar o estrangeiro intocado (vínculo pelo título/horário). +**Recomendação: A.** +**Justificativa:** é o menor modelo que satisfaz o aceite inteiro (idempotência por vínculo único, edição no MESMO evento, cancelamento sem apagar estrangeiro) sem twin de coleção e sem duplicar a máquina de snapshot/CAS. B rejeitada: 1:1 sem ciclo de vida próprio, exigiria join/coleção nova e dois escritores de estado. C rejeitada: viola "editar no Teqo atualiza o MESMO evento" (o Teqo passaria a ter duas fontes: o evento estrangeiro e a cópia `teqo…`) e "nunca nasce um segundo evento". + +### Decisões de engenharia + +#### A. Modelo do vínculo (schema + migration) + +Opções: **A)** dois campos system-write na `Activity` — `googleEventId` (`text`, `unique`, `index` implícito, `admin.readOnly`, `access.create/update: canSetActivitySystemField`) + `googleCalendarId` (`text`, idem, sem unique); presença de `googleEventId` = importado (o espelho usa id determinístico e nunca persiste link); **B)** só `googleEventId`, assumindo calendário único; **C)** coleção `googleCalendarLink` (atividade↔evento) separada. +Recomendação: **A** — o par id+calendário é o vínculo estável: com a troca de calendário do C150, só o par responde "este link é deste calendário?" sem ambiguidade (skip seguro, sem 404/duplicata). O `unique` no `googleEventId` é a trava de idempotência no banco (Postgres aceita múltiplos NULL). Migration `20260915_HHMMSS_activity_google_import_link`: gera as duas colunas + unique index e `ALTER TABLE activity ALTER COLUMN municipality_id DROP NOT NULL`. +Alternativas rejeitadas: **B** porque a troca de calendário torna o id ambíguo (update em evento de outro calendário → 404 pausa a passada ou cria cópia); **C** porque é twin de um 1:1 (join por atividade, sem consulta independente) e dobra os pontos de escrita. + +#### B. Semântica de importação (corte, horizonte, forma e idempotência) + +Opções: **A)** predicado puro `isImportableGoogleEvent(event, { rangeStart, timeMax })` no módulo de mapping + import por `googleEventId` com fallback de título/slug determinístico; **B)** filtro inline no motor com literais de corte; **C)** importar tudo que o `listEvents` devolver (sem corte) e triar depois. +Recomendação: **A** — o corte é `formatBahiaCivilDate(startAt) >= CALENDAR_PHASE_ANCHORS.consolidationStart` (all-day: `date` já é civil; timed: `Date.parse(dateTime)` → data civil Bahia), o piso é `max(rangeStart, cutInstant)` e o teto é o `timeMax` do `buildListWindow` (mesma janela do espelho). Candidato = `decodeGoogleEventActivityId(event.id) === null` (corte existente `googleCalendarSync.ts:605-610`; um `teqo3` manual decodifica e fica no namespace do espelho — nunca importado, documentado). Forma do importado: `status: 'confirmado'`, `startAt/endAt/allDay` de `googleScheduleToActivityFields` (null → skip), `locality = event.location?.trim().slice(0,160)`, `description` ausente, `tags: []`, `googleEventId`/`googleCalendarId`, create com `overrideAccess: true` + `context: { mutationKind: 'googleCalendarSync' }` + `req.user` removido (mesmo padrão do reverse patch, `googleCalendarSync.ts:471-499`). Título = `googleTitleFromSummary(summary, undefined)`, exigindo `length ≥ 2` e `slugify(title) !== ''` (senão skip — não inventar título num campo imutável). Slug determinístico `${slugify(title)}-${sufixo civil}` (timed: `formatIsoAsBahiaDateTimeInput(startAt).replace(/[-:]/g,'')`; all-day: `allDayCivilDateOf`), com branch no hook `setCanonicalActivitySlug`: em `create` com `mutationKind === 'googleCalendarSync'` e slug fornecido, sanitiza e preserva (em vez de recomputar). Idempotência: mapa `googleEventId → activity` carregado numa query do passo; `unique` no banco como backstop; violação de unicidade do create só é engolida quando um link para o mesmo evento já existe (senão re-throw — ver decisão C). +Alternativas rejeitadas: **B** porque duplicaria o literal do corte (proibido pela intenção) e espalharia a política no motor; **C** porque backfill de eventos pessoais antigos é rabbit hole nomeado e viola o aceite "nada antes de 16/08". +Nota de produto respeitada: o importado **não** leva `[Município] ` no título nem depois de ganhar município (branch de importado no forward; ver D/E). + +#### C. Encaixe no motor, snapshot e concorrência + +Opções: **A)** função `runImportedEventsPass(...)` chamada dentro do `runSyncPass` depois dos loops 1–2 e **antes** do `recordLastSeenSnapshot`, compartilhando `remoteEvents`, `lastSeenIds`, CAS e `activityWhere`, devolvendo contadores + ids vistos para o snapshot; **B)** branches de importação dentro dos dois loops existentes; **C)** passada de importação de topo, com `listEvents` próprio, chamada pelo `runCampaignCalendarSync`. +Recomendação: **A** — os loops 1–2 são a máquina C114/C115 testada e assumem id decodificado; separar o passo isola a semântica nova (criar/cancelar linkado) e reusa o que já existe: a lista estrangeira vem do mesmo `listEvents` (sem RTT extra), o `lastSeenIds` alimenta a regra de exclusão e o snapshot ganha os ids estrangeiros **linkados** (não todos os estrangeiros — mantém o JSONB enxuto e o significado do conjunto). Contadores: estender `SyncCounts` só com `imported` (criações); patches Teqo→Google contam em `updated`, cancels do Google em `reverseEdits` (mesma classe do nativo), cancelamento no Teqo conta em `deleted` (PATCH `status: 'cancelled'`, nunca `DELETE`) — `toMatchObject` dos testes atuais não quebra. Query do passo: `payload.find` em `activity` com `and: [{ googleEventId: { exists: true } }, { googleCalendarId: { equals: calendarId } }, ...(activityWhere)]`, sem filtro de status (precisa das `cancelado` para re-assertar o cancel no Google). Concorrência de duas passadas no mesmo evento novo: o `unique` barra o segundo create; o catch só é tolerado se um link para aquele `googleEventId` já existir (re-leitura), senão re-throw (o erro real sobe e a passada vira `paused`, comportamento atual). +Alternativas rejeitadas: **B** porque mistura o branch frágil no loop que hoje garante o espelho (e o tratamento de cancelado difere entre nativo e importado); **C** porque paga um segundo `listEvents` por passada e cria um segundo escritor de snapshot/CAS. + +#### D. Editar no Teqo → MESMO evento (`patchEvent`, cancel) + +Opções: **A)** novo `patchEvent(calendarId, eventId, partial)` no client (HTTP `PATCH`, `events.patch`) usado por importados, com o body só dos campos que o Teqo controla (`summary`, `start`, `end`, `location`) e `sendUpdates: 'none'`; **B)** continuar com `updateEvent` (PUT) e copiar `description` do evento listado; **C)** PUT e aceitar a perda. +Recomendação: **A** — `PUT` (update) substitui o recurso inteiro: além da `description`, apagaria convidados/lembretes/Meet (fora de escopo, mas não podem ser destruídos). O `PATCH` preserva tudo que não enviamos. No branch de importado do `runSyncPass`, o payload usa o id remoto (`googleEventId`) e **nunca** `googleEventIdForActivity`; a igualdade de conteúdo do importado compara `summary` + `start/end` + `location` (descrição fora). Cancelar no Teqo = `patchEvent(..., { status: 'cancelled' })` no MESMO evento (a recomendação A da questão em aberto), nunca `DELETE` — o evento continua existindo (lixeira do Google) e o mandato "evento estrangeiro nunca é apagado" fica literal. Hard delete da atividade (admin) não apaga o evento estrangeiro. +Alternativas rejeitadas: **B** porque ainda é `PUT` (destrói o resto do recurso; copiar a descrição só maquia o problema); **C** porque destrói dado do usuário e é anti-goal explícito ("não alterar evento estrangeiro" além do combinado). + +#### E. Descrição no forward de importados + +Opções: **A)** o payload parcial do importado omite `description` (o `PATCH` preserva a do Google) e a igualdade de conteúdo ignora descrição; **B)** espelhar a descrição no campo `description` da atividade e reenviá-la; **C)** apagar a descrição do evento na primeira edição. +Recomendação: **A** — é a leitura literal de "descrição ignorada na v1 (texto vive no Google)": o Teqo não lê, não guarda e não escreve descrição de importado. Teste int pina que editar o título no Teqo mantém a descrição que existia no stub. +Alternativas rejeitadas: **B** porque a intenção v1 manda ignorar (e reenviar uma descrição Teqo-soberana contradiz "texto vive no Google"); **C** porque perde dado do usuário. + +#### F. Recorrência + +Opções: **A)** cada instância expandida vira atividade própria, linkada ao id da instância (`master_2026…T…`), sem campo novo de série; **B)** importar só a primeira ocorrência da série; **C)** guardar `recurringEventId`/`originalStartTime` e gerar/atualizar atividades por série. +Recomendação: **A** — é o literal da intenção (`singleEvents=true` já expande). O id de instância é estável para a ocorrência (contém o início original; mover a ocorrência no Google mantém o id), então remarcar uma ocorrência no Teqo faz `PATCH` naquela instância (vira exceção no Google) e remarcar a série atualiza as instâncias (reverse C115 por instância). O título repetido NÃO colide o slug graças ao sufixo determinístico (decisão B). Mudar a regra da série pode regenerar ids: as atividades órfãs "somem" do Google → canceladas pela regra de exclusão, e as novas entram (aceito e documentado na v1). Nenhum campo de série é criado (sem consumidor). +Alternativas rejeitadas: **B** porque perde compromissos reais e contraria o aceite; **C** porque modela série sem produto que a use (YAGNI) e multiplica estados. + +#### G. Access sem município (a armadilha do `visibility === 'tudo'`) + +Opções: **A)** guard de existência nas scopes de advisor + `canCreateActivity` ciente de `data`; **B)** reusar `resolveProfileScopedRead` e deixar o advisor "tudo" ver tudo (inclui sem município); **C)** trocar o `true` do perfil "tudo" por um where que exclui apenas `responsável` sem município. +Recomendação: **A** — `canReadActivity`: admin/unrestricted antes; `visibility === 'tudo'` passa a devolver `{ municipality: { exists: true } }`; a carteira devolve `{ and: [{ municipality: { exists: true } }, { or: [ responsible…, advisorMunicipalityScopeWhere ] }] }` (o `and` fecha o vazamento do branch `responsible`, que casaria atividade sem município). `canUpdateActivity`: `editing === 'tudo'` idem, carteira idem. `canCreateActivity`: sem `relationshipId(data?.municipality)` só `isCampaignUnrestricted`. Na action, advisor criando sem município, limpando município (`null`) ou repontando para fora da carteira → mensagem nomeada nova em `src/lib/schemas/activity.ts` (precedente `SUPPORTER_UNSCOPED_COORDINATOR_MESSAGE`), mapeada em `mapActivityOverlayError` (safeMessages) para não colapsar em genérico. Com município, nada muda (advisor por carteira/responsável). Leader continua `false`. Campos novos seguem `canSetActivitySystemField` (só admin no admin panel; o motor escreve com bypass). +Alternativas rejeitadas: **B** porque "perfil de visão" é eixo de leitura do catálogo com município, não licença para atividade órfã — furaria o aceite; **C** porque não fecha o branch `responsible` do `advisorActivityScopeWhere` (advisor responsável veria a atividade sem município). + +#### H. Demandas/rascunho sem município + +Opções: **A)** demandas continuam exigindo município: dereferência null-safe + rejeição nomeada quando há rascunhos sem município efetivo; **B)** permitir demanda sem município mudando `CampaignDemand.ts:242-279`; **C)** ignorar silenciosamente os rascunhos sem município. +Recomendação: **A** — `CampaignDemand` é dona da regra "demanda e atividade no mesmo município" (`:253,274-278`) e demandas são entidade municipal por design; o escopo deste item é atividade. Em `createActivityRecord` e `updateActivityRecord`: usar `relationshipId(activity.municipality)` (remove o TypeError de `:293-294`) e, se `demands.length > 0` com município efetivo nulo, lançar mensagem nomeada ("Atribua um município para criar demandas desta atividade."). Sem demandas, salvar sem município funciona. Giro (`createTourDraftActivitiesRecord`) intocado (sempre tem município). +Alternativas rejeitadas: **B** porque reabre o modelo de demandas (blast radius grande, fora do appetite); **C** porque perde dado em silêncio. + +#### I. Overlay (município opcional) + +Opções: **A)** `StrictCombobox` com opção sintética `{ value: '', label: 'Sem município' }` no topo, label "Município (opcional)", aviso âmbar quando vazio, hidden input enviando `''`, parser `nullableRelationshipFormValue`; **B)** input vazio + placeholder sem opção explícita; **C)** checkbox "sem município" separado. +Recomendação: **A** — é o rascunho UI aprovado (Impeccable B): default "Sem município", opção/chip exclusivo, aviso "Sem município, só coordenação e candidato veem. Ao atribuir, o assessor da carteira passa a ver." O `StrictCombobox` já suporta `value ''` (selected nulo, `showClear` some); a opção sintética dá o texto default e a volta depois de escolher um município. Remover a validação `if (!municipalityId)` (`ActivityOverlay.tsx:205`); o hidden `municipality` (`:459`) passa a valer `''`; o edit prefila o município existente (`:175-181`) e "Sem município" quando nulo. Sem mudança nos hosts (create/edit já passam `municipalityOptions`). +Alternativas rejeitadas: **B** porque "Sem município" seria só placeholder (não dá para reverter a escolha) e o draft pede opção explícita; **C** porque cria um estado a mais só para o mesmo campo. + +#### J. Lista e agenda (view models + badges) + +Opções: **A)** `importedFromGoogle` derivado de `Boolean(activity.googleEventId)` nos view models existentes, badges no `ActivityCard` (âmbar "Do Google", tracejado "Sem município", rodapé "Importado automaticamente") e marcador âmbar no evento da agenda (`ActivityAgenda`, `extendedProps`); **B)** campo booleano próprio no schema; **C)** componente de card novo só para importados. +Recomendação: **A** — derivar evita estado redundante (o link é a fonte); adicionar `googleEventId: true` a `activityAgendaSelect` cobre lista (`ActivityList`) e agenda (`ActivityAgendaEvent`) com um select só; `ActivityCard`/`Badge`/CSS da agenda são os shells existentes (Impeccable B: encaixe, não twin). Agenda renderiza o marcador "Do Google" e "Sem município" na linha de local (hoje a linha já filtra `municipalityName` nulo, `ActivityAgenda.tsx:136` — null-safe). Dossiê (`municipalityDossierData.ts:73-90`) continua filtrando por município: importado não aparece lá (aceito). +Alternativas rejeitadas: **B** porque duplica o que a presença do link já diz (estado redundante que pode divergir); **C** porque twin do `ActivityCard`. + +#### K. Omnibox/filtro "Sem município" + +Opções: **A)** estado booleano `unscoped?: boolean` + param `unscoped=1`, chip e sugestão "Sem município" (grupo Município), exclusivo com `municipality`; **B)** sobrecarregar `municipality` com o literal `sem`; **C)** só filtro visual sem URL. +Recomendação: **A** — `buildActivityListWhere` adiciona `{ municipality: { exists: false } }`; `parseActivityListParams`/`buildActivityListSearchParams` fazem round-trip de `unscoped`; aplicar `unscoped` limpa `municipality` e vice-versa; remover o chip limpa só o campo. Contagens por sugestão do rascunho são ilustrativas (o omnibox não tem plumbing de contagem hoje; a lista mantém o "N resultados" existente). `ActivityFilters` não muda de estrutura (só seeds/chips). +Alternativas rejeitadas: **B** porque quebra o parse numérico do param e mistura dois significados; **C** porque filtro sem URL quebra o padrão de navegação compartilhado (`useCampaignListFilterNavigation`). + +#### L. e2e manifest + +Opções: **A)** adicionar os prefixes faltantes: no entry de atividade (`:100-115`) `src/collections/Activity.ts`, `src/lib/schemas/activity`, `src/utilities/access/activities.ts`; no entry do motor (`:114-135`) `src/lib/googleCalendarEventMapping.ts`, `src/lib/googleCalendarReverseEdit.ts`; **B)** confiar na run `frontend`+`campaignNewsletter` que `src/lib/schemas` já acorda; **C)** ampliar para `src/utilities/access` inteiro no entry de atividade. +Recomendação: **A** — `src/lib/schemas` e `src/utilities/access` são risk prefixes e têm mappings genéricos próprios (`:238-251`); sem o par específico, um diff de activity acorda specs erradas (e o classifier pode falhar fechado `unmapped-risk`). O match é por `startsWith` e une os specs dos entries que casam, então o par específico não remove a cobertura de risco existente. E2E de importação real com stub de Google não existe na suíte (fora). +Alternativas rejeitadas: **B** porque a cobertura pinada é do formulário público/newsletter, não da agenda; **C** porque `src/utilities/access` completo acordaria as specs de activity em qualquer diff de RBAC de outro domínio (ruído). + +#### M. Testes + +Opções: **A)** unit para mapping/client/overlay/omnibox + int no motor (novo describe de importação) e nas regras de access/demandas + e2e de formulário/badge; **B)** só int no motor; **C)** e2e novo com stub de Google. +Recomendação: **A** — unit barato pina a matriz de corte/forma/slug/PATCH; int pina o comportamento do motor (idempotência, mesmo id, descrição preservada, snapshot, cancel) e as regras de access; e2e cobre o overlay sem município e o badge. **C** rejeitado: a suíte e2e não tem stub do Calendar (C164 já documentou a discricionariedade); int com stub é a camada do motor. +Detalhe por arquivo: `tests/unit/googleCalendarEventMapping.unit.spec.ts` (predicado de corte nas bordas 15/16-08 all-day e timed, forma do importado, igualdade sem descrição, slug determinístico, `teqo3` fora do import); `tests/unit/googleCalendarClient.unit.spec.ts` (`patchEvent` → `PATCH` no path certo); `tests/unit/googleCalendarReverseEdit.unit.spec.ts` (título `[X] ` verbatim com `municipalityName` undefined); `tests/unit/activityOverlay.unit.spec.tsx` (default "Sem município", salvar sem município envia `municipality=''`, aviso de visibilidade); `tests/unit/activityUi.unit.spec.ts`/`activityAgendaOmnibox.unit.spec.ts` (round-trip `unscoped`, chip, exclusividade); `tests/int/googleCalendarSync.int.spec.ts` (stub ganha `patchEvent` com merge: **1** importa timed ≥ corte sem município, título verbatim, sem evento `teqo…`; **2** 2ª passada converge; **3** edição no Google reflete com registro; **4** edição no Teqo PATCHa o mesmo id e preserva descrição; **5** atribuir município mantém título sem prefixo; **6** `cancelled` no Google cancela; **7** hard delete pós-visto cancela; **8** pré-corte fica fora e o dia 16/08 entra; **9** cancelar no Teqo vira `cancelled` no MESMO evento (sem DELETE); **10** summary vazio não importa e não pausa; **11** duas ocorrências de mesmo título geram slugs distintos); `tests/int/campaignActivity.int.spec.ts` (schema sem município; advisor não cria/limpa; advisor `visibility=tudo` não lê sem município via `payload.find`/where; coordenador lê; demandas sem município rejeitadas; dereferência null-safe); `tests/e2e/campaignActivity.e2e.spec.ts` (labels `Município (opcional)` nas 4 linhas pinadas e um create sem município com badge "Sem município"). +Alternativas rejeitadas: **B** porque a matriz de corte/forma é pura e determinística (não deve depender de banco); **C** idem acima. + +### Componentes / mudanças + +- **`src/collections/Activity.ts`:** `municipality` `required: false`; campos `googleEventId` (unique, readOnly, access system) e `googleCalendarId` (readOnly, access system) perto de `lastMirroredChangeAt`; branch no `setCanonicalActivitySlug` para preservar slug fornecido em create com `mutationKind === 'googleCalendarSync'`. +- **`src/lib/schemas/activity.ts`:** `municipality` opcional no create e `nullable().optional()` no update; `tourStopDraftSchema` mantém município obrigatório (extend explícito — o pick herdaria a opcionalidade); mensagem nomeada do sem-município e a de demandas. +- **`src/utilities/activityFormData.ts`:** `municipality: nullableRelationshipFormValue(formData, 'municipality')`. +- **`src/utilities/access/activities.ts`:** guards de município existente (read/update) e `canCreateActivity` por `data` (decisão G). +- **`src/app/(campaign)/campanha/actions/activity.ts`:** guard de advisor sem município/limpar/repontar; dereferência null-safe e rejeição de demandas (decisão H); exportar `updateActivityRecord` se o teste int precisar (precedente `createActivityRecord`). +- **`src/utilities/activityOverlayErrors.ts`:** safeMessages com a mensagem nova. +- **`src/lib/googleCalendarEventMapping.ts`:** `isImportableGoogleEvent`, `buildImportedGoogleEventPayload`, `googleImportedEventContentEquals`, `importedActivitySlug` (puros, reusando `googleScheduleToActivityFields`/`googleTitleFromSummary`/`allDayStartInstant`/`slugify`/`CALENDAR_PHASE_ANCHORS`). +- **`src/utilities/googleCalendarClient.ts`:** `patchEvent` (`PATCH`, `sendUpdates: 'none'`). +- **`src/utilities/googleCalendarSync.ts`:** `runImportedEventsPass` + integração no `runSyncPass` (merge de snapshot e contadores), branch de importado no forward (`patchEvent` pelo id remoto), cancel no Teqo via `status: 'cancelled'`, `SyncCounts.imported`. +- **`src/utilities/activityViewModels.ts`:** `googleEventId: true` no `activityAgendaSelect`; `importedFromGoogle` em `ActivityAgendaEvent`/`ActivityListViewModel`. +- **UI:** `ActivityOverlay.tsx` (decisão I), `ActivityCard.tsx` (badges/rodapé), `ActivityAgenda.tsx` + `ActivityAgenda.css` (marcador), `ActivityFilters.tsx` (seeds/chips) — shells existentes do rascunho aprovado. +- **Filtros:** `src/utilities/activityUi.ts` (`unscoped`), `src/utilities/activityOmnibox.ts` (chip/sugestão/exclusividade). +- **`scripts/lib/e2e-affected-manifest.mjs`:** prefixes da decisão L. +- **Migration:** `src/migrations/20260915_HHMMSS_activity_google_import_link.ts` (+ `index.ts`, `payload-types.ts` via `pnpm migrate:create`/`pnpm migrate`/`pnpm generate:types`). +- **Access/Consent:** nada de `Consent` novo (dado interno de staff); vínculo system-write; leader lockdown intocado. +- **Dados → forma:** N/A — a intenção resolve "não apresentar dados"; a apresentação é a UI de badges/overlay acima. + +## Fases verificáveis + +1. **Tracer schema + access (quota ~25% / ~1 dia).** Migration aplica; coordenador cria atividade sem município pelo Local API; advisor (inclusive `visibility=tudo`) não lê; demandas sem município rejeitadas com mensagem. + - `pnpm migrate:create activity_google_import_link` → auditar `up`/`down` → `pnpm migrate` → `pnpm generate:types`. + - Unit/int: `pnpm test:unit -- tests/unit/activityOverlayErrors.unit.spec.ts`; `pnpm test:int -- tests/int/campaignActivity.int.spec.ts`. + - Checkpoint: os testes pinados `campaignActivity.int.spec.ts:99-106` mudam de "requires a municipality" para "coordinator cria sem / advisor não cria". +2. **Motor import/link (quota ~40% / ~1,5 dia).** Evento estrangeiro elegível vira atividade linkada; 2ª passada converge; edição nos dois sentidos usa o mesmo evento; descrição preservada; cancel/exclusão; corte/horizonte. + - Unit: `pnpm test:unit -- tests/unit/googleCalendarEventMapping.unit.spec.ts tests/unit/googleCalendarClient.unit.spec.ts tests/unit/googleCalendarReverseEdit.unit.spec.ts`. + - Int: `pnpm test:int -- tests/int/googleCalendarSync.int.spec.ts`. +3. **UI (quota ~20% / ~0,75 dia).** Overlay com "Município (opcional)" + aviso; cards com "Do Google"/"Sem município"/rodapé; agenda com marcador; omnibox com chip/sugestão "Sem município". + - Unit: `pnpm test:unit -- tests/unit/activityOverlay.unit.spec.tsx tests/unit/activityUi.unit.spec.ts tests/unit/activityAgendaOmnibox.unit.spec.ts`. + - E2E: `pnpm test:e2e -- tests/e2e/campaignActivity.e2e.spec.ts` (labels novos + create sem município com badge; import real não é observável e2e — registrar discricionariedade). +4. **Gates + fechamento (quota ~15% / ~0,5 dia).** Manifest, suíte, changelog, PR. + - `pnpm gate:fast`; `pnpm test:int -- tests/int/googleCalendarSync.int.spec.ts tests/int/campaignActivity.int.spec.ts`; `pnpm test:e2e -- tests/e2e/campaignActivity.e2e.spec.ts`. + - Changelog `docs/changelog/2026-09-15-c165.md` (parágrafo único, abrindo `**C165 — … (2026-09-15):**`). + - `pnpm push` → PR com `Closes #1010`. + +## Rabbit holes / Não escopo (engenharia) + +- **Modelar série/recorrência** (`recurringEventId`, exceções, RRULE): fora — instância é atividade (decisão F). +- **Sincronizar convidados/RSVP/lembretes/Meet/cor**: fora; `PATCH` existe justamente para não destruí-los (decisão D). +- **Backfill pré-corte**: fora para sempre (predicado com piso; sem job). +- **Tela de triagem/notificação de importado**: a triagem é inline (lista + filtro + overlay). +- **Coleção de vínculo/estado de sync por evento**: twin rejeitado (decisão A/C). +- **Reconciliar calendários secundários**: fora; só o principal conectado. +- **Tocar leader lockdown, feed iCal público, consentimento**: fora. +- **Renomear/limpar links em troca de calendário**: v1 só ignora links de outro `googleCalendarId` (sem 404, sem duplicata); relink é trabalho futuro, se pedido. +- **Advisory lock global do sync**: fora — o CAS + unique já fecham a corrida sem segurar lock em I/O (C114-LOCK). + +## Riscos e mitigação + +- **Descrição/dados do usuário destruídos no forward:** `PATCH` parcial para importados + igualdade sem descrição (decisões D/E); int afirma que a descrição sobrevive a uma edição de título/local no Teqo. +- **Segundo evento `teqo…` criado:** branch por `googleEventId` nunca chama `insertEvent` com id determinístico; evento de link de outro calendário é ignorado; int afirma ausência de id `teqo…` no stub. +- **Colisão de slug (recorrência/título repetido):** sufixo civil determinístico + branch do hook; int com duas ocorrências de mesmo título. +- **Corrida de duas passadas importando o mesmo evento:** `unique` + catch que só tolera colisão quando o link já existe; senão re-throw (passada `paused`, sem duplicata — a trava é o banco). +- **Vazamento de visão para advisor (`visibility=tudo`/responsável):** guard `municipality exists` nos dois branches; int com `payload.find` sob o advisor; curated e2e de RBAC roda pelo risk prefix `src/utilities/access` (e o par específico acorda a suíte de atividade). +- **TypeError em demandas / demanda sem município:** dereferência null-safe + rejeição nomeada; `CampaignDemand` intocado; int cobre. +- **Título vazio/emoji-only quebrando a passada:** predicado de título/slug skip (não cria, não lança, passada segue `synced`); int cobre. +- **Borda do corte em fuso:** comparação por data civil Bahia (all-day direto; timed via `Date`); unit pina 15/08 (fora) e 16/08 (dentro). +- **Passada mais longa no hook (5s):** imports só adicionam uma query + creates de eventos novos; se o orçamento estourar, a passada vira `paused` e a próxima (webhook/manual, sem budget) converge — comportamento existente, registrar no PR. +- **Google fora do ar:** catch existente → `paused`; nenhum write path do Teqo depende do Google (guardrail preservado). +- **Snapshot maior:** só ids estrangeiros linkados entram (não todos), limitado pela janela/corte. +- **Migration em produção:** drop NOT NULL + unique index em coluna nova (todas as linhas NULL) — sem conflito; `down` reverte colunas e (best-effort) o NOT NULL. + +## Aceite de engenharia + +- [ ] Aceite de produto da intenção coberto: importação automática ≥ 16/08/2026, sem município visível só coordenação/candidato, triagem no overlay, vínculo idempotente editando o MESMO evento, Google→Teqo e excluir→cancelar, nada pré-corte, estrangeiro nunca apagado, leader lockdown intocado, sem Consent, Teqo inteiro se o Google cair. +- [ ] Invariantes AGENTS: migration commitada (`push: false`), Consent fail-closed preservado (nenhum novo), edição no dono (mapping/motor/access/overlay existentes, sem twin), sem literal duplicado do corte, URLs públicas intactas, PR com `Closes #1010`. +- [ ] Testes: unit (mapping/client/overlay/omnibox/reverse-edit), int (motor de importação + access/demandas), e2e pinado de formulário/badge; manifest e2e atualizado; `pnpm gate:fast` verde. +- [ ] Changelog `docs/changelog/2026-09-15-c165.md` commitado. + +## Débitos da revisão (triage 2026-09-15) + +**Registrado:** C165-F1 ([#1030](https://github.com/fsolla/teqo/issues/1030), kind defect, P2, +`depends #1010`, appetite ~0,25 dia) — o guard de valor do município vive só na action; +`PATCH /api/activity` com JWT de campanha escapa. Plano: +[c165-f1-update-access-municipality.md](c165-f1-update-access-municipality.md). + +**Já resolvido no simplify (não reabrir):** snapshot não re-importa id dismissado (+ teste de +2ª passada); create do import fora da transação do caller + pre-check; seen-and-gone para +atividade sem município; `down` da migration com `IF EXISTS`; params objeto em +`runImportedEventsPass`; `patchEvent(importedPatch)`; casts redundantes removidos; helpers de +teste hoistados; assert de slug por regex; `isImportableGoogleEvent` rejeita ids `teqo…`; +labels `ACTIVITY_IMPORTED_LABEL`/`ACTIVITY_UNSCOPED_LABEL`; `importedActivitySlug` +desexportado (knip); card sem span vazio. + +### Adiado com gatilho + +- **S2 — sleeps reais de 3,5s nos int do relógio** (margem ~0,5s contra a tolerância de 2s; o + irmão nativo é pré-C165). **Gatilho:** primeiro flake do relógio que consumir o `retry: 1` + em CI, ou qualquer mudança na tolerância/janela — trocar o sleep por timestamps/baseline + controlados, nos dois testes juntos. +- **S4 — "sumiu do Google" também cancela quem só saiu da janela bufferizada** (regra + deliberada do espelho nativo C115, agora herdada; remarcar para além de + `rangeEnd + SYNC_LIST_WINDOW_BUFFER_DAYS` vira cancelamento). **Gatilho:** primeiro relato + de cancelamento falso por remarcação fora da janela, ou mudança das janelas `FEED_*` — + revisar nativo + importado juntos. + +### Explicitamente fora + +- **S3 —** `googleScheduleToActivityFields` tipa `startAt`/`endAt` como `string | null` mas + nunca devolve null, e `importedActivitySlug` mascara com `?? ''`: pureza de tipo, sem call + site prejudicado; refazer só se um consumidor novo precisar do tipo estreito. +- **S5 —** `runImportedEventsPass` com ~150 linhas: extrair o ramo linkado em helper quando + uma feature tocar esse ramo; legibilidade não paga refatoração sem comportamento. +- **S6 —** `slugify(title) || 'evento'` inalcançável (o builder rejeita título sem slug em 2 + pontos): fallback defensivo morto; remover no próximo toque no mapping. + +### Self-score (decision-quality) + +**5/5** — (1) todas as decisões não triviais (A–M) trazem Opções A|B|C, Recomendação e rejeitadas com o porquê; (2) o diff cabe no appetite (~3–4 dias; fases somam 100% com quotas 25/40/20/15 e nenhuma frente inflada); (3) rabbit holes nomeados com corte explícito (série, convidados, backfill, tela de triagem, coleção twin); (4) depth check feito — helpers puros no dono (`googleCalendarEventMapping`), orquestração no motor que já é dona do snapshot/CAS, access no módulo de access, UI nos shells existentes; nenhum módulo/helper novo além da migration; (5) o outcome da intenção permanece intacto — as reavaliações confirmaram as armadilhas e as decisões as fecham, sem reescrever produto (descrição segue ignorada, líder segue trancado, evento estrangeiro não é apagado). diff --git a/scripts/lib/e2e-affected-manifest.mjs b/scripts/lib/e2e-affected-manifest.mjs index daf08278..843d0993 100644 --- a/scripts/lib/e2e-affected-manifest.mjs +++ b/scripts/lib/e2e-affected-manifest.mjs @@ -109,6 +109,13 @@ export const E2E_AFFECTED_MANIFEST = [ 'src/utilities/activity', 'src/utilities/activityOmnibox', 'src/lib/activityQuickActions', + // C165 — the activity schema/access/collection moved the município to + // optional and added the Google link; those diffs must wake the + // activity e2e family (the generic `src/lib/schemas` entry alone only + // wakes the public-site/newsletter specs). + 'src/collections/Activity.ts', + 'src/lib/schemas/activity', + 'src/utilities/access/activities.ts', ], // C148 — the activity components host the agenda's three modals: the // create/edit overlay (campaignActivity), the Google sync dialog and the @@ -122,6 +129,7 @@ export const E2E_AFFECTED_MANIFEST = [ // would only wake the home smoke, leaving the mirror spec unrun. // C150 — the one-click link builder and the campaign actions (picker + // choose) join the same domain entry. + // C165 — the pure mapping/reverse-edit modules joined the engine surface. prefixes: [ 'src/utilities/googleCalendarSync.ts', 'src/utilities/googleCalendarSyncHooks.ts', @@ -129,6 +137,8 @@ export const E2E_AFFECTED_MANIFEST = [ 'src/utilities/googleCalendarOAuth.ts', 'src/lib/googleCalendarOAuth.ts', 'src/lib/googleCalendarLink.ts', + 'src/lib/googleCalendarEventMapping.ts', + 'src/lib/googleCalendarReverseEdit.ts', 'src/components/campaign/activity/AgendaGoogleSyncChrome', 'src/components/campaign/activity/GoogleCalendarSyncDialog', 'src/components/campaign/activity/GoogleCalendarPickerDialog', diff --git a/src/app/(campaign)/campanha/actions/activity.ts b/src/app/(campaign)/campanha/actions/activity.ts index 06aeb9f9..2678e0f8 100644 --- a/src/app/(campaign)/campanha/actions/activity.ts +++ b/src/app/(campaign)/campanha/actions/activity.ts @@ -2,7 +2,9 @@ import type { Payload } from 'payload' +import { relationshipId } from '@/lib/relationship' import { + ACTIVITY_DEMANDS_MUNICIPALITY_MESSAGE, ACTIVITY_DEPUTY_RESCHEDULE_FORBIDDEN_MESSAGE, ACTIVITY_OUT_OF_SCOPE_MESSAGE, ACTIVITY_RESCHEDULE_FAILED_MESSAGE, @@ -10,6 +12,7 @@ import { ACTIVITY_RESULT_STAFF_MESSAGE, ACTIVITY_RESULT_TOO_LONG_MESSAGE, ACTIVITY_TASK_NOT_FOUND_MESSAGE, + ACTIVITY_UNSCOPED_ADVISOR_MESSAGE, ACTIVITY_UPDATE_BODY_REQUIRED_MESSAGE, ACTIVITY_UPDATE_BODY_TOO_LONG_MESSAGE, activityAgendaRequestSchema, @@ -36,6 +39,7 @@ import { canCampaignUserRescheduleActivity, getWritableMunicipalityIds, isCampaignStaff, + isCampaignUnrestricted, } from '@/utilities/campaignAccess' import { getCampaignActionContext, reloadCampaignActor } from '@/utilities/campaignActionContext' import { hookFilledCreateData } from '@/utilities/hookFilledData' @@ -125,10 +129,21 @@ export const createActivityRecord = async ( // create must check the municipality against the WRITE scope itself — // with Visão "Tudo" + Edição "Carteira" the read widens while the // carteira stays the edit boundary (same rule as the giro batch). + // C165 — without a município, only coordination/candidate may create; + // advisors cannot reach this branch (their `canCreateActivity` requires + // a município), but the guard keeps the rule local to the action too. + const municipalityID = raw.municipality ?? null const writableIDs = await getWritableMunicipalityIds(payload, currentActor, req) - if (writableIDs !== null && !writableIDs.includes(raw.municipality)) { + if (municipalityID === null) { + if (!isCampaignUnrestricted(currentActor)) { + throw new Error(ACTIVITY_UNSCOPED_ADVISOR_MESSAGE) + } + } else if (writableIDs !== null && !writableIDs.includes(municipalityID)) { throw new Error(ACTIVITY_OUT_OF_SCOPE_MESSAGE) } + if (municipalityID === null && demands.length > 0) { + throw new Error(ACTIVITY_DEMANDS_MUNICIPALITY_MESSAGE) + } const activity = await payload.create({ collection: 'activity', @@ -139,19 +154,21 @@ export const createActivityRecord = async ( req, }) - for (const demand of demands) { - await payload.create({ - collection: 'campaignDemand', - data: hookFilledCreateData<'campaignDemand'>({ - ...demand, - municipality: raw.municipality, - activity: activity.id, - }), - depth: 0, - user: currentActor, - overrideAccess: false, - req, - }) + if (demands.length > 0 && municipalityID !== null) { + for (const demand of demands) { + await payload.create({ + collection: 'campaignDemand', + data: hookFilledCreateData<'campaignDemand'>({ + ...demand, + municipality: municipalityID, + activity: activity.id, + }), + depth: 0, + user: currentActor, + overrideAccess: false, + req, + }) + } } return activity @@ -254,7 +271,7 @@ export const createTourDraftActivitiesRecord = async ( ) } -const updateActivityRecord = async ( +export const updateActivityRecord = async ( payload: Payload, actor: CampaignUser, input: ActivityUpdateInput, @@ -280,6 +297,24 @@ const updateActivityRecord = async ( const currentActor = await reloadCampaignActor(payload, actor, req) await acquireTextAdvisoryLocks(payload, req, [`activity:${id}`]) + // C165 — the Edição axis grants the ROW, not any município: an advisor + // cannot clear the município (coordination/candidate-only) nor point the + // activity at one outside their write scope. Coordination/candidate are + // unrestricted by definition. + if (!isCampaignUnrestricted(currentActor)) { + const writableIDs = await getWritableMunicipalityIds(payload, currentActor, req) + if (data.municipality === null) { + throw new Error(ACTIVITY_UNSCOPED_ADVISOR_MESSAGE) + } + if ( + typeof data.municipality === 'number' && + writableIDs !== null && + !writableIDs.includes(data.municipality) + ) { + throw new Error(ACTIVITY_OUT_OF_SCOPE_MESSAGE) + } + } + const activity = (await payload.update({ collection: 'activity', id, @@ -290,21 +325,25 @@ const updateActivityRecord = async ( req, })) as unknown as Activity - const municipality = - typeof activity.municipality === 'number' ? activity.municipality : activity.municipality.id - for (const demand of demands) { - await payload.create({ - collection: 'campaignDemand', - data: hookFilledCreateData<'campaignDemand'>({ - ...demand, - municipality, - activity: activity.id, - }), - depth: 0, - user: currentActor, - overrideAccess: false, - req, - }) + const municipality = relationshipId(activity.municipality) + if (demands.length > 0 && municipality === null) { + throw new Error(ACTIVITY_DEMANDS_MUNICIPALITY_MESSAGE) + } + if (municipality !== null) { + for (const demand of demands) { + await payload.create({ + collection: 'campaignDemand', + data: hookFilledCreateData<'campaignDemand'>({ + ...demand, + municipality, + activity: activity.id, + }), + depth: 0, + user: currentActor, + overrideAccess: false, + req, + }) + } } return activity diff --git a/src/collections/Activity.ts b/src/collections/Activity.ts index 16734594..867e6c61 100644 --- a/src/collections/Activity.ts +++ b/src/collections/Activity.ts @@ -93,7 +93,16 @@ const setCanonicalActivitySlug: CollectionBeforeValidateHook = ({ throw new APIError('O título da atividade não pode ser alterado após a criação.', 409) } data.title = title - data.slug = operation === 'create' ? slug : originalDoc?.slug + if (operation === 'create') { + // C165 — an imported Google event carries its own deterministic slug + // (title + occurrence suffix) so two occurrences of a recurring event + // never collide on the unique index. Sanitize and preserve it; fall back + // to the title's own slug when absent. + const providedSlug = isGoogleCalendarWrite ? slugify(String(data.slug ?? '')) : '' + data.slug = providedSlug || slug + } else { + data.slug = originalDoc?.slug + } return data } @@ -556,7 +565,10 @@ export const Activity: CollectionConfig = { type: 'relationship', relationTo: 'municipality', label: 'Município', - required: true, + // C165 — optional: imported Google events are born without a município + // and the coordination triages later. Without one, only the + // coordination/candidate see the activity (see access/activities). + required: false, index: true, }, { @@ -768,6 +780,37 @@ export const Activity: CollectionConfig = { update: canSetActivitySystemField, }, }, + { + // C165 — link to the FOREIGN Google event this activity was imported + // from. Its presence is what makes an activity "imported": the mirror + // writes `teqo…` events and never fills this field. `unique` is the + // idempotency lock (Postgres allows many NULLs); the calendar id pins + // WHICH calendar, so a calendar switch never patches the wrong event. + name: 'googleEventId', + type: 'text', + label: 'Evento do Google', + unique: true, + index: true, + admin: { + readOnly: true, + }, + access: { + create: canSetActivitySystemField, + update: canSetActivitySystemField, + }, + }, + { + name: 'googleCalendarId', + type: 'text', + label: 'Calendário do Google', + admin: { + readOnly: true, + }, + access: { + create: canSetActivitySystemField, + update: canSetActivitySystemField, + }, + }, systemStampedActorField({ label: 'Criada por', setAccess: canSetActivitySystemField }), ], } diff --git a/src/components/campaign/activity/ActivityAgenda.css b/src/components/campaign/activity/ActivityAgenda.css index 778b8305..9f0ba397 100644 --- a/src/components/campaign/activity/ActivityAgenda.css +++ b/src/components/campaign/activity/ActivityAgenda.css @@ -283,6 +283,16 @@ white-space: nowrap; } +.activity-agenda-imported { + overflow: hidden; + font-size: 0.625rem; + font-weight: 600; + letter-spacing: 0.04em; + opacity: 0.85; + text-overflow: ellipsis; + white-space: nowrap; +} + .activity-agenda-event-status { font-size: 0.625rem; font-weight: 600; diff --git a/src/components/campaign/activity/ActivityAgenda.tsx b/src/components/campaign/activity/ActivityAgenda.tsx index c62c29ae..cf09bfd0 100644 --- a/src/components/campaign/activity/ActivityAgenda.tsx +++ b/src/components/campaign/activity/ActivityAgenda.tsx @@ -48,7 +48,9 @@ import { } from '@/lib/activityAllDay' import { formatBahiaCivilDate } from '@/lib/campaignTime' import { + ACTIVITY_IMPORTED_LABEL, ACTIVITY_RESCHEDULE_FAILED_MESSAGE, + ACTIVITY_UNSCOPED_LABEL, activityStatusLabels, type ActivityStatus, } from '@/lib/schemas/activity' @@ -72,6 +74,8 @@ type AgendaEventProps = { municipalityName: string | null locality: string | null tags: string[] + /** C165 — imported from the Google Calendar (link present). */ + importedFromGoogle: boolean } const eventColors: Record = { @@ -115,6 +119,7 @@ const toEventInput = (event: ActivityAgendaEvent): EventInput => ({ municipalityName: event.municipality?.name ?? null, locality: event.locality, tags: event.tags, + importedFromGoogle: event.importedFromGoogle, } satisfies AgendaEventProps, }) @@ -150,6 +155,13 @@ const renderEventContent = ({ event, timeText, view }: EventDisplayInfo) => { {event.title} {location ? {location} : null} + {props.importedFromGoogle ? ( + + {props.municipalityName + ? ACTIVITY_IMPORTED_LABEL + : `${ACTIVITY_IMPORTED_LABEL} · ${ACTIVITY_UNSCOPED_LABEL}`} + + ) : null} {tags ? {tags} : null} {activityStatusLabels[props.status]} diff --git a/src/components/campaign/activity/ActivityCard.tsx b/src/components/campaign/activity/ActivityCard.tsx index a0f5c22f..de646ac6 100644 --- a/src/components/campaign/activity/ActivityCard.tsx +++ b/src/components/campaign/activity/ActivityCard.tsx @@ -4,6 +4,7 @@ import Link from 'next/link' import { ActivityStatusBadge } from '@/components/campaign/activity/ActivityStatusBadge' import { Badge } from '@/components/ui/Badge' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { ACTIVITY_IMPORTED_LABEL, ACTIVITY_UNSCOPED_LABEL } from '@/lib/schemas/activity' import { formatActivityWhenLabel, type ActivityListViewModel } from '@/utilities/activityViewModels' export const ActivityCard = ({ activity }: { activity: ActivityListViewModel }) => ( @@ -16,6 +17,16 @@ export const ActivityCard = ({ activity }: { activity: ActivityListViewModel }) ))} + {activity.importedFromGoogle ? ( + + {ACTIVITY_IMPORTED_LABEL} + + ) : null} + {!activity.municipalityName ? ( + + {ACTIVITY_UNSCOPED_LABEL} + + ) : null} {activity.deputyPresent ? Deputado presente : null} @@ -66,9 +77,16 @@ export const ActivityCard = ({ activity }: { activity: ActivityListViewModel }) 'Resp: Não definido' )} - {activity.taskProgress.total > 0 ? ( - - {activity.taskProgress.done}/{activity.taskProgress.total} tarefas + {activity.importedFromGoogle || activity.taskProgress.total > 0 ? ( + + {activity.importedFromGoogle ? ( + Importado automaticamente + ) : null} + {activity.taskProgress.total > 0 ? ( + + {activity.taskProgress.done}/{activity.taskProgress.total} tarefas + + ) : null} ) : null} diff --git a/src/components/campaign/activity/ActivityOverlay.tsx b/src/components/campaign/activity/ActivityOverlay.tsx index 43578329..05a54b01 100644 --- a/src/components/campaign/activity/ActivityOverlay.tsx +++ b/src/components/campaign/activity/ActivityOverlay.tsx @@ -52,6 +52,7 @@ import { formatIsoAsBahiaDateTimeInput, parseBahiaDateTimeInput, } from '@/lib/campaignTime' +import { ACTIVITY_UNSCOPED_LABEL } from '@/lib/schemas/activity' import type { ActivityAgendaState } from '@/utilities/activityUi' import type { ActivityFormViewModel } from '@/utilities/activityViewModels' import type { StrictComboboxOption } from '@/utilities/territory/territoryComboboxOptions' @@ -84,8 +85,12 @@ type ActivityOverlayProps = { onSaved: () => void } -const inlineMunicipalityOptions = (options: RelationOption[]): StrictComboboxOption[] => - options.map((option) => ({ value: String(option.id), label: option.name })) +const inlineMunicipalityOptions = (options: RelationOption[]): StrictComboboxOption[] => [ + // C165 — the explicit "no município" state; imported Google events are born + // here and only coordination/candidate can keep them (server-enforced). + { value: '', label: ACTIVITY_UNSCOPED_LABEL }, + ...options.map((option) => ({ value: String(option.id), label: option.name })), +] /** List-row styling for the label-less mobile sheet (C103). */ const sheetFieldInputClass = @@ -192,7 +197,6 @@ const ActivityOverlayForm = ({ event.preventDefault() const formData = new FormData(event.currentTarget) const trimmedTitle = title.trim() - const municipalityId = Number(municipalityValue) const startIso = parseBahiaDateTimeInput(start) const endIso = end ? parseBahiaDateTimeInput(end) : undefined @@ -202,7 +206,8 @@ const ActivityOverlayForm = ({ } else if (!isEdit && trimmedTitle.length < 2) { nextFieldErrors.title = ['Informe um título com ao menos 2 caracteres.'] } - if (!municipalityId) nextFieldErrors.municipality = ['Informe o município.'] + // C165 — município is optional: "Sem município" is a valid state for an + // imported event awaiting triage (the server gates who may keep it). if (allDay) { if (!startIso) nextFieldErrors.startAt = ['Informe a data de início do compromisso.'] if (startIso && endIso && !allDayRangeValid(startIso, endIso)) { @@ -374,7 +379,7 @@ const ActivityOverlayForm = ({ <> - Município * + Município (opcional) {errorFor('municipality') ? ( {errorFor('municipality')} ) : null} + {!municipalityValue ? ( +

+ Sem município, só coordenação e candidato veem. Ao atribuir, o assessor da carteira + passa a ver. +

+ ) : null}
diff --git a/src/lib/googleCalendarEventMapping.ts b/src/lib/googleCalendarEventMapping.ts index 5610dcc4..30307001 100644 --- a/src/lib/googleCalendarEventMapping.ts +++ b/src/lib/googleCalendarEventMapping.ts @@ -13,8 +13,17 @@ */ import type { Activity } from '@/payload-types' -import { allDayCivilDateOf, allDayExclusiveEndDate } from '@/lib/activityAllDay' +import { + allDayCivilDateOf, + allDayEndInstantFromExclusive, + allDayExclusiveEndDate, + allDayStartInstant, + isCivilDate, +} from '@/lib/activityAllDay' import { buildActivityDescriptionParts } from '@/lib/activityDescription' +import { formatBahiaCivilDate, formatIsoAsBahiaDateTimeInput } from '@/lib/campaignTime' +import { slugify } from '@/lib/slug' +import { CALENDAR_PHASE_ANCHORS } from '@/lib/visitPlannerAnchors' const GOOGLE_EVENT_ID_PREFIX = 'teqo' @@ -106,13 +115,9 @@ export const buildGoogleCalendarDescription = ( municipalityName?: string, ): string => buildActivityDescriptionParts(activity, municipalityName).join('\n') -export const buildGoogleEventPayload = ( - activity: Pick< - Activity, - 'id' | 'title' | 'startAt' | 'endAt' | 'allDay' | 'locality' | 'tags' | 'deputyPresent' - >, - municipalityName?: string, -): GoogleCalendarEventPayload => { +const buildGoogleEventSchedule = ( + activity: Pick, +): { start: GoogleCalendarEventPayload['start']; end: GoogleCalendarEventPayload['end'] } => { // C14 guarantees startAt beyond draft; a null start has no Google shape — // fail loudly in the mapping (the engine never feeds one: the window query // only returns dated activities). @@ -120,9 +125,6 @@ export const buildGoogleEventPayload = ( throw new Error('Atividade sem data de início não pode virar evento do Google.') } - const summary = municipalityName ? `[${municipalityName}] ${activity.title}` : activity.title - const description = buildGoogleCalendarDescription(activity, municipalityName) - const allDay = Boolean(activity.allDay) const start = allDay ? { date: allDayCivilDateOf(activity.startAt) } @@ -134,6 +136,20 @@ export const buildGoogleEventPayload = ( activity.endAt ?? addMinutes(activity.startAt, DEFAULT_TIMED_EVENT_DURATION_MINUTES), ), } + return { start, end } +} + +export const buildGoogleEventPayload = ( + activity: Pick< + Activity, + 'id' | 'title' | 'startAt' | 'endAt' | 'allDay' | 'locality' | 'tags' | 'deputyPresent' + >, + municipalityName?: string, +): GoogleCalendarEventPayload => { + const summary = municipalityName ? `[${municipalityName}] ${activity.title}` : activity.title + const description = buildGoogleCalendarDescription(activity, municipalityName) + + const { start, end } = buildGoogleEventSchedule(activity) const locality = activity.locality?.trim() const location = locality || municipalityName || undefined @@ -198,3 +214,227 @@ export const googleEventContentEquals = ( (remote.location ?? '') === (payload.location ?? '') && startEndInstantEquals(remote.start, payload.start) && startEndInstantEquals(remote.end, payload.end) + +/** Matches the `title` field cap in the Activity collection — the same bound the Teqo form enforces. */ +const ACTIVITY_TITLE_MAX_LENGTH = 160 + +/** + * The summary carries a `[Município] ` prefix we wrote; the user edits what + * follows it. Only OUR OWN prefix is stripped (matched against the activity's + * municipality name); anything else is the user's title verbatim — and the + * forward direction re-prefixes it consistently. A summary that is ONLY the + * prefix is structural tampering → null → the Teqo re-asserts. + */ +export const googleTitleFromSummary = ( + summary: string | undefined, + municipalityName?: string, +): string | null => { + const raw = summary?.trim() + if (!raw) return null + const stripped = + municipalityName && raw.startsWith(`[${municipalityName}]`) + ? raw.slice(municipalityName.length + 2).trim() + : raw + if (!stripped) return null + return stripped.length > ACTIVITY_TITLE_MAX_LENGTH + ? stripped.slice(0, ACTIVITY_TITLE_MAX_LENGTH) + : stripped +} + +const parseInstant = (value: string | undefined): string | null => { + if (!value) return null + const parsed = new Date(value) + return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString() +} + +/** + * Event start/end → activity schedule. `date` (all-day) maps through the + * `activityAllDay` conventions; `dateTime` (timed) maps by absolute instant. + * Malformed values (missing end, exclusive end not after start, unparseable) + * → null: the Teqo keeps its state and the forward direction re-asserts + * (fail-safe — a bad Google value never corrupts the activity). + */ +export const googleScheduleToActivityFields = ( + event: Pick, +): { startAt: string | null; endAt: string | null; allDay: boolean } | null => { + const start = event.start + if (!start) return null + + if (start.date) { + if (!isCivilDate(start.date)) return null + const startAt = allDayStartInstant(start.date) + let endAt = startAt + if (event.end?.date) { + if (!isCivilDate(event.end.date) || event.end.date <= start.date) return null + endAt = allDayEndInstantFromExclusive(event.end.date) + } + return { startAt, endAt, allDay: true } + } + + if (start.dateTime) { + const startAt = parseInstant(start.dateTime) + if (!startAt || !event.end?.dateTime) return null + const endAt = parseInstant(event.end.dateTime) + if (!endAt) return null + return { startAt, endAt, allDay: false } + } + + return null +} + +/** + * C165 — fire only for events the mirror does NOT own: the full deterministic + * id decode is null. A manual `teqo3`-looking id decodes (non-null) and stays + * in the mirror namespace — never imported. + */ +export const isForeignGoogleEvent = (event: GoogleRemoteEvent): boolean => { + const id = event.id + return typeof id === 'string' && id.length > 0 && decodeGoogleEventActivityId(id) === null +} + +/** C165 — the import window mirrors the push window (90d/365d) with the cut as its floor. */ +export type GoogleEventImportWindow = { rangeStart: string; rangeEnd: string } + +/** + * C165 — the product cut: only events that START on/after the legal campaign + * start (`CALENDAR_PHASE_ANCHORS.consolidationStart`) are imported — no + * backfill, ever. The comparison is a Bahia civil date (the day turns at + * midnight in Bahia, not in UTC), the same rule the phase anchors use. + */ +export const isImportableGoogleEvent = ( + event: GoogleRemoteEvent, + window: GoogleEventImportWindow, +): boolean => { + if (!event.id || event.status === GOOGLE_EVENT_STATUS_CANCELLED) return false + if (!isForeignGoogleEvent(event)) return false + const schedule = googleScheduleToActivityFields(event) + if (!schedule?.startAt) return false + + const startMs = Date.parse(schedule.startAt) + const rangeStartMs = Date.parse(window.rangeStart) + const rangeEndMs = Date.parse(window.rangeEnd) + if (Number.isNaN(startMs) || !(startMs >= rangeStartMs && startMs < rangeEndMs)) return false + + return ( + formatBahiaCivilDate(new Date(schedule.startAt)) >= CALENDAR_PHASE_ANCHORS.consolidationStart + ) +} + +/** + * Short deterministic digest of the Google event id (FNV-1a, base36). The + * slug suffix must be unique even for two distinct events with the same title + * and start (duplicated invites are real): the event id is the only unique + * input, and hashing keeps the slug reproducible across passes. + */ +const shortGoogleEventHash = (eventId: string): string => { + let hash = 2166136261 + for (let index = 0; index < eventId.length; index += 1) { + hash ^= eventId.charCodeAt(index) + hash = Math.imul(hash, 16777619) + } + return (hash >>> 0).toString(36) +} + +/** + * C165 — deterministic slug for an imported event: title + occurrence (civil + * date/date-time in Bahia) + event digest. The occurrence suffix keeps two + * instances of a recurring series apart; the digest keeps equal title+start + * events apart. + */ +const importedActivitySlug = ( + title: string, + schedule: { startAt: string | null; allDay: boolean }, + eventId: string, +): string => { + const occurrence = schedule.allDay + ? allDayCivilDateOf(schedule.startAt ?? '') + : formatIsoAsBahiaDateTimeInput(schedule.startAt ?? '') + const suffix = occurrence.replace(/[-:]/g, '') + const base = slugify(title) || 'evento' + return `${base}-${suffix}-${shortGoogleEventHash(eventId)}` +} + +/** + * C165 — the activity an imported Google event is born as: `confirmado`, + * title verbatim (no `[Município] ` prefix — that prefix belongs to mirrored + * events), `location → locality`, description deliberately ignored (the text + * lives in Google), and the system-write link that makes the import + * idempotent. Null when the event has no usable title/schedule (skip — never + * invent a title in an immutable field, never pause the pass). + */ +export const buildImportedGoogleEventActivity = ( + event: GoogleRemoteEvent, + calendarId: string, +): { + title: string + slug: string + status: 'confirmado' + startAt: string + endAt?: string + allDay: boolean + locality?: string + googleEventId: string + googleCalendarId: string + tags: string[] +} | null => { + if (!event.id) return null + const title = googleTitleFromSummary(event.summary, undefined) + if (!title || title.length < 2 || slugify(title) === '') return null + + const schedule = googleScheduleToActivityFields(event) + if (!schedule?.startAt) return null + + const locality = event.location?.trim().slice(0, 160) + + return { + title, + slug: importedActivitySlug(title, schedule, event.id), + status: 'confirmado', + startAt: schedule.startAt, + ...(schedule.endAt ? { endAt: schedule.endAt } : {}), + allDay: schedule.allDay, + ...(locality ? { locality } : {}), + googleEventId: event.id, + googleCalendarId: calendarId, + tags: [], + } +} + +/** + * C165 — the partial event the engine PATCHes for an imported activity: only + * the fields the Teqo owns from then on (title, schedule, location). No + * `description` (the Google text is preserved by the partial PATCH), no + * `id` (the linked remote id is the target), no prefix on the summary. + */ +export type GoogleImportedEventPatch = { + summary: string + location: string + start: GoogleCalendarEventPayload['start'] + end: GoogleCalendarEventPayload['end'] +} + +export const buildImportedGoogleEventPatch = ( + activity: Pick, +): GoogleImportedEventPatch => { + const { start, end } = buildGoogleEventSchedule(activity) + return { + summary: activity.title, + location: activity.locality?.trim() ?? '', + start, + end, + } +} + +/** + * C165 — content equality for imported activities: summary + schedule + + * location. Description is out by design (the Teqo never reads, stores or + * writes the Google description of an imported event). + */ +export const googleImportedEventContentEquals = ( + remote: GoogleRemoteEvent, + patch: GoogleImportedEventPatch, +): boolean => + (remote.summary ?? '') === patch.summary && + (remote.location ?? '') === patch.location && + startEndInstantEquals(remote.start, patch.start) && + startEndInstantEquals(remote.end, patch.end) diff --git a/src/lib/googleCalendarReverseEdit.ts b/src/lib/googleCalendarReverseEdit.ts index 74e03545..1bc71654 100644 --- a/src/lib/googleCalendarReverseEdit.ts +++ b/src/lib/googleCalendarReverseEdit.ts @@ -7,14 +7,8 @@ * * This module is pure and client-safe; only the engine touches the network. */ -import { - allDayEndInstantFromExclusive, - allDayStartInstant, - formatAllDayRangeLabel, - isCivilDate, -} from '@/lib/activityAllDay' +import { formatAllDayRangeLabel } from '@/lib/activityAllDay' import { formatBahiaDateTimeLabel } from '@/lib/campaignTime' -import type { GoogleRemoteEvent } from '@/lib/googleCalendarEventMapping' /** Body prefix that marks an activity-update entry as written by the reverse direction. */ export const GOOGLE_REVERSE_EDIT_BODY_PREFIX = 'Google Calendar:' @@ -27,9 +21,6 @@ export type GoogleReverseActivityEdit = { allDay?: boolean | null } -/** Matches the `title` field cap in the Activity collection — the same bound the Teqo form enforces. */ -const ACTIVITY_TITLE_MAX_LENGTH = 160 - /** * D3 — the conflict clock rule: the Google edit wins only when its `updated` * instant is strictly newer than the activity's last Teqo write, with a small @@ -49,70 +40,6 @@ export const googleEditIsNewer = ( return remoteMs > localMs + toleranceMs } -/** - * The summary carries a `[Município] ` prefix we wrote; the user edits what - * follows it. Only OUR OWN prefix is stripped (matched against the activity's - * municipality name); anything else is the user's title verbatim — and the - * forward direction re-prefixes it consistently. A summary that is ONLY the - * prefix is structural tampering → null → the Teqo re-asserts. - */ -export const googleTitleFromSummary = ( - summary: string | undefined, - municipalityName?: string, -): string | null => { - const raw = summary?.trim() - if (!raw) return null - const stripped = - municipalityName && raw.startsWith(`[${municipalityName}]`) - ? raw.slice(municipalityName.length + 2).trim() - : raw - if (!stripped) return null - return stripped.length > ACTIVITY_TITLE_MAX_LENGTH - ? stripped.slice(0, ACTIVITY_TITLE_MAX_LENGTH) - : stripped -} - -const parseInstant = (value: string | undefined): string | null => { - if (!value) return null - const parsed = new Date(value) - return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString() -} - -/** - * Event start/end → activity schedule. `date` (all-day) maps through the - * `activityAllDay` conventions; `dateTime` (timed) maps by absolute instant. - * Malformed values (missing end, exclusive end not after start, unparseable) - * → null: the Teqo keeps its state and the forward direction re-asserts - * (fail-safe — a bad Google value never corrupts the activity). - */ -export const googleScheduleToActivityFields = ( - event: Pick, -): Pick | null => { - const start = event.start - if (!start) return null - - if (start.date) { - if (!isCivilDate(start.date)) return null - const startAt = allDayStartInstant(start.date) - let endAt = startAt - if (event.end?.date) { - if (!isCivilDate(event.end.date) || event.end.date <= start.date) return null - endAt = allDayEndInstantFromExclusive(event.end.date) - } - return { startAt, endAt, allDay: true } - } - - if (start.dateTime) { - const startAt = parseInstant(start.dateTime) - if (!startAt || !event.end?.dateTime) return null - const endAt = parseInstant(event.end.dateTime) - if (!endAt) return null - return { startAt, endAt, allDay: false } - } - - return null -} - const scheduleLabelOf = ( schedule: Pick, ): string | null => { diff --git a/src/lib/schemas/activity.ts b/src/lib/schemas/activity.ts index 42021123..07b54b85 100644 --- a/src/lib/schemas/activity.ts +++ b/src/lib/schemas/activity.ts @@ -29,6 +29,17 @@ export const ACTIVITY_RESCHEDULE_FAILED_MESSAGE = 'Não foi possível remarcar a atividade. O horário anterior foi mantido.' export const ACTIVITY_OUT_OF_SCOPE_MESSAGE = 'O município escolhido está fora do seu escopo de edição. Atualize a página e tente novamente.' +export const ACTIVITY_UNSCOPED_ADVISOR_MESSAGE = + 'Só a coordenação e o candidato podem criar ou manter uma atividade sem município.' +export const ACTIVITY_DEMANDS_MUNICIPALITY_MESSAGE = + 'Atribua um município para criar demandas desta atividade.' + +/** + * C165 — the shared labels of the imported/triage states (overlay option, + * card badges, agenda marker and omnibox chip/seed): one spelling, no drift. + */ +export const ACTIVITY_IMPORTED_LABEL = 'Do Google' +export const ACTIVITY_UNSCOPED_LABEL = 'Sem município' /** * C14 — tags are free-form labels the mesa invents (comício, imprensa, etc.). @@ -257,6 +268,10 @@ export const activityRescheduleSchema = z export const activityCreateSchema = activityFieldsSchema .extend({ status: z.enum(['confirmado']).default('confirmado'), + // C165 — an activity may be born without a municipality (an imported + // Google event awaiting triage). The base field stays required so the giro + // stop pick keeps its município obligation. + municipality: positiveRelationshipId.nullable().optional(), }) .superRefine((data, context) => { validateSchedule(data, context, true) @@ -271,6 +286,8 @@ export const activityUpdateSchema = activityFieldsSchema .partial() .extend({ id: positiveRelationshipId, + // C165 — `null` clears the município; only coordination/candidate can. + municipality: positiveRelationshipId.nullable().optional(), locality: trimmedNullableText(160), description: trimmedNullableText(4000), startAt: z.string().datetime().nullable().optional(), diff --git a/src/migrations/20260915_142058_activity_google_import_link.json b/src/migrations/20260915_142058_activity_google_import_link.json new file mode 100644 index 00000000..4f215e55 --- /dev/null +++ b/src/migrations/20260915_142058_activity_google_import_link.json @@ -0,0 +1,15423 @@ +{ + "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 + }, + "contact_id": { + "name": "contact_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "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'" + }, + "visibility": { + "name": "visibility", + "type": "enum_campaign_user_visibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'carteira'" + }, + "editing": { + "name": "editing", + "type": "enum_campaign_user_editing", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'carteira'" + }, + "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_contact_idx": { + "name": "campaign_user_contact_idx", + "columns": [ + { + "expression": "contact_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "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_contact_id_contact_id_fk": { + "name": "campaign_user_contact_id_contact_id_fk", + "tableFrom": "campaign_user", + "tableTo": "contact", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "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": "set null", + "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 + }, + "campaign_user_id": { + "name": "campaign_user_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": {} + }, + "leadership_rels_campaign_user_id_idx": { + "name": "leadership_rels_campaign_user_id_idx", + "columns": [ + { + "expression": "campaign_user_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" + }, + "leadership_rels_campaign_user_fk": { + "name": "leadership_rels_campaign_user_fk", + "tableFrom": "leadership_rels", + "tableTo": "campaign_user", + "columnsFrom": [ + "campaign_user_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 + }, + "ballot_name": { + "name": "ballot_name", + "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": "set null", + "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 + }, + "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": { + "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_campaign_user_id_idx": { + "name": "campaign_demand_rels_campaign_user_id_idx", + "columns": [ + { + "expression": "campaign_user_id", + "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_campaign_user_fk": { + "name": "campaign_demand_rels_campaign_user_fk", + "tableFrom": "campaign_demand_rels", + "tableTo": "campaign_user", + "columnsFrom": [ + "campaign_user_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": false + }, + "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_comments": { + "name": "municipality_update_comments", + "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": { + "municipality_update_comments_order_idx": { + "name": "municipality_update_comments_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "municipality_update_comments_parent_id_idx": { + "name": "municipality_update_comments_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "municipality_update_comments_author_idx": { + "name": "municipality_update_comments_author_idx", + "columns": [ + { + "expression": "author_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "municipality_update_comments_author_id_campaign_user_id_fk": { + "name": "municipality_update_comments_author_id_campaign_user_id_fk", + "tableFrom": "municipality_update_comments", + "tableTo": "campaign_user", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "municipality_update_comments_parent_id_fk": { + "name": "municipality_update_comments_parent_id_fk", + "tableFrom": "municipality_update_comments", + "tableTo": "municipality_update", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "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 + }, + "polarity": { + "name": "polarity", + "type": "enum_municipality_update_polarity", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'neutra'" + }, + "urgent": { + "name": "urgent", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "adversary_signal": { + "name": "adversary_signal", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "body": { + "name": "body", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "active_volunteers": { + "name": "active_volunteers", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "new_supports": { + "name": "new_supports", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "responsible_id": { + "name": "responsible_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "resolved_by_id": { + "name": "resolved_by_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_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_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_polarity_idx": { + "name": "municipality_update_polarity_idx", + "columns": [ + { + "expression": "polarity", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "municipality_update_urgent_idx": { + "name": "municipality_update_urgent_idx", + "columns": [ + { + "expression": "urgent", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "municipality_update_adversary_signal_idx": { + "name": "municipality_update_adversary_signal_idx", + "columns": [ + { + "expression": "adversary_signal", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "municipality_update_responsible_idx": { + "name": "municipality_update_responsible_idx", + "columns": [ + { + "expression": "responsible_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "municipality_update_resolved_by_idx": { + "name": "municipality_update_resolved_by_idx", + "columns": [ + { + "expression": "resolved_by_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "municipality_update_resolved_at_idx": { + "name": "municipality_update_resolved_at_idx", + "columns": [ + { + "expression": "resolved_at", + "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" + }, + "municipality_update_responsible_id_campaign_user_id_fk": { + "name": "municipality_update_responsible_id_campaign_user_id_fk", + "tableFrom": "municipality_update", + "tableTo": "campaign_user", + "columnsFrom": [ + "responsible_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "municipality_update_resolved_by_id_campaign_user_id_fk": { + "name": "municipality_update_resolved_by_id_campaign_user_id_fk", + "tableFrom": "municipality_update", + "tableTo": "campaign_user", + "columnsFrom": [ + "resolved_by_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 + }, + "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 + }, + "status": { + "name": "status", + "type": "enum_activity_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'confirmado'" + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "deputy_present": { + "name": "deputy_present", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "all_day": { + "name": "all_day", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "public_event": { + "name": "public_event", + "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 + }, + "municipality_id": { + "name": "municipality_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "locality": { + "name": "locality", + "type": "varchar", + "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 + }, + "last_mirrored_change_at": { + "name": "last_mirrored_change_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "google_event_id": { + "name": "google_event_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "google_calendar_id": { + "name": "google_calendar_id", + "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": { + "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_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_all_day_idx": { + "name": "activity_all_day_idx", + "columns": [ + { + "expression": "all_day", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_public_event_idx": { + "name": "activity_public_event_idx", + "columns": [ + { + "expression": "public_event", + "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_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_google_event_id_idx": { + "name": "activity_google_event_id_idx", + "columns": [ + { + "expression": "google_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "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_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_texts": { + "name": "activity_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": { + "activity_texts_order_parent": { + "name": "activity_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": {} + }, + "activity_texts_text_idx": { + "name": "activity_texts_text_idx", + "columns": [ + { + "expression": "text", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "activity_texts_parent_fk": { + "name": "activity_texts_parent_fk", + "tableFrom": "activity_texts", + "tableTo": "activity", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "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 + }, + "leadership_id": { + "name": "leadership_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "state_deputy_id": { + "name": "state_deputy_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_leadership_id_idx": { + "name": "activity_rels_leadership_id_idx", + "columns": [ + { + "expression": "leadership_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_rels_state_deputy_id_idx": { + "name": "activity_rels_state_deputy_id_idx", + "columns": [ + { + "expression": "state_deputy_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_leadership_fk": { + "name": "activity_rels_leadership_fk", + "tableFrom": "activity_rels", + "tableTo": "leadership", + "columnsFrom": [ + "leadership_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "activity_rels_state_deputy_fk": { + "name": "activity_rels_state_deputy_fk", + "tableFrom": "activity_rels", + "tableTo": "state_deputy", + "columnsFrom": [ + "state_deputy_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.speech_topics": { + "name": "speech_topics", + "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_speech_topics", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + } + }, + "indexes": { + "speech_topics_order_idx": { + "name": "speech_topics_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "speech_topics_parent_idx": { + "name": "speech_topics_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "speech_topics_value_idx": { + "name": "speech_topics_value_idx", + "columns": [ + { + "expression": "value", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "speech_topics_parent_fk": { + "name": "speech_topics_parent_fk", + "tableFrom": "speech_topics", + "tableTo": "speech", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.speech_scopes": { + "name": "speech_scopes", + "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_speech_scopes", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + } + }, + "indexes": { + "speech_scopes_order_idx": { + "name": "speech_scopes_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "speech_scopes_parent_idx": { + "name": "speech_scopes_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "speech_scopes_value_idx": { + "name": "speech_scopes_value_idx", + "columns": [ + { + "expression": "value", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "speech_scopes_parent_fk": { + "name": "speech_scopes_parent_fk", + "tableFrom": "speech_scopes", + "tableTo": "speech", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.speech": { + "name": "speech", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "source_key": { + "name": "source_key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "speech_at": { + "name": "speech_at", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "year": { + "name": "year", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "legislature": { + "name": "legislature", + "type": "enum_speech_legislature", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "phase": { + "name": "phase", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "duration_seconds": { + "name": "duration_seconds", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "official_transcript": { + "name": "official_transcript", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "official_text_url": { + "name": "official_text_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "search_text": { + "name": "search_text", + "type": "varchar", + "primaryKey": false, + "notNull": false, + "default": "''" + }, + "event_id": { + "name": "event_id", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "event_start_at": { + "name": "event_start_at", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "event_end_at": { + "name": "event_end_at", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "youtube_url": { + "name": "youtube_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "presiding_officer": { + "name": "presiding_officer", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "audio_id": { + "name": "audio_id", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "excerpt_t_ms": { + "name": "excerpt_t_ms", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "vod_playback_url": { + "name": "vod_playback_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "vod_download_url": { + "name": "vod_download_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "classified_by": { + "name": "classified_by", + "type": "enum_speech_classified_by", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'gazetteer'" + }, + "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": { + "speech_source_key_idx": { + "name": "speech_source_key_idx", + "columns": [ + { + "expression": "source_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "speech_speech_at_idx": { + "name": "speech_speech_at_idx", + "columns": [ + { + "expression": "speech_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "speech_year_idx": { + "name": "speech_year_idx", + "columns": [ + { + "expression": "year", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "speech_legislature_idx": { + "name": "speech_legislature_idx", + "columns": [ + { + "expression": "legislature", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "speech_phase_idx": { + "name": "speech_phase_idx", + "columns": [ + { + "expression": "phase", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "speech_event_id_idx": { + "name": "speech_event_id_idx", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "speech_audio_id_idx": { + "name": "speech_audio_id_idx", + "columns": [ + { + "expression": "audio_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "speech_excerpt_t_ms_idx": { + "name": "speech_excerpt_t_ms_idx", + "columns": [ + { + "expression": "excerpt_t_ms", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "speech_classified_by_idx": { + "name": "speech_classified_by_idx", + "columns": [ + { + "expression": "classified_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "speech_updated_at_idx": { + "name": "speech_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "speech_created_at_idx": { + "name": "speech_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.speech_texts": { + "name": "speech_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": { + "speech_texts_order_parent": { + "name": "speech_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": { + "speech_texts_parent_fk": { + "name": "speech_texts_parent_fk", + "tableFrom": "speech_texts", + "tableTo": "speech", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.speech_rels": { + "name": "speech_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": { + "speech_rels_order_idx": { + "name": "speech_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "speech_rels_parent_idx": { + "name": "speech_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "speech_rels_path_idx": { + "name": "speech_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "speech_rels_municipality_id_idx": { + "name": "speech_rels_municipality_id_idx", + "columns": [ + { + "expression": "municipality_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "speech_rels_parent_fk": { + "name": "speech_rels_parent_fk", + "tableFrom": "speech_rels", + "tableTo": "speech", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "speech_rels_municipality_fk": { + "name": "speech_rels_municipality_fk", + "tableFrom": "speech_rels", + "tableTo": "municipality", + "columnsFrom": [ + "municipality_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.speech_segment": { + "name": "speech_segment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "speech_id": { + "name": "speech_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "start_seconds": { + "name": "start_seconds", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "end_seconds": { + "name": "end_seconds", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "search_text": { + "name": "search_text", + "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()" + } + }, + "indexes": { + "speech_segment_speech_idx": { + "name": "speech_segment_speech_idx", + "columns": [ + { + "expression": "speech_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "speech_segment_updated_at_idx": { + "name": "speech_segment_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "speech_segment_created_at_idx": { + "name": "speech_segment_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "speech_segment_speech_id_speech_id_fk": { + "name": "speech_segment_speech_id_speech_id_fk", + "tableFrom": "speech_segment", + "tableTo": "speech", + "columnsFrom": [ + "speech_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.speech_cut": { + "name": "speech_cut", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "speech_id": { + "name": "speech_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "start_seconds": { + "name": "start_seconds", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "end_seconds": { + "name": "end_seconds", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "duration_seconds": { + "name": "duration_seconds", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "enum_speech_cut_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'processing'" + }, + "step": { + "name": "step", + "type": "enum_speech_cut_step", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "media_id": { + "name": "media_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "published_at": { + "name": "published_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": { + "speech_cut_speech_idx": { + "name": "speech_cut_speech_idx", + "columns": [ + { + "expression": "speech_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "speech_cut_status_idx": { + "name": "speech_cut_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "speech_cut_media_idx": { + "name": "speech_cut_media_idx", + "columns": [ + { + "expression": "media_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "speech_cut_created_by_idx": { + "name": "speech_cut_created_by_idx", + "columns": [ + { + "expression": "created_by_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "speech_cut_updated_at_idx": { + "name": "speech_cut_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "speech_cut_created_at_idx": { + "name": "speech_cut_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "speech_cut_speech_id_speech_id_fk": { + "name": "speech_cut_speech_id_speech_id_fk", + "tableFrom": "speech_cut", + "tableTo": "speech", + "columnsFrom": [ + "speech_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "speech_cut_media_id_media_id_fk": { + "name": "speech_cut_media_id_media_id_fk", + "tableFrom": "speech_cut", + "tableTo": "media", + "columnsFrom": [ + "media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "speech_cut_created_by_id_campaign_user_id_fk": { + "name": "speech_cut_created_by_id_campaign_user_id_fk", + "tableFrom": "speech_cut", + "tableTo": "campaign_user", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.calendar_feed": { + "name": "calendar_feed", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "secret_slug": { + "name": "secret_slug", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "filter_municipality_id": { + "name": "filter_municipality_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "filter_deputy_present": { + "name": "filter_deputy_present", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "filter_tag": { + "name": "filter_tag", + "type": "varchar", + "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": { + "calendar_feed_secret_slug_idx": { + "name": "calendar_feed_secret_slug_idx", + "columns": [ + { + "expression": "secret_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "calendar_feed_filter_municipality_idx": { + "name": "calendar_feed_filter_municipality_idx", + "columns": [ + { + "expression": "filter_municipality_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "calendar_feed_revoked_at_idx": { + "name": "calendar_feed_revoked_at_idx", + "columns": [ + { + "expression": "revoked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "calendar_feed_created_by_idx": { + "name": "calendar_feed_created_by_idx", + "columns": [ + { + "expression": "created_by_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "calendar_feed_updated_at_idx": { + "name": "calendar_feed_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "calendar_feed_created_at_idx": { + "name": "calendar_feed_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "calendar_feed_filter_municipality_id_municipality_id_fk": { + "name": "calendar_feed_filter_municipality_id_municipality_id_fk", + "tableFrom": "calendar_feed", + "tableTo": "municipality", + "columnsFrom": [ + "filter_municipality_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "calendar_feed_created_by_id_campaign_user_id_fk": { + "name": "calendar_feed_created_by_id_campaign_user_id_fk", + "tableFrom": "calendar_feed", + "tableTo": "campaign_user", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.google_calendar_sync": { + "name": "google_calendar_sync", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "calendar_id": { + "name": "calendar_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "oauth_refresh_token": { + "name": "oauth_refresh_token", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "oauth_scope": { + "name": "oauth_scope", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "oauth_connected_at": { + "name": "oauth_connected_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "oauth_error_at": { + "name": "oauth_error_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "oauth_error": { + "name": "oauth_error", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "disabled_at": { + "name": "disabled_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "last_success_at": { + "name": "last_success_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error_at": { + "name": "last_error_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "push_channel_id": { + "name": "push_channel_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "push_channel_resource_id": { + "name": "push_channel_resource_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "push_channel_expires_at": { + "name": "push_channel_expires_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "push_channel_secret": { + "name": "push_channel_secret", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "push_channel_error": { + "name": "push_channel_error", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "last_seen_event_ids": { + "name": "last_seen_event_ids", + "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": { + "google_calendar_sync_calendar_id_idx": { + "name": "google_calendar_sync_calendar_id_idx", + "columns": [ + { + "expression": "calendar_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "google_calendar_sync_disabled_at_idx": { + "name": "google_calendar_sync_disabled_at_idx", + "columns": [ + { + "expression": "disabled_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "google_calendar_sync_updated_at_idx": { + "name": "google_calendar_sync_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "google_calendar_sync_created_at_idx": { + "name": "google_calendar_sync_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.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_phones": { + "name": "contact_phones", + "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 + }, + "value": { + "name": "value", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "contact_phones_order_idx": { + "name": "contact_phones_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "contact_phones_parent_id_idx": { + "name": "contact_phones_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "contact_phones_value_idx": { + "name": "contact_phones_value_idx", + "columns": [ + { + "expression": "value", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "contact_phones_parent_id_fk": { + "name": "contact_phones_parent_id_fk", + "tableFrom": "contact_phones", + "tableTo": "contact", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "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 + }, + "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": false + }, + "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_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 + }, + "campaign_level": { + "name": "campaign_level", + "type": "enum_subscription_campaign_level", + "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": { + "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 + }, + "speech_id": { + "name": "speech_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "speech_segment_id": { + "name": "speech_segment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "speech_cut_id": { + "name": "speech_cut_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "calendar_feed_id": { + "name": "calendar_feed_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "google_calendar_sync_id": { + "name": "google_calendar_sync_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_speech_id_idx": { + "name": "payload_locked_documents_rels_speech_id_idx", + "columns": [ + { + "expression": "speech_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_speech_segment_id_idx": { + "name": "payload_locked_documents_rels_speech_segment_id_idx", + "columns": [ + { + "expression": "speech_segment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_speech_cut_id_idx": { + "name": "payload_locked_documents_rels_speech_cut_id_idx", + "columns": [ + { + "expression": "speech_cut_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_calendar_feed_id_idx": { + "name": "payload_locked_documents_rels_calendar_feed_id_idx", + "columns": [ + { + "expression": "calendar_feed_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_google_calendar_sync_id_idx": { + "name": "payload_locked_documents_rels_google_calendar_sync_id_idx", + "columns": [ + { + "expression": "google_calendar_sync_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_speech_fk": { + "name": "payload_locked_documents_rels_speech_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "speech", + "columnsFrom": [ + "speech_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_speech_segment_fk": { + "name": "payload_locked_documents_rels_speech_segment_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "speech_segment", + "columnsFrom": [ + "speech_segment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_speech_cut_fk": { + "name": "payload_locked_documents_rels_speech_cut_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "speech_cut", + "columnsFrom": [ + "speech_cut_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_calendar_feed_fk": { + "name": "payload_locked_documents_rels_calendar_feed_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "calendar_feed", + "columnsFrom": [ + "calendar_feed_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_google_calendar_sync_fk": { + "name": "payload_locked_documents_rels_google_calendar_sync_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "google_calendar_sync", + "columnsFrom": [ + "google_calendar_sync_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 + }, + "tracking_facebook_pixel_id": { + "name": "tracking_facebook_pixel_id", + "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_tracking_facebook_pixel_id": { + "name": "version_tracking_facebook_pixel_id", + "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 + }, + "public.social_feed_settings_excluded_items": { + "name": "social_feed_settings_excluded_items", + "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_social_feed_settings_excluded_items_platform", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "item_id": { + "name": "item_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "social_feed_settings_excluded_items_order_idx": { + "name": "social_feed_settings_excluded_items_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "social_feed_settings_excluded_items_parent_id_idx": { + "name": "social_feed_settings_excluded_items_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "social_feed_settings_excluded_items_parent_id_fk": { + "name": "social_feed_settings_excluded_items_parent_id_fk", + "tableFrom": "social_feed_settings_excluded_items", + "tableTo": "social_feed_settings", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.social_feed_settings": { + "name": "social_feed_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "youtube_enabled": { + "name": "youtube_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "youtube_api_key": { + "name": "youtube_api_key", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "youtube_channel_id": { + "name": "youtube_channel_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "youtube_max_items": { + "name": "youtube_max_items", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "instagram_enabled": { + "name": "instagram_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "instagram_access_token": { + "name": "instagram_access_token", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "instagram_user_id": { + "name": "instagram_user_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "instagram_max_items": { + "name": "instagram_max_items", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "youtube_feed_snapshot": { + "name": "youtube_feed_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "instagram_feed_snapshot": { + "name": "instagram_feed_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "instagram_sync_status": { + "name": "instagram_sync_status", + "type": "jsonb", + "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", + "communicator", + "leader" + ] + }, + "public.enum_campaign_user_visibility": { + "name": "enum_campaign_user_visibility", + "schema": "public", + "values": [ + "carteira", + "tudo" + ] + }, + "public.enum_campaign_user_editing": { + "name": "enum_campaign_user_editing", + "schema": "public", + "values": [ + "carteira", + "tudo", + "somente_leitura" + ] + }, + "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", + "lembranca", + "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_polarity": { + "name": "enum_municipality_update_polarity", + "schema": "public", + "values": [ + "boa", + "neutra", + "ruim" + ] + }, + "public.enum_activity_status": { + "name": "enum_activity_status", + "schema": "public", + "values": [ + "confirmado", + "realizado", + "cancelado" + ] + }, + "public.enum_speech_topics": { + "name": "enum_speech_topics", + "schema": "public", + "values": [ + "saude", + "educacao", + "cultura", + "esporte", + "seguranca-publica", + "meio-ambiente", + "economia-trabalho", + "direitos-humanos", + "infraestrutura", + "ciencia-tecnologia", + "politica-instituicoes", + "agricultura", + "habitacao-cidades", + "comunicacao-midia", + "igualdade-racial", + "mulheres-genero", + "juventude", + "pessoa-deficiencia" + ] + }, + "public.enum_speech_scopes": { + "name": "enum_speech_scopes", + "schema": "public", + "values": [ + "bahia", + "brasil", + "internacional" + ] + }, + "public.enum_speech_legislature": { + "name": "enum_speech_legislature", + "schema": "public", + "values": [ + "54", + "55", + "56", + "57" + ] + }, + "public.enum_speech_classified_by": { + "name": "enum_speech_classified_by", + "schema": "public", + "values": [ + "gazetteer", + "llm", + "manual" + ] + }, + "public.enum_speech_cut_status": { + "name": "enum_speech_cut_status", + "schema": "public", + "values": [ + "processing", + "published", + "unpublished", + "failed" + ] + }, + "public.enum_speech_cut_step": { + "name": "enum_speech_cut_step", + "schema": "public", + "values": [ + "resolving", + "cutting", + "metadata", + "publishing" + ] + }, + "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_subscription_campaign_level": { + "name": "enum_subscription_campaign_level", + "schema": "public", + "values": [ + "time", + "esporadico" + ] + }, + "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" + ] + }, + "public.enum_social_feed_settings_excluded_items_platform": { + "name": "enum_social_feed_settings_excluded_items_platform", + "schema": "public", + "values": [ + "youtube", + "instagram" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "id": "62d8e448-c4a8-4acf-a423-f6b40f8f0d27", + "prevId": "00000000-0000-0000-0000-000000000000" +} \ No newline at end of file diff --git a/src/migrations/20260915_142058_activity_google_import_link.ts b/src/migrations/20260915_142058_activity_google_import_link.ts new file mode 100644 index 00000000..7a02417f --- /dev/null +++ b/src/migrations/20260915_142058_activity_google_import_link.ts @@ -0,0 +1,20 @@ +import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres' + +export async function up({ db, payload, req }: MigrateUpArgs): Promise { + await db.execute(sql` + ALTER TABLE "activity" ALTER COLUMN "municipality_id" DROP NOT NULL; + ALTER TABLE "activity" ADD COLUMN "google_event_id" varchar; + ALTER TABLE "activity" ADD COLUMN "google_calendar_id" varchar; + CREATE UNIQUE INDEX "activity_google_event_id_idx" ON "activity" USING btree ("google_event_id");`) +} + +export async function down({ db, payload, req }: MigrateDownArgs): Promise { + // NOT NULL first: down cannot succeed while imported (municipality-less) + // activities exist, and failing before dropping anything leaves the schema + // intact for a retry. `IF EXISTS` keeps the retry idempotent. + await db.execute(sql` + ALTER TABLE "activity" ALTER COLUMN "municipality_id" SET NOT NULL; + DROP INDEX IF EXISTS "activity_google_event_id_idx"; + ALTER TABLE "activity" DROP COLUMN IF EXISTS "google_event_id"; + ALTER TABLE "activity" DROP COLUMN IF EXISTS "google_calendar_id";`) +} diff --git a/src/migrations/index.ts b/src/migrations/index.ts index 9826b582..98dde943 100644 --- a/src/migrations/index.ts +++ b/src/migrations/index.ts @@ -71,6 +71,7 @@ import * as migration_20260913_001200_add_speech_segment_trgm_index from './2026 import * as migration_20260913_145820_add_speech_search_text from './20260913_145820_add_speech_search_text'; import * as migration_20260913_150000_backfill_speech_search_text_trgm_index from './20260913_150000_backfill_speech_search_text_trgm_index'; import * as migration_20260915_122356_add_speech_cut from './20260915_122356_add_speech_cut'; +import * as migration_20260915_142058_activity_google_import_link from './20260915_142058_activity_google_import_link'; export const migrations = [ { @@ -436,6 +437,11 @@ export const migrations = [ { up: migration_20260915_122356_add_speech_cut.up, down: migration_20260915_122356_add_speech_cut.down, - name: '20260915_122356_add_speech_cut' + name: '20260915_122356_add_speech_cut', + }, + { + up: migration_20260915_142058_activity_google_import_link.up, + down: migration_20260915_142058_activity_google_import_link.down, + name: '20260915_142058_activity_google_import_link' }, ]; diff --git a/src/payload-types.ts b/src/payload-types.ts index 39c92446..1aff060f 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -788,7 +788,7 @@ export interface Activity { publicEvent?: boolean | null; startAt?: string | null; endAt?: string | null; - municipality: number | Municipality; + municipality?: (number | null) | Municipality; locality?: string | null; organizations?: (number | Organization)[] | null; responsible?: @@ -834,6 +834,8 @@ export interface Activity { resultRecordedBy?: (number | null) | CampaignUser; resultRecordedAt?: string | null; lastMirroredChangeAt?: string | null; + googleEventId?: string | null; + googleCalendarId?: string | null; createdBy?: (number | null) | CampaignUser; updatedAt: string; createdAt: string; @@ -2112,6 +2114,8 @@ export interface ActivitySelect { resultRecordedBy?: T; resultRecordedAt?: T; lastMirroredChangeAt?: T; + googleEventId?: T; + googleCalendarId?: T; createdBy?: T; updatedAt?: T; createdAt?: T; diff --git a/src/utilities/access/activities.ts b/src/utilities/access/activities.ts index 3dc267fd..16384990 100644 --- a/src/utilities/access/activities.ts +++ b/src/utilities/access/activities.ts @@ -4,6 +4,7 @@ import type { Access, FieldAccess, PayloadRequest, Where } from 'payload' +import { relationshipId } from '@/lib/relationship' import type { CampaignUser } from '@/payload-types' import { getAccessibleMunicipalityIds } from '@/utilities/access/municipalities' import { @@ -27,15 +28,20 @@ const canStaffCreateActivity: FieldAccess = async ({ req }) => { * a per-município constraint on create), so the Edição axis narrows it for * advisors: a `somente_leitura` advisor creates nothing. The giro batch checks * the stops against the WRITE scope on top (`getWritableMunicipalityIds`). + * C165 — an activity without município is coordination/candidate-only (the + * `supporter` precedent), so an advisor's create requires one; the carteira + * membership of that município is still enforced by the action. */ -export const canCreateActivity: Access = async ({ req }) => { +export const canCreateActivity: Access = async ({ data, req }) => { if (isPayloadAdmin(req.user)) return true const currentUser = await getFreshCampaignUser(req) if (!currentUser || !isCampaignStaff(currentUser)) return false - if (currentUser.role === 'advisor') return advisorEditingAccess(currentUser) !== 'none' + if (isCampaignUnrestricted(currentUser)) return true + if (currentUser.role !== 'advisor') return false + if (advisorEditingAccess(currentUser) === 'none') return false - return true + return relationshipId(data?.municipality) !== null } export const canCampaignUserRescheduleActivity = ( @@ -49,16 +55,24 @@ const advisorActivityScopeWhere = async ( ): Promise => { const municipalityIDs = await getAccessibleMunicipalityIds(req, currentUser) return { - or: [ - // C90 — polymorphic `responsible`: the only scalar query the adapter - // supports on a multi-relation relationship is the object notation - // with `equals` (see @payloadcms/drizzle sanitizeQueryValue). + // C165 — the `and` closes the responsible branch for an activity WITHOUT + // município: it is coordination/candidate-only (precedente `supporter`), + // so an advisor listed as responsible must not see/update it. + and: [ + { municipality: { exists: true } }, { - responsible: { - equals: { relationTo: 'campaignUser', value: currentUser.id }, - }, + or: [ + // C90 — polymorphic `responsible`: the only scalar query the adapter + // supports on a multi-relation relationship is the object notation + // with `equals` (see @payloadcms/drizzle sanitizeQueryValue). + { + responsible: { + equals: { relationTo: 'campaignUser', value: currentUser.id }, + }, + }, + advisorMunicipalityScopeWhere('municipality', municipalityIDs), + ], }, - advisorMunicipalityScopeWhere('municipality', municipalityIDs), ], } } @@ -72,8 +86,9 @@ export const canReadActivity: Access = async ({ req }): Promise if (isCampaignUnrestricted(currentUser)) return true if (currentUser.role === 'advisor') { - // C141 — Visão "Tudo": the advisor sees the whole activity agenda. - if (currentUser.visibility === 'tudo') return true + // C141 — Visão "Tudo": the advisor sees the whole activity agenda, except + // C165 activities without município (coordination/candidate-only). + if (currentUser.visibility === 'tudo') return { municipality: { exists: true } } return advisorActivityScopeWhere(req, currentUser) } @@ -84,6 +99,7 @@ export const canReadActivity: Access = async ({ req }): Promise * C141 — the Edição axis rules activity updates. `tudo` widens to every * activity; `somente_leitura` closes updates entirely; the carteira branch * keeps the same row scope as read (responsible OR municipality). + * C165 — `tudo` never widens into activities without município. */ export const canUpdateActivity: Access = async ({ req }): Promise => { if (isPayloadAdmin(req.user)) return true @@ -96,7 +112,7 @@ export const canUpdateActivity: Access = async ({ req }): Promise { publicEvent: checkboxFormValue(formData, 'publicEvent'), startAt: parseScheduleFormField(formData, 'startAt', allDay), endAt: parseScheduleFormField(formData, 'endAt', allDay), - municipality: requiredRelationshipFormValue(formData, 'municipality'), + // C165 — empty input means "Sem município" (`null`), not a missing field: + // the overlay always renders the hidden input. + municipality: nullableRelationshipFormValue(formData, 'municipality'), locality: optionalFormText(formData, 'locality'), organizations: repeatedRelationshipFormValues(formData, 'organizations'), responsible: parseResponsiblesFormData(formData), diff --git a/src/utilities/activityOmnibox.ts b/src/utilities/activityOmnibox.ts index 0e7fcd86..2fb2b582 100644 --- a/src/utilities/activityOmnibox.ts +++ b/src/utilities/activityOmnibox.ts @@ -9,7 +9,7 @@ import { type CampaignListOmniboxSuggestion, } from '@/lib/campaignListOmnibox' import { isContactSearchQueryReady, normalizeContactSearchQuery } from '@/lib/contactSearchQuery' -import { activityStatusLabels } from '@/lib/schemas/activity' +import { ACTIVITY_UNSCOPED_LABEL, activityStatusLabels } from '@/lib/schemas/activity' import { activityTabLabels, activityTabs, @@ -38,7 +38,7 @@ const parseStateFromParams = ( const setExclusiveField = ( state: ActivityListState, - field: 'tag' | 'status' | 'municipality', + field: 'tag' | 'status' | 'municipality' | 'unscoped', value: string | undefined, ): ActivityListState => { const params = buildActivityListSearchParams(withPageReset(state)) @@ -48,10 +48,14 @@ const setExclusiveField = ( raw.tag = value } else if (field === 'status') { raw.status = value - } else { + } else if (field === 'municipality') { + // C165 — picking a município leaves the "Sem município" triage filter. raw.municipality = value + delete raw.unscoped + } else if (field === 'unscoped') { + raw.unscoped = value + delete raw.municipality } - return parseStateFromParams(raw) } @@ -104,6 +108,11 @@ export const buildActivityOmniboxChips = ({ }) } + if (state.unscoped) { + // C165 — the triage filter: activities without a município. + chips.push({ id: 'unscoped', label: ACTIVITY_UNSCOPED_LABEL }) + } + if (state.municipality) { chips.push({ id: `municipality:${state.municipality}`, @@ -183,6 +192,16 @@ export const buildActivityOmniboxSuggestionSeeds = ({ ) } + // C165 — the triage entry: imported Google activities awaiting a município. + seeds.push( + createOmniboxSuggestionSeed({ + id: 'unscoped', + group: 'Município', + label: ACTIVITY_UNSCOPED_LABEL, + keywords: ['municipio', 'sem', 'importado', 'google'], + }), + ) + return seeds } @@ -229,6 +248,13 @@ export const applyActivityOmniboxSuggestion = ({ return { kind: 'url', state: next } } + if (suggestionId === 'unscoped') { + const next = state.unscoped + ? setExclusiveField(state, 'unscoped', undefined) + : setExclusiveField(state, 'unscoped', '1') + return { kind: 'url', state: next } + } + if (suggestionId.startsWith('municipality:')) { const value = suggestionId.slice(13) const next = @@ -262,6 +288,10 @@ export const removeActivityOmniboxChip = ({ return { kind: 'url', state: setExclusiveField(state, 'status', undefined) } } + if (chipId === 'unscoped') { + return { kind: 'url', state: setExclusiveField(state, 'unscoped', undefined) } + } + if (chipId.startsWith('municipality:')) { return { kind: 'url', state: setExclusiveField(state, 'municipality', undefined) } } diff --git a/src/utilities/activityOverlayErrors.ts b/src/utilities/activityOverlayErrors.ts index e2fbbeae..1db6199e 100644 --- a/src/utilities/activityOverlayErrors.ts +++ b/src/utilities/activityOverlayErrors.ts @@ -3,6 +3,11 @@ import { ACTIVITY_DUPLICATE_TITLE_MESSAGE, ACTIVITY_OVERLAY_GENERIC_FAILURE_MESSAGE, } from '@/lib/activityOverlayMessages' +import { + ACTIVITY_DEMANDS_MUNICIPALITY_MESSAGE, + ACTIVITY_OUT_OF_SCOPE_MESSAGE, + ACTIVITY_UNSCOPED_ADVISOR_MESSAGE, +} from '@/lib/schemas/activity' import { mapCampaignFormActionError } from '@/utilities/campaignFormActionError' /** @@ -34,6 +39,11 @@ export const mapActivityOverlayError = (error: unknown): ActivityOverlayErrorRes } const mapped = mapCampaignFormActionError({ error, + safeMessages: [ + ACTIVITY_OUT_OF_SCOPE_MESSAGE, + ACTIVITY_UNSCOPED_ADVISOR_MESSAGE, + ACTIVITY_DEMANDS_MUNICIPALITY_MESSAGE, + ], genericMessage: ACTIVITY_OVERLAY_GENERIC_FAILURE_MESSAGE, }) return { diff --git a/src/utilities/activityRelationOptions.ts b/src/utilities/activityRelationOptions.ts index 75705486..402e0ee2 100644 --- a/src/utilities/activityRelationOptions.ts +++ b/src/utilities/activityRelationOptions.ts @@ -21,7 +21,9 @@ const isActivityTitleQueryReady = (query: string) => const toActivityRelationOption = (activity: { id: number title: string - municipality: unknown + // C165 — optional in the generated type (the município is nullable) and in + // partial `select`s; a municipality-less activity is simply not an option. + municipality?: unknown }): ActivityRelationOption | null => { const municipalityId = relationshipId(activity.municipality) if (!municipalityId) return null diff --git a/src/utilities/activityUi.ts b/src/utilities/activityUi.ts index 7b19d370..83d4cced 100644 --- a/src/utilities/activityUi.ts +++ b/src/utilities/activityUi.ts @@ -43,6 +43,8 @@ export type ActivityListState = { tag?: string status?: ActivityStatus municipality?: number + /** C165 — the triage filter: activities without a município (imported). */ + unscoped?: boolean } type RawSearchParams = CampaignListRawSearchParams @@ -386,7 +388,15 @@ export const activityDefaultCreatePrefill = ( ): { startAt: string; endAt: string } | null => activitySlotPrefill({ allDay: true, dateStr: formatBahiaCivilDate(now) }) -const activityListParamNames = ['q', 'tab', 'tag', 'status', 'municipality', 'page'] as const +const activityListParamNames = [ + 'q', + 'tab', + 'tag', + 'status', + 'municipality', + 'unscoped', + 'page', +] as const const activityListParamNameSet = new Set(activityListParamNames) @@ -411,7 +421,10 @@ export const parseActivityListParams = (params: RawSearchParams): ActivityListSt const tag = rawTag?.trim() ? rawTag.trim() : undefined const rawStatus = tab === 'todos' ? firstValue(params.status) : undefined const status = isActivityStatus(rawStatus) ? rawStatus : undefined - const municipality = strictDecimalInteger(firstValue(params.municipality)) + // C165 — `unscoped` and `municipality` are mutually exclusive; `unscoped` + // wins a hand-crafted URL with both (the canonical form keeps one). + const unscoped = firstValue(params.unscoped) === '1' + const municipality = unscoped ? undefined : strictDecimalInteger(firstValue(params.municipality)) return { page: rawPage ?? 1, @@ -419,6 +432,7 @@ export const parseActivityListParams = (params: RawSearchParams): ActivityListSt ...(q ? { q } : {}), ...(tag ? { tag } : {}), ...(status ? { status } : {}), + ...(unscoped ? { unscoped: true } : {}), ...(municipality ? { municipality } : {}), } } @@ -437,7 +451,11 @@ export const buildActivityListWhere = (state: ActivityListState, now: Date): Whe } if (state.tag) filters.push({ tags: { contains: state.tag } }) - if (state.municipality) filters.push({ municipality: { equals: state.municipality } }) + if (state.unscoped) { + filters.push({ municipality: { exists: false } }) + } else if (state.municipality) { + filters.push({ municipality: { equals: state.municipality } }) + } if (state.tab === 'proximos') { filters.push({ status: { equals: 'confirmado' } }) @@ -462,6 +480,7 @@ export const buildActivityListSearchParams = ( tag: state.tag, status: state.status, municipality: state.municipality === undefined ? undefined : String(state.municipality), + unscoped: state.unscoped ? '1' : undefined, }) const params = new URLSearchParams() @@ -469,6 +488,7 @@ export const buildActivityListSearchParams = ( if (canonicalState.tab !== 'proximos') params.set('tab', canonicalState.tab) if (canonicalState.tag) params.set('tag', canonicalState.tag) if (canonicalState.status) params.set('status', canonicalState.status) + if (canonicalState.unscoped) params.set('unscoped', '1') if (canonicalState.municipality) params.set('municipality', String(canonicalState.municipality)) if (canonicalState.page > 1) params.set('page', String(canonicalState.page)) diff --git a/src/utilities/activityViewModels.ts b/src/utilities/activityViewModels.ts index 00af183a..936b7def 100644 --- a/src/utilities/activityViewModels.ts +++ b/src/utilities/activityViewModels.ts @@ -126,6 +126,8 @@ export const activityAgendaSelect = { endAt: true, municipality: true, locality: true, + // C165 — the link is what makes an activity "Do Google". + googleEventId: true, } as const export const activityListSelect = { @@ -148,6 +150,8 @@ export type ActivityAgendaEvent = { municipality: ActivityMunicipalitySummary | null locality: string | null canReschedule: boolean + /** C165 — imported from the Google Calendar (link present). */ + importedFromGoogle: boolean } export const toActivityAgendaEvent = ( @@ -172,6 +176,7 @@ export const toActivityAgendaEvent = ( municipality: activityMunicipalitySummary(activity.municipality), locality: activity.locality ?? null, canReschedule: canCampaignUserRescheduleActivity(user, deputyPresent), + importedFromGoogle: Boolean(activity.googleEventId), } } @@ -190,6 +195,8 @@ export type ActivityListViewModel = { locationLabel: string responsibles: ActivityResponsibleDisplay[] taskProgress: { done: number; total: number } + /** C165 — imported from the Google Calendar (link present). */ + importedFromGoogle: boolean } const relationshipName = ( @@ -217,6 +224,7 @@ export const toActivityListViewModel = (activity: Activity): ActivityListViewMod done: activity.taskDoneCount ?? 0, total: activity.taskTotal ?? 0, }, + importedFromGoogle: Boolean(activity.googleEventId), } } diff --git a/src/utilities/calendarFeed.ts b/src/utilities/calendarFeed.ts index 13d86455..4fe8668f 100644 --- a/src/utilities/calendarFeed.ts +++ b/src/utilities/calendarFeed.ts @@ -158,8 +158,15 @@ export const buildActivityWindowRange = (): { rangeStart: string; rangeEnd: stri * clauses — shared by the iCal feed and the C114 Google mirror so both * surfaces keep the SAME horizon by definition. The feed adds its per-feed * filters (municipality/deputy/tag + advisor scope) on top. + * C165 — `includeCancelled` drops only the `status != cancelado` clause: the + * imported-events pass needs the cancelled activities to re-assert their + * cancellation on the same remote event. */ -export const buildActivityWindowWhereClauses = (rangeStart: string, rangeEnd: string): Where[] => [ +export const buildActivityWindowWhereClauses = ( + rangeStart: string, + rangeEnd: string, + options: { includeCancelled?: boolean } = {}, +): Where[] => [ { startAt: { less_than: rangeEnd } }, { or: [ @@ -169,7 +176,7 @@ export const buildActivityWindowWhereClauses = (rangeStart: string, rangeEnd: st }, ], }, - { status: { not_equals: 'cancelado' } }, + ...(options.includeCancelled ? [] : [{ status: { not_equals: 'cancelado' as const } }]), ] const buildFeedWhere = ( diff --git a/src/utilities/googleCalendarClient.ts b/src/utilities/googleCalendarClient.ts index 7234714e..3457d2ea 100644 --- a/src/utilities/googleCalendarClient.ts +++ b/src/utilities/googleCalendarClient.ts @@ -101,6 +101,17 @@ export type GoogleCalendarClient = { ) => Promise insertEvent: (calendarId: string, event: GoogleRemoteEvent) => Promise updateEvent: (calendarId: string, eventId: string, event: GoogleRemoteEvent) => Promise + /** + * C165 — partial update (`events.patch`) for events the Teqo did NOT create: + * only the fields the campaign owns go in the body, so the user's + * description, guests, reminders and Meet link survive an edit made here. + * `sendUpdates: 'none'` — the campaign's edit is not a broadcast. + */ + patchEvent: ( + calendarId: string, + eventId: string, + patch: Partial, + ) => Promise deleteEvent: (calendarId: string, eventId: string) => Promise /** C115 — push channel for the Google→Teqo direction (events.watch). */ watchEvents: ( @@ -388,6 +399,14 @@ export const createGoogleCalendarClient = ( }) } + const patchEvent: GoogleCalendarClient['patchEvent'] = async (calendarId, eventId, patch) => { + await apiFetch(`${eventsUrl(calendarId, eventId)}?sendUpdates=none`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(patch), + }) + } + const deleteEvent: GoogleCalendarClient['deleteEvent'] = async (calendarId, eventId) => { await apiFetch(eventsUrl(calendarId, eventId), { method: 'DELETE' }) } @@ -437,6 +456,7 @@ export const createGoogleCalendarClient = ( listEvents, insertEvent, updateEvent, + patchEvent, deleteEvent, watchEvents, stopChannel, diff --git a/src/utilities/googleCalendarSync.ts b/src/utilities/googleCalendarSync.ts index d60e6bfa..979e347d 100644 --- a/src/utilities/googleCalendarSync.ts +++ b/src/utilities/googleCalendarSync.ts @@ -8,19 +8,24 @@ import type { Payload, PayloadRequest, Where } from 'payload' import { activityMunicipalityIdOf, buildGoogleEventPayload, + buildImportedGoogleEventActivity, + buildImportedGoogleEventPatch, decodeGoogleEventActivityId, GOOGLE_EVENT_STATUS_CANCELLED, googleEventContentEquals, googleEventIdForActivity, + googleImportedEventContentEquals, + googleScheduleToActivityFields, googleStartEndInstantEquals, + googleTitleFromSummary, + isForeignGoogleEvent, + isImportableGoogleEvent, type GoogleRemoteEvent, } from '@/lib/googleCalendarEventMapping' import { buildGoogleReverseCancelBody, buildGoogleReverseUpdateBody, googleEditIsNewer, - googleScheduleToActivityFields, - googleTitleFromSummary, type GoogleReverseActivityEdit, } from '@/lib/googleCalendarReverseEdit' import { @@ -436,7 +441,14 @@ const deleteRemoteEvent = async ( } } -type SyncCounts = { created: number; updated: number; deleted: number; reverseEdits: number } +type SyncCounts = { + created: number + updated: number + deleted: number + reverseEdits: number + /** C165 — foreign events that became activities in this pass. */ + imported: number +} /** * C115 — the reconciliation's last-seen snapshot (JSONB on the sync row). @@ -541,6 +553,224 @@ const googleEditableContentEquals = ( googleStartEndInstantEquals(remote.start, payload.start) && googleStartEndInstantEquals(remote.end, payload.end) +type ImportedEventsCounts = Omit + +type ImportedEventsPassInput = { + payload: Payload + req: PayloadRequest | undefined + client: GoogleCalendarClient + calendarId: string + remoteEvents: GoogleRemoteEvent[] + lastSeenIds: Set + remoteEventIds: Set + window: { rangeStart: string; rangeEnd: string } + activityWhere: Where | undefined +} + +/** + * C165 — the import/link pass: events on the campaign calendar that the Teqo + * did NOT create (full id decode is null), start on/after the campaign's + * legal start and sit in the mirror window become activities of their own. + * + * The link (`googleEventId` + `googleCalendarId`, system-write) is what keeps + * the pass idempotent and what makes the forward direction patch the SAME + * remote event instead of inserting a second `teqo…` one. Foreign events are + * never deleted: a Teqo cancellation patches `status: cancelled`, a Teqo + * "reopen" patches it back. A previously-seen linked event that vanished from + * Google cancels its activity (same rule as the native mirror); a linked + * activity hard-deleted in the Teqo leaves the event alone (its id stays in + * the last-seen snapshot, so it is never re-imported). + * + * Runs inside `runSyncPass`, sharing its list window, last-seen snapshot and + * CAS write — no second listing, no second snapshot writer. + */ +const runImportedEventsPass = async ({ + payload, + req, + client, + calendarId, + remoteEvents, + lastSeenIds, + remoteEventIds, + window, + activityWhere, +}: ImportedEventsPassInput): Promise => { + const linked = await payload.find({ + collection: 'activity', + depth: 0, + limit: 0, + pagination: false, + where: { + and: [ + { googleEventId: { exists: true } }, + { googleCalendarId: { equals: calendarId } }, + ...buildActivityWindowWhereClauses(window.rangeStart, window.rangeEnd, { + includeCancelled: true, + }), + ...(activityWhere ? [activityWhere] : []), + ], + }, + // Intentional admin bypass: same espelho-cheio rationale as loadSyncActivities. + overrideAccess: true, + req, + }) + + const linkedByEventId = new Map() + for (const activity of linked.docs) { + if (activity.googleEventId) linkedByEventId.set(activity.googleEventId, activity) + } + + let imported = 0 + let updated = 0 + let deleted = 0 + let reverseEdits = 0 + + for (const event of remoteEvents) { + const eventId = event.id + if (!eventId || !isForeignGoogleEvent(event)) continue + + const activity = linkedByEventId.get(eventId) + + if (!activity) { + // Previously seen without a link: its imported activity was hard-deleted + // in the Teqo. The id STAYS in the snapshot — dropping it here would + // re-import the same event on the very next pass. + if (lastSeenIds.has(eventId)) { + remoteEventIds.add(eventId) + continue + } + if (!isImportableGoogleEvent(event, window)) continue + + const data = buildImportedGoogleEventActivity(event, calendarId) + if (!data) continue + + // Pre-check right before the insert: the unique `googleEventId` is the + // lock of last resort (create outside the caller's transaction keeps a + // failed insert from poisoning it), and this read narrows the race — + // the common concurrent case converges here, without a failed write. + const existingLink = await payload.find({ + collection: 'activity', + depth: 0, + limit: 1, + pagination: false, + where: { googleEventId: { equals: eventId } }, + // Intentional admin bypass: the link is system state. + overrideAccess: true, + req, + }) + const winner = existingLink.docs[0] + if (winner) { + linkedByEventId.set(eventId, winner) + remoteEventIds.add(eventId) + continue + } + + try { + const created = await payload.create({ + collection: 'activity', + data, + depth: 0, + // Intentional admin bypass: the Google editor acts through the + // calendar, and the link fields are system-write by design. + overrideAccess: true, + context: { mutationKind: 'googleCalendarSync' }, + // Deliberately OUTSIDE the caller's transaction (no `req`): the + // import is Google-sourced and independent from the save that + // triggered this pass. Sharing its transaction would let a unique + // violation in a concurrent import poison the user's save; here the + // recovery read below runs in its own session. + }) + linkedByEventId.set(eventId, created) + imported += 1 + } catch (error) { + // A concurrent pass may still have won between the pre-check and the + // insert: converge on the winner; any other failure is real and + // pauses the pass. + const existing = await payload.find({ + collection: 'activity', + depth: 0, + limit: 1, + pagination: false, + where: { googleEventId: { equals: eventId } }, + // Intentional admin bypass: same rationale as the pre-check above. + overrideAccess: true, + req, + }) + if (!existing.docs[0]) throw error + linkedByEventId.set(eventId, existing.docs[0]) + } + remoteEventIds.add(eventId) + continue + } + + // The linked event is ours to reconcile; it stays in the snapshot even if + // this pass only re-asserts it. + remoteEventIds.add(eventId) + + if (activity.status === 'cancelado') { + // Teqo cancelled it: the foreign event is never deleted, only trashed — + // the event continues to exist in the user's Google (and its history). + if (event.status !== GOOGLE_EVENT_STATUS_CANCELLED) { + await client.patchEvent(calendarId, eventId, { + status: GOOGLE_EVENT_STATUS_CANCELLED, + }) + deleted += 1 + } + continue + } + + if (event.status === GOOGLE_EVENT_STATUS_CANCELLED) { + // Same clock rule as the native mirror: a NEWER Google cancellation + // cancels the activity; an older one is re-asserted (reopen). + if (activity.status === 'confirmado') { + if (googleEditIsNewer(event.updated, activity.lastMirroredChangeAt ?? activity.updatedAt)) { + await cancelActivityFromGoogle(payload, req, activity) + reverseEdits += 1 + } else { + await client.patchEvent(calendarId, eventId, { status: 'confirmed' }) + updated += 1 + } + } + continue + } + + const importedPatch = buildImportedGoogleEventPatch(activity) + if (googleImportedEventContentEquals(event, importedPatch)) continue + + if ( + activity.status === 'confirmado' && + googleEditIsNewer(event.updated, activity.lastMirroredChangeAt ?? activity.updatedAt) + ) { + // No `[Município] ` prefix to strip here: imported titles are verbatim. + const reverseEdit = googleReverseEditOf(event, undefined) + if (reverseEdit) { + const body = buildGoogleReverseUpdateBody(activity, reverseEdit) + await applyGoogleReverseActivityPatch(payload, req, activity, reverseEdit, body) + reverseEdits += 1 + continue + } + } + + // Teqo newer/different: PATCH only the fields the campaign owns, on the + // SAME remote event (never `insertEvent`, never the deterministic id). + await client.patchEvent(calendarId, eventId, importedPatch) + updated += 1 + } + + for (const [eventId, activity] of linkedByEventId) { + if (remoteEventIds.has(eventId)) continue + // Seen before and now gone: the user permanently removed the event (trash + // still shows as `cancelled`, handled above). Never-seen ids are simply + // out of this pass's listing. + if (!lastSeenIds.has(eventId)) continue + if (activity.status !== 'confirmado') continue + await cancelActivityFromGoogle(payload, req, activity) + reverseEdits += 1 + } + + return { imported, updated, deleted, reverseEdits } +} + /** * C115 — one full pass, now BIDIRECTIONAL (D2): for every event of ours the * diff decides the direction by the clock rule (D3) — Google wins when its @@ -593,6 +823,7 @@ const runSyncPass = async ( let updated = 0 let deleted = 0 let reverseEdits = 0 + let imported = 0 // Ids PRESENT in this pass's remote list (the second loop's "exists" check) // — never removed mid-pass. Ids the engine DELETES are tracked separately so @@ -676,6 +907,24 @@ const runSyncPass = async ( updated += 1 } + // C165 — foreign events (id does not decode) are imported/linked here, + // sharing this pass's listing, last-seen snapshot and CAS write. + const importedCounts = await runImportedEventsPass({ + payload, + req, + client, + calendarId, + remoteEvents, + lastSeenIds, + remoteEventIds, + window: { rangeStart, rangeEnd }, + activityWhere, + }) + imported += importedCounts.imported + updated += importedCounts.updated + deleted += importedCounts.deleted + reverseEdits += importedCounts.reverseEdits + for (const [activityId, activity] of wantedById) { const eventId = googleEventIdForActivity(activityId) if (remoteEventIds.has(eventId)) continue @@ -689,6 +938,15 @@ const runSyncPass = async ( continue } + // C165 — imported activities are reconciled by their Google link above; + // a municipality-less activity waits for triage before entering the + // mirror (the original cut: untriaged events are not official agenda). + // The seen-and-gone rule above still applies to a municipality-less + // native activity whose mirrored event was removed in Google. + if (activity.googleEventId || activityMunicipalityIdOf(activity.municipality) === undefined) { + continue + } + const municipalityId = activityMunicipalityIdOf(activity.municipality) const municipalityName = municipalityId ? municipalityNames.get(municipalityId) : undefined await client.insertEvent(calendarId, buildGoogleEventPayload(activity, municipalityName)) @@ -704,12 +962,12 @@ const runSyncPass = async ( }) // Only log if there were actual changes or errors - if (created > 0 || updated > 0 || deleted > 0 || reverseEdits > 0) { + if (created > 0 || updated > 0 || deleted > 0 || reverseEdits > 0 || imported > 0) { payload.logger.info( - `[GoogleCalendarSync] Passada concluída com mudanças: created=${created}, updated=${updated}, deleted=${deleted}, reverseEdits=${reverseEdits}, calendarId=${calendarId}`, + `[GoogleCalendarSync] Passada concluída com mudanças: created=${created}, updated=${updated}, deleted=${deleted}, reverseEdits=${reverseEdits}, imported=${imported}, calendarId=${calendarId}`, ) } - return { created, updated, deleted, reverseEdits } + return { created, updated, deleted, reverseEdits, imported } } type SyncStatePatch = Partial< @@ -1183,6 +1441,7 @@ export const runCampaignCalendarSync = async ( updated: 0, deleted: 0, reverseEdits: 0, + imported: 0, at, } } @@ -1227,7 +1486,15 @@ export const runCampaignCalendarSync = async ( // derived connection state to `error`, which offers reconnect in one click. Object.assign(patch, oauthErrorPatchFor(error, at) ?? {}) await recordSyncState(payload, options.req, patch) - return { status: 'paused', created: 0, updated: 0, deleted: 0, reverseEdits: 0, at } + return { + status: 'paused', + created: 0, + updated: 0, + deleted: 0, + reverseEdits: 0, + imported: 0, + at, + } } } diff --git a/tests/e2e/campaignActivity.e2e.spec.ts b/tests/e2e/campaignActivity.e2e.spec.ts index 7e474421..e1a2f791 100644 --- a/tests/e2e/campaignActivity.e2e.spec.ts +++ b/tests/e2e/campaignActivity.e2e.spec.ts @@ -23,9 +23,13 @@ const openAgendaSlot = async ({ }: { campaign: { baseURL: string } page: import('@playwright/test').Page - municipalityId: number + /** C165 — omit to open the agenda unfiltered (the triage create). */ + municipalityId?: number }) => { - await page.goto(`${campaign.baseURL}/campanha/agenda?municipality=${municipalityId}`) + const agendaUrl = municipalityId + ? `${campaign.baseURL}/campanha/agenda?municipality=${municipalityId}` + : `${campaign.baseURL}/campanha/agenda` + await page.goto(agendaUrl) await expect(page.getByText('Carregando compromissos…')).toHaveCount(0, { timeout: 15_000 }) const slotLocator = page.locator('[data-time="14:00:00"]:visible').last() await expect(slotLocator).toBeVisible() @@ -85,7 +89,7 @@ test.describe('Atividades — registro-fundação', () => { expect(pairBoxes.endX).toBeGreaterThan(pairBoxes.startX) await page.getByLabel('Título *').fill(activityTitle) // The municipality filter prefills the field; the times come from the slot. - await expect(page.getByLabel('Município *')).toHaveValue(municipality.name) + await expect(page.getByLabel('Município (opcional)')).toHaveValue(municipality.name) // C90 — the unified polymorphic selector replaces the old Contact // responsável + assessores/liderança fields. @@ -276,7 +280,7 @@ test.describe('Agenda — calendário operacional', () => { ]) expect(startLabel).toMatch(/\d{2}\/\d{2}\/\d{4}/) expect(endLabel).toMatch(/\d{2}\/\d{2}\/\d{4}/) - await expect(page.getByLabel('Município *')).toHaveValue(municipality.name) + await expect(page.getByLabel('Município (opcional)')).toHaveValue(municipality.name) // C123 — the time selects are INLINE, visible without opening anything. await expect(page.getByLabel('Hora de Início')).toHaveValue('14') await expect(page.getByLabel('Minuto de Início')).toHaveValue('00') @@ -405,7 +409,7 @@ test.describe('Agenda — calendário operacional', () => { await expect(editModal).toBeVisible() await expect(page).toHaveURL(new RegExp('/campanha/agenda.*')) await expect(editModal.getByLabel('Título *')).toHaveValue(title, { timeout: 30_000 }) - await expect(editModal.getByLabel('Município *')).toHaveValue(municipality.name) + await expect(editModal.getByLabel('Município (opcional)')).toHaveValue(municipality.name) await expect(editModal.getByLabel('Hora de Início')).toHaveValue('14') await expect(editModal.getByLabel('Minuto de Término')).toHaveValue('30') // C123 — the edit overlay carries the id the old /editar page lost (C14 @@ -517,6 +521,36 @@ test.describe('Agenda — calendário operacional', () => { await expect(page.getByRole('button', { name: 'Hoje', exact: true })).toHaveCount(0) await expect(page.getByRole('grid').first()).toBeVisible() }) + + test('cria atividade sem município e a triagem mostra o badge "Sem município" (C165)', async ({ + campaign, + page, + }) => { + const { fixtures } = campaign + const coordinator = await fixtures.createCampaignUser('coordinator') + const title = fixtures.value('Compromisso sem município') + + await campaign.login(page, coordinator.email!, coordinator.password) + // Sem filtro de município: o overlay nasce no estado "Sem município". + await openAgendaSlot({ campaign, page }) + + const modal = page.getByRole('dialog', { name: 'Nova atividade' }) + await expect(modal).toBeVisible() + await page.getByLabel('Título *').fill(title) + await expect(page.getByLabel('Município (opcional)')).toHaveValue('Sem município') + await expect(page.getByText(/só coordenação e candidato veem/)).toBeVisible() + + await page.getByRole('button', { name: 'Salvar', exact: true }).click() + await expect(page.getByText(title, { exact: true })).toBeVisible({ timeout: 15_000 }) + + // A triagem: o filtro "Sem município" da omnibox lista o card com o badge. + await page.goto(`${campaign.baseURL}/campanha/atividades?tab=todos&unscoped=1`) + await expect(page.getByText(title, { exact: true })).toBeVisible({ timeout: 15_000 }) + await expect(page.getByText('Sem município', { exact: true }).first()).toBeVisible() + // Nativa (criada no Teqo), não importada: sem badge "Do Google". + await expect(page.getByText('Do Google', { exact: true })).toHaveCount(0) + await expect(page.getByText('Importado automaticamente')).toHaveCount(0) + }) }) test.describe('Atividades — agenda mobile (C103)', () => { @@ -547,7 +581,7 @@ test.describe('Atividades — agenda mobile (C103)', () => { // Label-less form: the placeholder gives context, the sr-only label keeps // the field addressable and accessible. await page.getByLabel('Título *').fill(title) - await expect(page.getByLabel('Município *')).toHaveValue(municipality.name) + await expect(page.getByLabel('Município (opcional)')).toHaveValue(municipality.name) // The footer is FIXED: "Salvar" is visible without scrolling on a small // viewport — the acceptance center of C103. diff --git a/tests/int/campaignActivity.int.spec.ts b/tests/int/campaignActivity.int.spec.ts index afed47c4..b07b7749 100644 --- a/tests/int/campaignActivity.int.spec.ts +++ b/tests/int/campaignActivity.int.spec.ts @@ -14,11 +14,15 @@ import { loadActivityAgendaEventsRecord, loadActivityEditDraftRecord, rescheduleActivityRecord, + updateActivityRecord, } from '@/app/(campaign)/campanha/actions/activity' import { allDayEndInstant, allDayStartInstant } from '@/lib/activityAllDay' import { relationshipId } from '@/lib/relationship' import { + ACTIVITY_DEMANDS_MUNICIPALITY_MESSAGE, ACTIVITY_DEPUTY_RESCHEDULE_FORBIDDEN_MESSAGE, + ACTIVITY_OUT_OF_SCOPE_MESSAGE, + ACTIVITY_UNSCOPED_ADVISOR_MESSAGE, activityCreateSchema, activityUpdateSchema, } from '@/lib/schemas/activity' @@ -96,13 +100,185 @@ describe('activity domain', () => { ).toBe(false) }) - it('requires a municipality on create', () => { - const result = activityCreateSchema.safeParse({ - title: campaignFixtures().value('Atividade sem município'), - tags: ['Caminhada'], - status: 'confirmado', + it('allows an activity without a municipality for coordination and blocks advisors', async () => { + const fixtures = campaignFixtures() + const coordinator = await fixtures.createCampaignUser('coordinator') + const advisor = await fixtures.createCampaignUser('advisor') + const municipality = await fixtures.getMunicipality() + await fixtures.assignMunicipalityAdvisors(municipality, [advisor]) + + // C165 — imported Google events are born without a município: the schema + // accepts it and the create access enforces the role split. + expect( + activityCreateSchema.safeParse({ + title: fixtures.value('Atividade sem município'), + tags: ['Caminhada'], + status: 'confirmado', + startAt: new Date(Date.now() + 86_400_000).toISOString(), + municipality: null, + }).success, + ).toBe(true) + + const coordinatorCreate = await canCreateActivity({ + req: stub({ user: coordinator, payload, context: {} }), + data: { title: 'Sem município' }, }) - expect(result.success).toBe(false) + expect(coordinatorCreate).toBe(true) + + const advisorCreateWithoutMunicipality = await canCreateActivity({ + req: stub({ user: advisor, payload, context: {} }), + data: { title: 'Sem município' }, + }) + expect(advisorCreateWithoutMunicipality).toBe(false) + + const advisorCreateWithMunicipality = await canCreateActivity({ + req: stub({ user: advisor, payload, context: {} }), + data: { title: 'Com município', municipality: municipality.id }, + }) + expect(advisorCreateWithMunicipality).toBe(true) + }) + + it('creates and clears a municipality-less activity for coordination only (C165)', async () => { + const fixtures = campaignFixtures() + const coordinator = await fixtures.createCampaignUser('coordinator') + const advisor = await fixtures.createCampaignUser('advisor') + const municipality = await fixtures.getMunicipality() + const outside = await fixtures.getMunicipality() + await fixtures.assignMunicipalityAdvisors(municipality, [advisor]) + + const activity = await createActivityRecord(payload, coordinator, { + title: fixtures.value('Atividade sem município'), + tags: [], + status: 'confirmado', + startAt: new Date(Date.now() + 86_400_000).toISOString(), + municipality: null, + }) + fixtures.own('activity', activity.id) + expect(relationshipId(activity.municipality)).toBeNull() + + // Demandas continuam exigindo município (a demanda é entidade municipal). + await expect( + createActivityRecord( + payload, + coordinator, + { + ...validActivityInput(municipality.id), + title: fixtures.value('Atividade sem município com demanda'), + municipality: null, + }, + [{ title: 'Panfletos', kind: 'material' }], + ), + ).rejects.toThrow(ACTIVITY_DEMANDS_MUNICIPALITY_MESSAGE) + + // O advisor não cria sem município (access + guarda da action)… + await expect( + createActivityRecord(payload, advisor, { + title: fixtures.value('Sem município do assessor'), + tags: [], + status: 'confirmado', + startAt: new Date(Date.now() + 86_400_000).toISOString(), + municipality: null, + }), + ).rejects.toThrow(ACTIVITY_UNSCOPED_ADVISOR_MESSAGE) + + // …nem aponta para um município fora da carteira. + await expect( + createActivityRecord(payload, advisor, { + ...validActivityInput(outside.id), + title: fixtures.value('Fora da carteira'), + }), + ).rejects.toThrow(ACTIVITY_OUT_OF_SCOPE_MESSAGE) + + // Updates: o coordenador limpa o município; o advisor não pode. + const cleared = await updateActivityRecord(payload, coordinator, { + id: activity.id, + startAt: activity.startAt, + municipality: null, + }) + expect(relationshipId(cleared.municipality)).toBeNull() + + const advisorActivity = await createActivityRecord(payload, advisor, { + ...validActivityInput(municipality.id), + title: fixtures.value('Com município do assessor'), + }) + fixtures.own('activity', advisorActivity.id) + + await expect( + updateActivityRecord(payload, advisor, { + id: advisorActivity.id, + startAt: advisorActivity.startAt, + municipality: null, + }), + ).rejects.toThrow(ACTIVITY_UNSCOPED_ADVISOR_MESSAGE) + + await expect( + updateActivityRecord(payload, advisor, { + id: advisorActivity.id, + startAt: advisorActivity.startAt, + municipality: outside.id, + }), + ).rejects.toThrow(ACTIVITY_OUT_OF_SCOPE_MESSAGE) + }) + + it('hides municipality-less activities from advisors even with Visão "Tudo" (C165)', async () => { + const fixtures = campaignFixtures() + const coordinator = await fixtures.createCampaignUser('coordinator') + const wideAdvisor = await fixtures.createCampaignUser('advisor', { visibility: 'tudo' }) + const responsibleAdvisor = await fixtures.createCampaignUser('advisor') + const municipality = await fixtures.getMunicipality() + await fixtures.assignMunicipalityAdvisors(municipality, [responsibleAdvisor]) + + const orphan = await createActivityRecord(payload, coordinator, { + title: fixtures.value('Importada sem município'), + tags: [], + status: 'confirmado', + startAt: new Date(Date.now() + 86_400_000).toISOString(), + municipality: null, + }) + fixtures.own('activity', orphan.id) + + // A coordenação lê; o advisor "tudo" e o advisor listado como responsável + // não — a atividade sem município é coordenação/candidato-only. + const coordinatorRead = await payload.find({ + collection: 'activity', + where: { id: { equals: orphan.id } }, + user: coordinator, + overrideAccess: false, + depth: 0, + }) + expect(coordinatorRead.totalDocs).toBe(1) + + const wideRead = await payload.find({ + collection: 'activity', + where: { id: { equals: orphan.id } }, + user: wideAdvisor, + overrideAccess: false, + depth: 0, + }) + expect(wideRead.totalDocs).toBe(0) + + await payload.update({ + collection: 'activity', + id: orphan.id, + data: { + responsible: [{ relationTo: 'campaignUser', value: responsibleAdvisor.id }], + }, + overrideAccess: true, + }) + const responsibleRead = await payload.find({ + collection: 'activity', + where: { id: { equals: orphan.id } }, + user: responsibleAdvisor, + overrideAccess: false, + depth: 0, + }) + expect(responsibleRead.totalDocs).toBe(0) + + expect( + await canReadActivity({ + req: stub({ user: wideAdvisor, payload, context: {} }), + }), + ).toEqual({ municipality: { exists: true } }) }) it('parses several demand drafts from the activity form', () => { @@ -237,6 +413,7 @@ describe('activity domain', () => { }) const createAsAdvisor = await canCreateActivity({ req: stub({ user: advisor, payload, context: {} }), + data: { municipality: municipality.id }, }) const createAsLeader = await canCreateActivity({ req: stub({ user: leaderAccount, payload, context: {} }), @@ -268,9 +445,14 @@ describe('activity domain', () => { req: stub({ user: advisor, payload, context: {} }), }) expect(advisorRead).toEqual({ - or: [ - { responsible: { equals: { relationTo: 'campaignUser', value: advisor.id } } }, - { municipality: { in: [municipality.id] } }, + and: [ + { municipality: { exists: true } }, + { + or: [ + { responsible: { equals: { relationTo: 'campaignUser', value: advisor.id } } }, + { municipality: { in: [municipality.id] } }, + ], + }, ], }) @@ -278,9 +460,14 @@ describe('activity domain', () => { req: stub({ user: otherAdvisor, payload, context: {} }), }) expect(otherAdvisorRead).toEqual({ - or: [ - { responsible: { equals: { relationTo: 'campaignUser', value: otherAdvisor.id } } }, - { municipality: { in: [] } }, + and: [ + { municipality: { exists: true } }, + { + or: [ + { responsible: { equals: { relationTo: 'campaignUser', value: otherAdvisor.id } } }, + { municipality: { in: [] } }, + ], + }, ], }) @@ -299,9 +486,14 @@ describe('activity domain', () => { req: stub({ user: advisor, payload, context: {} }), }) expect(advisorUpdate).toEqual({ - or: [ - { responsible: { equals: { relationTo: 'campaignUser', value: advisor.id } } }, - { municipality: { in: [municipality.id] } }, + and: [ + { municipality: { exists: true } }, + { + or: [ + { responsible: { equals: { relationTo: 'campaignUser', value: advisor.id } } }, + { municipality: { in: [municipality.id] } }, + ], + }, ], }) diff --git a/tests/int/googleCalendarPrimaryCalendarAction.int.spec.ts b/tests/int/googleCalendarPrimaryCalendarAction.int.spec.ts index c14083e6..88e1f34f 100644 --- a/tests/int/googleCalendarPrimaryCalendarAction.int.spec.ts +++ b/tests/int/googleCalendarPrimaryCalendarAction.int.spec.ts @@ -97,6 +97,7 @@ const createStubClient = (): StubClient => { listEvents, insertEvent: vi.fn(async () => {}), updateEvent: vi.fn(async () => {}), + patchEvent: vi.fn(async () => {}), deleteEvent: vi.fn(async () => {}), watchEvents: vi.fn(async () => ({ id: 'watch-1', diff --git a/tests/int/googleCalendarSync.int.spec.ts b/tests/int/googleCalendarSync.int.spec.ts index bbd0208c..cb513f91 100644 --- a/tests/int/googleCalendarSync.int.spec.ts +++ b/tests/int/googleCalendarSync.int.spec.ts @@ -71,6 +71,10 @@ const createStubClient = (store: GoogleRemoteEvent[] = []): GoogleCalendarClient const index = store.findIndex((entry) => entry.id === eventId) if (index >= 0) store[index] = event }, + patchEvent: async (_calendarId, eventId, patch) => { + const index = store.findIndex((entry) => entry.id === eventId) + if (index >= 0) store[index] = { ...store[index], ...patch } + }, deleteEvent: async (_calendarId, eventId) => { const index = store.findIndex((entry) => entry.id === eventId) if (index >= 0) store.splice(index, 1) @@ -177,6 +181,21 @@ describe('campaign Google calendar sync engine (C114)', () => { }), ) + /** UTC ISO → the `-03:00` dateTime shape the Calendar API echoes. */ + const formatBahiaDateTime = (iso: string): string => + `${new Date(new Date(iso).getTime() - 3 * 3_600_000).toISOString().slice(0, 19)}-03:00` + + /** Replaces one event's fields in the stub store (throws when absent). */ + const replaceEvent = ( + store: GoogleRemoteEvent[], + eventId: string, + patch: Partial, + ) => { + const index = store.findIndex((entry) => entry.id === eventId) + if (index < 0) throw new Error(`Evento ${eventId} não está no stub store.`) + store[index] = { ...store[index], ...patch } + } + it('creates the full mirror on the first pass (timed + all-day, municipality summary)', async () => { const municipality = await campaignFixtures().getMunicipality() const timed = await createActivity({ municipality: municipality.id, locality: 'Centro' }) @@ -477,19 +496,6 @@ describe('campaign Google calendar sync engine (C114)', () => { }) describe('bidirectional reconciliation (C115)', () => { - /** UTC ISO → the `-03:00` dateTime shape the Calendar API echoes. */ - const formatBahiaDateTime = (iso: string): string => - `${new Date(new Date(iso).getTime() - 3 * 3_600_000).toISOString().slice(0, 19)}-03:00` - - const replaceEvent = ( - store: GoogleRemoteEvent[], - eventId: string, - patch: Partial, - ) => { - const index = store.findIndex((entry) => entry.id === eventId) - store[index] = { ...store[index], ...patch } - } - const reloadActivity = async (id: number) => payload.findByID({ collection: 'activity', @@ -986,4 +992,388 @@ describe('campaign Google calendar sync engine (C114)', () => { expect((await reloadActivity(activity.id)).status).toBe('confirmado') }) }) + + describe('imported Google events (C165)', () => { + /** A foreign event inside the window and after the legal-start cut. */ + const foreignEvent = ( + overrides: Partial & { id: string }, + ): GoogleRemoteEvent => { + const start = new Date(Date.now() + 2 * 86_400_000) + return { + summary: `C165 ${crypto.randomUUID().slice(0, 8)}`, + description: 'descrição que vive no Google', + location: 'Gabinete', + start: { dateTime: formatBahiaDateTime(start.toISOString()) }, + end: { + dateTime: formatBahiaDateTime(new Date(start.getTime() + 3_600_000).toISOString()), + }, + ...overrides, + } + } + + /** + * The imported activity is born DURING the pass, so it enters the file's + * scope/cleanup only after this read — subsequent scoped passes then see + * it as a linked row. + */ + const trackImported = async (eventId: string) => { + const found = await payload.find({ + collection: 'activity', + where: { googleEventId: { equals: eventId } }, + depth: 0, + limit: 1, + pagination: false, + overrideAccess: true, + }) + const activity = found.docs[0] + if (activity) ownedActivityIds.add(activity.id) + return activity + } + + it('imports an eligible foreign event once — confirmado, sem município, título verbatim', async () => { + await createConfig(calendarA) + const event = foreignEvent({ id: `foreign-${crypto.randomUUID()}` }) + const store: GoogleRemoteEvent[] = [event] + const client = createStubClient(store) + + const outcome = await runSync(client) + expect(outcome.status).toBe('synced') + expect(outcome.imported).toBe(1) + expect(outcome.created).toBe(0) + + const activity = await trackImported(event.id!) + expect(activity).toBeTruthy() + expect(activity?.status).toBe('confirmado') + expect(activity?.title).toBe(event.summary) + expect(activity?.municipality).toBeNull() + expect(activity?.locality).toBe('Gabinete') + expect(activity?.googleCalendarId).toBe(calendarA) + // Description deliberately ignored: the text lives in Google. + expect(activity?.description ?? null).toBeNull() + // The mirror never materializes an imported activity as a `teqo…` event. + expect(store.filter((entry) => entry.id?.startsWith('teqo'))).toHaveLength(0) + + // Idempotent: a second pass converges and never re-imports. + const again = await runSync(client) + expect(again).toMatchObject({ imported: 0, created: 0, updated: 0, reverseEdits: 0 }) + expect(store).toHaveLength(1) + }) + + it('leaves pre-cut events out — no backfill — and imports the cut day', async () => { + await createConfig(calendarA) + const beforeCut = foreignEvent({ + id: `foreign-${crypto.randomUUID()}`, + // 2026-08-15 22:00 in Bahia — the civil date before the legal start. + start: { dateTime: '2026-08-15T22:00:00-03:00' }, + end: { dateTime: '2026-08-15T23:00:00-03:00' }, + }) + const onCut = foreignEvent({ + id: `foreign-${crypto.randomUUID()}`, + start: { dateTime: '2026-08-16T09:00:00-03:00' }, + end: { dateTime: '2026-08-16T10:00:00-03:00' }, + }) + const store: GoogleRemoteEvent[] = [beforeCut, onCut] + + const outcome = await runSync(createStubClient(store)) + expect(outcome.imported).toBe(1) + expect(await trackImported(beforeCut.id!)).toBeUndefined() + expect(await trackImported(onCut.id!)).toBeTruthy() + // The pre-cut event stays untouched in Google. + expect(store).toHaveLength(2) + }) + + it('skips events without a usable title without pausing the pass', async () => { + await createConfig(calendarA) + const emptyTitle = foreignEvent({ id: `foreign-${crypto.randomUUID()}`, summary: '' }) + const oneChar = foreignEvent({ id: `foreign-${crypto.randomUUID()}`, summary: 'A' }) + const valid = foreignEvent({ id: `foreign-${crypto.randomUUID()}` }) + const store: GoogleRemoteEvent[] = [emptyTitle, oneChar, valid] + + const outcome = await runSync(createStubClient(store)) + expect(outcome.status).toBe('synced') + expect(outcome.imported).toBe(1) + expect(await trackImported(valid.id!)).toBeTruthy() + expect(store).toHaveLength(3) + }) + + it('applies a newer Google edit back with an audit record, then converges', async () => { + await createConfig(calendarA) + const event = foreignEvent({ id: `foreign-${crypto.randomUUID()}` }) + const store: GoogleRemoteEvent[] = [event] + const client = createStubClient(store) + await runSync(client) + const activity = await trackImported(event.id!) + expect(activity).toBeTruthy() + + const renamed = `C165 ${crypto.randomUUID().slice(0, 8)} (renomeada no Google)` + // Whole seconds: the Calendar API echoes second precision, and the + // helper below formats the same way. + const newStart = new Date(Math.floor((Date.now() + 3 * 86_400_000) / 1000) * 1000) + replaceEvent(store, event.id!, { + summary: renamed, + start: { dateTime: formatBahiaDateTime(newStart.toISOString()) }, + end: { + dateTime: formatBahiaDateTime(new Date(newStart.getTime() + 3_600_000).toISOString()), + }, + updated: new Date(Date.now() + 60_000).toISOString(), + }) + + const outcome = await runSync(client) + expect(outcome.reverseEdits).toBe(1) + expect(outcome.updated).toBe(0) + + const reloaded = await payload.findByID({ + collection: 'activity', + id: activity!.id, + depth: 0, + overrideAccess: true, + }) + expect(reloaded.title).toBe(renamed) + expect(reloaded.slug).toBe(activity!.slug) + expect(new Date(reloaded.startAt!).getTime()).toBe(newStart.getTime()) + expect((reloaded.updates ?? []).at(-1)?.body).toContain('Google Calendar:') + + const again = await runSync(client) + expect(again).toMatchObject({ imported: 0, updated: 0, reverseEdits: 0 }) + expect(store).toHaveLength(1) + }) + + it('edits the Teqo into the SAME remote event — never a second `teqo…`, description preserved', async () => { + await createConfig(calendarA) + const event = foreignEvent({ id: `foreign-${crypto.randomUUID()}` }) + const store: GoogleRemoteEvent[] = [event] + const client = createStubClient(store) + await runSync(client) + const activity = await trackImported(event.id!) + expect(activity).toBeTruthy() + + await payload.update({ + collection: 'activity', + id: activity!.id, + data: { locality: 'Centro (atualizado no Teqo)' }, + depth: 0, + overrideAccess: true, + }) + + const outcome = await runSync(client) + expect(outcome.updated).toBe(1) + expect(outcome.imported).toBe(0) + expect(store).toHaveLength(1) + expect(store[0].id).toBe(event.id) + expect(store[0].location).toBe('Centro (atualizado no Teqo)') + // The partial PATCH never touches the user's description. + expect(store[0].description).toBe('descrição que vive no Google') + expect(store.filter((entry) => entry.id?.startsWith('teqo'))).toHaveLength(0) + }) + + it('assigning a município keeps the verbatim title (no `[Município] ` prefix)', async () => { + await createConfig(calendarA) + const event = foreignEvent({ id: `foreign-${crypto.randomUUID()}` }) + const store: GoogleRemoteEvent[] = [event] + const client = createStubClient(store) + await runSync(client) + const activity = await trackImported(event.id!) + const municipality = await campaignFixtures().getMunicipality() + + await payload.update({ + collection: 'activity', + id: activity!.id, + data: { municipality: municipality.id }, + depth: 0, + overrideAccess: true, + }) + + const outcome = await runSync(client) + expect(outcome.updated).toBe(0) + expect(store[0].summary).toBe(event.summary) + expect(store[0].summary).not.toContain('[') + }) + + it('cancelling in the Teqo trashes the SAME remote event — never deletes it', async () => { + await createConfig(calendarA) + const event = foreignEvent({ id: `foreign-${crypto.randomUUID()}` }) + const store: GoogleRemoteEvent[] = [event] + const client = createStubClient(store) + await runSync(client) + const activity = await trackImported(event.id!) + + await payload.update({ + collection: 'activity', + id: activity!.id, + data: { status: 'cancelado' }, + depth: 0, + overrideAccess: true, + }) + + const outcome = await runSync(client) + expect(outcome.deleted).toBe(1) + expect(store).toHaveLength(1) + expect(store[0].status).toBe('cancelled') + + // Converged: no repeated patch on the next pass. + const again = await runSync(client) + expect(again).toMatchObject({ imported: 0, updated: 0, deleted: 0, reverseEdits: 0 }) + }) + + it('a newer Google cancellation cancels the activity; a staff reopen re-asserts the event', async () => { + await createConfig(calendarA) + const event = foreignEvent({ id: `foreign-${crypto.randomUUID()}` }) + const store: GoogleRemoteEvent[] = [event] + const client = createStubClient(store) + await runSync(client) + const activity = await trackImported(event.id!) + + replaceEvent(store, event.id!, { + status: 'cancelled', + // +5s: newer than the import stamp (the cancel wins), yet older than + // the reopen's stamp after the 3.5s sleep below (the reopen wins). + updated: new Date(Date.now() + 5_000).toISOString(), + }) + let outcome = await runSync(client) + expect(outcome.reverseEdits).toBe(1) + let reloaded = await payload.findByID({ + collection: 'activity', + id: activity!.id, + depth: 0, + overrideAccess: true, + }) + expect(reloaded.status).toBe('cancelado') + expect((reloaded.updates ?? []).at(-1)?.body).toContain('Google Calendar: cancelada') + + // Staff reopens after the cancel: the clock rule says Teqo wins — the + // SAME event is patched back to confirmed (never deleted/recreated). + await new Promise((resolve) => setTimeout(resolve, 3_500)) + await payload.update({ + collection: 'activity', + id: activity!.id, + data: { status: 'confirmado' }, + depth: 0, + overrideAccess: true, + }) + outcome = await runSync(client) + expect(outcome.updated).toBe(1) + expect(outcome.reverseEdits).toBe(0) + expect(store).toHaveLength(1) + expect(store[0].status).toBe('confirmed') + reloaded = await payload.findByID({ + collection: 'activity', + id: activity!.id, + depth: 0, + overrideAccess: true, + }) + expect(reloaded.status).toBe('confirmado') + }) + + it('a permanently removed remote event cancels the linked activity (snapshot rule)', async () => { + await createConfig(calendarA) + const event = foreignEvent({ id: `foreign-${crypto.randomUUID()}` }) + const store: GoogleRemoteEvent[] = [event] + const client = createStubClient(store) + await runSync(client) + const activity = await trackImported(event.id!) + + store.splice(0, 1) + + const outcome = await runSync(client) + expect(outcome.reverseEdits).toBe(1) + const reloaded = await payload.findByID({ + collection: 'activity', + id: activity!.id, + depth: 0, + overrideAccess: true, + }) + expect(reloaded.status).toBe('cancelado') + }) + + it('a hard-deleted imported activity leaves its foreign event alone — never resurrected', async () => { + await createConfig(calendarA) + const event = foreignEvent({ id: `foreign-${crypto.randomUUID()}` }) + const store: GoogleRemoteEvent[] = [event] + const client = createStubClient(store) + await runSync(client) + const activity = await trackImported(event.id!) + expect(activity).toBeTruthy() + + await payload.delete({ collection: 'activity', id: activity!.id, overrideAccess: true }) + ownedActivityIds.delete(activity!.id) + + const outcome = await runSync(client) + expect(outcome.imported).toBe(0) + expect(store).toHaveLength(1) + expect(await trackImported(event.id!)).toBeUndefined() + + // The snapshot kept the dismissed id: the FIRST pass after the delete + // must not have dropped it, or this second pass would re-import. + const again = await runSync(client) + expect(again.imported).toBe(0) + expect(await trackImported(event.id!)).toBeUndefined() + }) + + it('a municipality-less native activity whose mirrored event vanished is cancelled (seen-and-gone)', async () => { + const municipality = await campaignFixtures().getMunicipality() + const activity = await createActivity({ municipality: municipality.id }) + await createConfig(calendarA) + const store: GoogleRemoteEvent[] = [] + const client = createStubClient(store) + await runSync(client) + expect(store).toHaveLength(1) + + // Triage clears the município; the user then permanently removes the + // event in Google. The seen-and-gone rule must still cancel it (the C165 + // mirror guard only skips the CREATE of municipality-less activities). + await payload.update({ + collection: 'activity', + id: activity.id, + data: { municipality: null }, + depth: 0, + overrideAccess: true, + }) + store.splice(0, 1) + + const outcome = await runSync(client) + expect(outcome.reverseEdits).toBe(1) + const reloaded = await payload.findByID({ + collection: 'activity', + id: activity.id, + depth: 0, + overrideAccess: true, + }) + expect(reloaded.status).toBe('cancelado') + }) + + it('never touches a different foreign event and never imports a mirrored `teqo…` event', async () => { + const activity = await createActivity() + await createConfig(calendarA) + const untouched: GoogleRemoteEvent = { id: 'foreign-sem-data', summary: 'Sem data' } + const store: GoogleRemoteEvent[] = [untouched] + const client = createStubClient(store) + await runSync(client) + expect(store).toHaveLength(2) + + // A manual `teqo…`-looking event is NOT a mirror id and is out of the + // import criteria: it is never imported (the mirror's stray-event + // cleanup owns the native namespace, not the import pass). + const manual: GoogleRemoteEvent = { + id: 'teqo999', + summary: 'Evento manual com cara de espelho', + start: { dateTime: '2026-09-20T09:00:00-03:00' }, + end: { dateTime: '2026-09-20T10:00:00-03:00' }, + } + store.push(manual) + + const outcome = await runSync(client) + expect(outcome.imported).toBe(0) + expect(store.find((entry) => entry.id === 'foreign-sem-data')).toBe(untouched) + const importedManual = await payload.find({ + collection: 'activity', + where: { googleEventId: { equals: 'teqo999' } }, + depth: 0, + limit: 1, + pagination: false, + overrideAccess: true, + }) + expect(importedManual.docs).toHaveLength(0) + expect(store.find((entry) => entry.id === googleEventIdForActivity(activity.id))).toBeTruthy() + }) + }) }) diff --git a/tests/unit/activityAgenda.unit.spec.ts b/tests/unit/activityAgenda.unit.spec.ts index 3dfdd801..fa07cb9b 100644 --- a/tests/unit/activityAgenda.unit.spec.ts +++ b/tests/unit/activityAgenda.unit.spec.ts @@ -167,6 +167,7 @@ describe('activity agenda event view model', () => { municipality: { id: 12, name: 'Ilhéus', slug: 'ilheus' }, locality: 'Centro histórico', canReschedule: true, + importedFromGoogle: false, }) }) diff --git a/tests/unit/activityAgendaSwipePreview.unit.spec.tsx b/tests/unit/activityAgendaSwipePreview.unit.spec.tsx index 7b7c78f4..91b232ae 100644 --- a/tests/unit/activityAgendaSwipePreview.unit.spec.tsx +++ b/tests/unit/activityAgendaSwipePreview.unit.spec.tsx @@ -21,6 +21,7 @@ const event = (overrides: Partial = {}): ActivityAgendaEven municipality: null, locality: null, canReschedule: true, + importedFromGoogle: false, ...overrides, }) diff --git a/tests/unit/activityOverlay.unit.spec.tsx b/tests/unit/activityOverlay.unit.spec.tsx index 82e001ec..ff533d88 100644 --- a/tests/unit/activityOverlay.unit.spec.tsx +++ b/tests/unit/activityOverlay.unit.spec.tsx @@ -156,7 +156,9 @@ describe('ActivityOverlay — criação (modal central)', () => { expect( (screen.getByRole('combobox', { name: 'Minuto de Término' }) as HTMLSelectElement).value, ).toBe('30') - expect((screen.getByLabelText('Município *') as HTMLInputElement).value).toBe('Camaçari') + expect((screen.getByLabelText('Município (opcional)') as HTMLInputElement).value).toBe( + 'Camaçari', + ) // C123 — "Local" aparece uma única vez no overlay. expect(screen.getAllByLabelText('Local (opcional)')).toHaveLength(1) @@ -242,7 +244,7 @@ describe('ActivityOverlay — criação (modal central)', () => { expect(onSaved).toHaveBeenCalledTimes(1) }) - it('bloqueia salvar sem município e não invoca a ação', async () => { + it('salva sem município: "Sem município" é estado válido e envia o campo vazio (C165)', async () => { const onSaved = vi.fn() mocks.createOverlay.mockResolvedValue({ ok: true }) renderOverlay({ agendaState: {}, onSaved }) @@ -250,11 +252,17 @@ describe('ActivityOverlay — criação (modal central)', () => { fireEvent.change(screen.getByLabelText('Título *'), { target: { value: 'Café com apoiadores' }, }) + // O aviso de visibilidade acompanha o estado sem município. + expect((screen.getByLabelText('Município (opcional)') as HTMLInputElement).value).toBe( + 'Sem município', + ) + expect(screen.getByText(/só coordenação e candidato veem/)).toBeTruthy() + fireEvent.click(screen.getByRole('button', { name: 'Salvar' })) - await waitFor(() => expect(screen.getByText('Informe o município.')).toBeTruthy()) - expect(mocks.createOverlay).not.toHaveBeenCalled() - expect(onSaved).not.toHaveBeenCalled() + await waitFor(() => expect(mocks.createOverlay).toHaveBeenCalledTimes(1)) + expect(submittedFormData().get('municipality')).toBe('') + expect(onSaved).toHaveBeenCalledTimes(1) }) it('mantém o overlay aberto com erro inline quando a criação falha', async () => { @@ -362,7 +370,9 @@ describe('ActivityOverlay — edição (modal central)', () => { expect( (screen.getByRole('combobox', { name: 'Hora de Término' }) as HTMLSelectElement).value, ).toBe('14') - expect((screen.getByLabelText('Município *') as HTMLInputElement).value).toBe('Camaçari') + expect((screen.getByLabelText('Município (opcional)') as HTMLInputElement).value).toBe( + 'Camaçari', + ) expect((screen.getByLabelText('Local (opcional)') as HTMLInputElement).value).toBe( 'Centro histórico', ) @@ -445,8 +455,8 @@ describe('ActivityOverlay — sheet mobile (C103)', () => { expect( (screen.getByRole('combobox', { name: 'Minuto de Início' }) as HTMLSelectElement).value, ).toBe('00') - expect((screen.getByLabelText('Município *') as HTMLInputElement).placeholder).toBe( - 'Município *', + expect((screen.getByLabelText('Município (opcional)') as HTMLInputElement).placeholder).toBe( + 'Município (opcional)', ) expect((screen.getByLabelText('Local (opcional)') as HTMLInputElement).placeholder).toBe( 'Local (opcional)', diff --git a/tests/unit/activityUi.unit.spec.ts b/tests/unit/activityUi.unit.spec.ts index a77bf151..8a771314 100644 --- a/tests/unit/activityUi.unit.spec.ts +++ b/tests/unit/activityUi.unit.spec.ts @@ -318,3 +318,37 @@ describe('activity slot prefill (C91)', () => { }) }) }) + +describe('activity triage filter — Sem município (C165)', () => { + it('parses and serializes `unscoped` exclusively with `municipality`', () => { + expect(parseActivityListParams({ unscoped: '1' })).toMatchObject({ unscoped: true }) + // A hand-crafted URL with both: the triage filter wins and the município + // drops (the canonical form keeps only one). + const both = parseActivityListParams({ unscoped: '1', municipality: '12' }) + expect(both.unscoped).toBe(true) + expect(both.municipality).toBeUndefined() + + const params = buildActivityListSearchParams(both) + expect(params.get('unscoped')).toBe('1') + expect(params.get('municipality')).toBeNull() + + // O caminho inverso: município presente nunca serializa `unscoped`. + const scoped = buildActivityListSearchParams(parseActivityListParams({ municipality: '12' })) + expect(scoped.get('municipality')).toBe('12') + expect(scoped.get('unscoped')).toBeNull() + }) + + it('builds `municipality exists: false` on any tab', () => { + const where = buildActivityListWhere( + parseActivityListParams({ tab: 'todos', unscoped: '1' }), + NOW, + ) + expect(where).toEqual({ and: [{ municipality: { exists: false } }] }) + + const scoped = buildActivityListWhere( + parseActivityListParams({ tab: 'todos', municipality: '12' }), + NOW, + ) + expect(scoped).toEqual({ and: [{ municipality: { equals: 12 } }] }) + }) +}) diff --git a/tests/unit/googleCalendarClient.unit.spec.ts b/tests/unit/googleCalendarClient.unit.spec.ts index ead3fa0a..7f324a01 100644 --- a/tests/unit/googleCalendarClient.unit.spec.ts +++ b/tests/unit/googleCalendarClient.unit.spec.ts @@ -153,6 +153,16 @@ describe('createGoogleCalendarClient', () => { headers: { 'Content-Type': 'application/json' }, }) } + if (url.includes('/events') && method === 'PATCH') { + const patch = JSON.parse(body ?? '{}') as Record + const eventId = new URL(url).pathname.split('/').pop() + const index = events.findIndex((e) => e.id === eventId) + if (index >= 0) events[index] = { ...events[index], ...patch } + return new Response(JSON.stringify(events[index] ?? patch), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }) + } if (url.includes('/events') && method === 'DELETE') { const eventId = url.split('/').pop() const index = events.findIndex((e) => e.id === eventId) @@ -377,6 +387,24 @@ describe('createGoogleCalendarClient', () => { expect(tokenRequests).toBe(2) }) + it('patches partial fields with sendUpdates=none, preserving what was not sent (C165)', async () => { + const { fetchImpl, calls, events } = stubTransport([ + { id: 'foreign-1', summary: 'Antigo', description: 'descrição do usuário' }, + ]) + const client = createGoogleCalendarClient(serviceAccountAuth, fetchImpl) + + await client.patchEvent(calendarId, 'foreign-1', { summary: 'Novo', location: '' }) + + const patchCall = calls.find((call) => call.method === 'PATCH') + expect(patchCall?.url).toContain( + `/calendars/${encodeURIComponent(calendarId)}/events/foreign-1`, + ) + expect(patchCall?.url).toContain('sendUpdates=none') + expect(JSON.parse(patchCall?.body ?? '{}')).toEqual({ summary: 'Novo', location: '' }) + // O PATCH preserva o que não foi enviado — a descrição do usuário fica. + expect(events[0]).toMatchObject({ summary: 'Novo', description: 'descrição do usuário' }) + }) + it('normalizes the watch expiration string into millis (C164)', async () => { const { fetchImpl, calls } = stubTransport() const client = createGoogleCalendarClient(serviceAccountAuth, fetchImpl) diff --git a/tests/unit/googleCalendarEventMapping.unit.spec.ts b/tests/unit/googleCalendarEventMapping.unit.spec.ts index d633f7fa..90db3d7b 100644 --- a/tests/unit/googleCalendarEventMapping.unit.spec.ts +++ b/tests/unit/googleCalendarEventMapping.unit.spec.ts @@ -3,9 +3,14 @@ import { describe, expect, it } from 'vitest' import { buildGoogleCalendarDescription, buildGoogleEventPayload, + buildImportedGoogleEventActivity, + buildImportedGoogleEventPatch, decodeGoogleEventActivityId, googleEventContentEquals, googleEventIdForActivity, + googleImportedEventContentEquals, + isForeignGoogleEvent, + isImportableGoogleEvent, type GoogleRemoteEvent, } from '@/lib/googleCalendarEventMapping' @@ -181,3 +186,244 @@ describe('googleEventContentEquals', () => { ).toBe(true) }) }) + +describe('isForeignGoogleEvent (C165)', () => { + it('accepts only ids the mirror does not own (full decode, never a prefix)', () => { + expect(isForeignGoogleEvent({ id: 'abc123' })).toBe(true) + // `teqo` (the bare prefix) does not decode to an activity — foreign. + expect(decodeGoogleEventActivityId('teqo')).toBeNull() + expect(isForeignGoogleEvent({ id: 'teqo' })).toBe(true) + expect(isForeignGoogleEvent({ id: 'teqo3' })).toBe(false) + // A manual `teqo3` decodes to activity 3: it stays in the mirror + // namespace and is never imported. + expect(decodeGoogleEventActivityId('teqo3')).toBe(3) + expect(isForeignGoogleEvent({})).toBe(false) + }) +}) + +describe('isImportableGoogleEvent (C165 cut + window)', () => { + const window = { rangeStart: '2026-07-01T00:00:00.000Z', rangeEnd: '2027-09-01T00:00:00.000Z' } + const timed = (startAt: string): GoogleRemoteEvent => ({ + id: 'foreign-1', + summary: 'Reunião', + start: { dateTime: startAt }, + end: { dateTime: new Date(Date.parse(startAt) + 3_600_000).toISOString() }, + }) + + it('applies the cut on the Bahia civil date: 15/08 out, 16/08 in', () => { + expect(isImportableGoogleEvent(timed('2026-08-16T02:59:00.000Z'), window)).toBe(false) + expect(isImportableGoogleEvent(timed('2026-08-16T03:00:00.000Z'), window)).toBe(true) + expect( + isImportableGoogleEvent( + { + id: 'foreign-2', + summary: 'Aniversário', + start: { date: '2026-08-15' }, + end: { date: '2026-08-16' }, + }, + window, + ), + ).toBe(false) + expect( + isImportableGoogleEvent( + { + id: 'foreign-3', + summary: 'Caminhada', + start: { date: '2026-08-16' }, + end: { date: '2026-08-17' }, + }, + window, + ), + ).toBe(true) + }) + + it('respects the mirror window on both ends', () => { + expect(isImportableGoogleEvent(timed('2027-09-02T12:00:00.000Z'), window)).toBe(false) + expect( + isImportableGoogleEvent(timed('2026-06-30T12:00:00.000Z'), { + rangeStart: '2026-08-16T03:00:00.000Z', + rangeEnd: '2027-09-01T00:00:00.000Z', + }), + ).toBe(false) + }) + + it('rejects cancelled, mirrored and malformed events', () => { + expect( + isImportableGoogleEvent( + { ...timed('2026-08-20T12:00:00.000Z'), status: 'cancelled' }, + window, + ), + ).toBe(false) + expect(isImportableGoogleEvent({ id: 'foreign-4', summary: 'Sem data' }, window)).toBe(false) + expect( + isImportableGoogleEvent( + { id: 'foreign-5', summary: 'Sem fim', start: { dateTime: '2026-08-20T12:00:00.000Z' } }, + window, + ), + ).toBe(false) + // A `teqo…` id belongs to the mirror namespace, never to the import pass. + expect( + isImportableGoogleEvent({ ...timed('2026-08-20T12:00:00.000Z'), id: 'teqo3' }, window), + ).toBe(false) + }) +}) + +describe('buildImportedGoogleEventActivity (C165)', () => { + const calendarId = 'c_imprensa@group.calendar.google.com' + + it('shapes a timed event: title verbatim, location as locality, description ignored', () => { + const data = buildImportedGoogleEventActivity( + { + id: 'foreign-1', + summary: 'Reunião com o prefeito', + description: 'pauta longa que fica no Google', + location: 'Gabinete', + start: { dateTime: '2026-08-20T12:00:00-03:00' }, + end: { dateTime: '2026-08-20T13:00:00-03:00' }, + }, + calendarId, + ) + + expect(data).toMatchObject({ + title: 'Reunião com o prefeito', + status: 'confirmado', + startAt: '2026-08-20T15:00:00.000Z', + endAt: '2026-08-20T16:00:00.000Z', + allDay: false, + locality: 'Gabinete', + googleEventId: 'foreign-1', + googleCalendarId: calendarId, + tags: [], + }) + expect(data && 'description' in data).toBe(false) + }) + + it('keeps a `[X] ` prefix of a foreign summary verbatim (no municipality to strip)', () => { + const data = buildImportedGoogleEventActivity( + { + id: 'foreign-2', + summary: '[URGENTE] Reunião', + start: { dateTime: '2026-08-20T12:00:00-03:00' }, + end: { dateTime: '2026-08-20T13:00:00-03:00' }, + }, + calendarId, + ) + expect(data?.title).toBe('[URGENTE] Reunião') + }) + + it('maps all-day events through the C104 civil-date convention', () => { + const data = buildImportedGoogleEventActivity( + { + id: 'foreign-3', + summary: 'Feriado', + start: { date: '2026-08-20' }, + end: { date: '2026-08-22' }, + }, + calendarId, + ) + expect(data).toMatchObject({ + allDay: true, + startAt: '2026-08-20T03:00:00.000Z', + endAt: '2026-08-21T03:00:00.000Z', + }) + }) + + it('skips events without a usable title or schedule (never pauses the pass)', () => { + const schedule = { + start: { dateTime: '2026-08-20T12:00:00-03:00' }, + end: { dateTime: '2026-08-20T13:00:00-03:00' }, + } + expect(buildImportedGoogleEventActivity({ id: 'f', ...schedule }, calendarId)).toBeNull() + expect( + buildImportedGoogleEventActivity({ id: 'f', summary: 'A', ...schedule }, calendarId), + ).toBeNull() + expect( + buildImportedGoogleEventActivity({ id: 'f', summary: '💡', ...schedule }, calendarId), + ).toBeNull() + expect( + buildImportedGoogleEventActivity({ summary: 'Sem id', ...schedule }, calendarId), + ).toBeNull() + expect( + buildImportedGoogleEventActivity( + { id: 'f', summary: 'Sem data', description: 'x' }, + calendarId, + ), + ).toBeNull() + }) + + it('builds a deterministic slug unique per event (recurrence + duplicate titles)', () => { + const first = buildImportedGoogleEventActivity( + { + id: 'master_20260816T120000Z', + summary: 'Reunião', + start: { dateTime: '2026-08-16T12:00:00-03:00' }, + end: { dateTime: '2026-08-16T13:00:00-03:00' }, + }, + calendarId, + ) + const second = buildImportedGoogleEventActivity( + { + id: 'master_20260817T120000Z', + summary: 'Reunião', + start: { dateTime: '2026-08-17T12:00:00-03:00' }, + end: { dateTime: '2026-08-17T13:00:00-03:00' }, + }, + calendarId, + ) + const duplicate = buildImportedGoogleEventActivity( + { + id: 'outro-evento', + summary: 'Reunião', + start: { dateTime: '2026-08-16T12:00:00-03:00' }, + end: { dateTime: '2026-08-16T13:00:00-03:00' }, + }, + calendarId, + ) + + expect(first?.slug).toMatch(/^reuniao-20260816T1200-[a-z0-9]+$/) + expect(first?.slug).not.toBe(second?.slug) + expect(first?.slug).not.toBe(duplicate?.slug) + }) +}) + +describe('buildImportedGoogleEventPatch / googleImportedEventContentEquals (C165)', () => { + const activity = { + title: 'Reunião com o prefeito', + startAt: '2026-08-20T15:00:00.000Z', + endAt: '2026-08-20T16:00:00.000Z', + allDay: false, + locality: 'Gabinete', + } + + it('patches only the fields the Teqo owns: no description, no id, verbatim summary', () => { + expect(buildImportedGoogleEventPatch(activity)).toEqual({ + summary: 'Reunião com o prefeito', + location: 'Gabinete', + start: { dateTime: '2026-08-20T12:00:00-03:00' }, + end: { dateTime: '2026-08-20T13:00:00-03:00' }, + }) + expect(buildImportedGoogleEventPatch({ ...activity, locality: '' }).location).toBe('') + }) + + it('compares summary + schedule + location and IGNORES the Google description', () => { + const patch = buildImportedGoogleEventPatch(activity) + const remote: GoogleRemoteEvent = { + id: 'foreign-1', + summary: patch.summary, + description: 'texto que só existe no Google', + location: patch.location, + start: patch.start, + end: patch.end, + } + expect(googleImportedEventContentEquals(remote, patch)).toBe(true) + + expect(googleImportedEventContentEquals({ ...remote, summary: 'Outro' }, patch)).toBe(false) + expect(googleImportedEventContentEquals({ ...remote, location: 'Outro' }, patch)).toBe(false) + expect( + googleImportedEventContentEquals( + { ...remote, start: { dateTime: '2026-08-21T12:00:00-03:00' } }, + patch, + ), + ).toBe(false) + }) +}) diff --git a/tests/unit/googleCalendarReverseEdit.unit.spec.ts b/tests/unit/googleCalendarReverseEdit.unit.spec.ts index 11a37e63..0e4f726b 100644 --- a/tests/unit/googleCalendarReverseEdit.unit.spec.ts +++ b/tests/unit/googleCalendarReverseEdit.unit.spec.ts @@ -1,12 +1,14 @@ import { describe, expect, it } from 'vitest' +import { + googleScheduleToActivityFields, + googleTitleFromSummary, +} from '@/lib/googleCalendarEventMapping' import { buildGoogleReverseCancelBody, buildGoogleReverseUpdateBody, GOOGLE_REVERSE_EDIT_BODY_PREFIX, googleEditIsNewer, - googleScheduleToActivityFields, - googleTitleFromSummary, } from '@/lib/googleCalendarReverseEdit' const MUNICIPALITY = 'Salvador' diff --git a/tests/unit/googleCalendarSyncHooks.unit.spec.ts b/tests/unit/googleCalendarSyncHooks.unit.spec.ts index ae73f959..b7cb12d3 100644 --- a/tests/unit/googleCalendarSyncHooks.unit.spec.ts +++ b/tests/unit/googleCalendarSyncHooks.unit.spec.ts @@ -39,6 +39,7 @@ const settledOutcome = { updated: 0, deleted: 0, reverseEdits: 0, + imported: 0, at: '', } diff --git a/tests/unit/listOmniboxB128.unit.spec.ts b/tests/unit/listOmniboxB128.unit.spec.ts index 9d2648e7..f45ffadf 100644 --- a/tests/unit/listOmniboxB128.unit.spec.ts +++ b/tests/unit/listOmniboxB128.unit.spec.ts @@ -234,6 +234,48 @@ describe('list omnibox adapters (B128)', () => { if (removedTab.kind === 'url') expect(removedTab.state.tab).toBe('proximos') }) + it('activity triage filter toggles "Sem município" exclusively with a município (C165)', () => { + const base = parseActivityListParams({ tab: 'todos' }) + const seeds = buildActivityOmniboxSuggestionSeeds({ tab: base.tab, municipalityOptions: [] }) + const suggestions = filterActivityOmniboxSuggestions(seeds, 'sem') + expect(suggestions.some((entry) => entry.id === 'unscoped')).toBe(true) + + const applied = applyActivityOmniboxSuggestion({ state: base, suggestionId: 'unscoped' }) + expect(applied.kind).toBe('url') + if (applied.kind !== 'url') return + expect(applied.state.unscoped).toBe(true) + + const chips = buildActivityOmniboxChips({ + state: applied.state, + municipalityLabelsById: new Map(), + }) + expect(chips.map((chip) => chip.id)).toEqual(['tab:todos', 'unscoped']) + + // Escolher um município tira o filtro de triagem (exclusivos)… + const withMunicipality = applyActivityOmniboxSuggestion({ + state: applied.state, + suggestionId: 'municipality:12', + }) + expect(withMunicipality.kind).toBe('url') + if (withMunicipality.kind === 'url') { + expect(withMunicipality.state.municipality).toBe(12) + expect(withMunicipality.state.unscoped).toBeUndefined() + } + + // …e aplicar "Sem município" de novo limpa o município. + const backToTriage = applyActivityOmniboxSuggestion({ + state: parseActivityListParams({ municipality: '12' }), + suggestionId: 'unscoped', + }) + if (backToTriage.kind === 'url') { + expect(backToTriage.state.unscoped).toBe(true) + expect(backToTriage.state.municipality).toBeUndefined() + } + + const removed = removeActivityOmniboxChip({ state: applied.state, chipId: 'unscoped' }) + if (removed.kind === 'url') expect(removed.state.unscoped).toBeUndefined() + }) + it('demand toggles exclusive status, kind and search; preserves activity on clear', () => { const state = parseDemandListParams({ status: 'aberta',