From 7e2dd3a4cb845cdde37f86242084b40e80b05315 Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Sat, 19 Sep 2026 12:05:35 -0300 Subject: [PATCH] =?UTF-8?q?feat(C200):=20separar=20falas=20por=20pessoa=20?= =?UTF-8?q?nas=20grava=C3=A7=C3=B5es=20enviadas=20(Closes=20#1167)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/changelog/2026-09-19-c200.md | 1 + .../plans/acervo-separacao-por-pessoa-impl.md | 239 + docs/plans/gates-gravacoes-predicados.md | 1 + .../acervo/gravacoes/[id]/falantes/route.ts | 32 + .../acervo/gravacoes/[id]/page.tsx | 2 + .../comunicacao/acervo/gravacoes/types.ts | 5 + .../(app)/comunicacao/acervo/page.tsx | 35 +- .../(campaign)/campanha/actions/recording.ts | 85 + src/collections/Recording.ts | 70 +- src/collections/RecordingSegment.ts | 13 +- .../recording/RecordingDetailPlayer.tsx | 104 +- .../recording/RecordingPersonFilter.tsx | 87 + .../recording/RecordingResultCard.tsx | 17 +- .../recording/RecordingSpeakerDialog.tsx | 174 + .../recording/RecordingSpeakerTranscript.tsx | 103 + .../RecordingTranscriptSegmentButton.tsx | 51 + src/lib/campaignPaths.ts | 4 + src/lib/recording.ts | 30 + src/lib/recordingDiarization.ts | 241 + src/lib/recordingTranscription.ts | 42 +- src/lib/schemas/recording.ts | 25 +- ...0260919_032729_add_recording_speakers.json | 16760 ++++++++++++++++ .../20260919_032729_add_recording_speakers.ts | 51 + src/migrations/index.ts | 10 +- src/payload-types.ts | 32 + src/utilities/ai/assemblyAiDiarize.ts | 178 + src/utilities/recordings/recordingJob.ts | 93 +- .../recordings/recordingListFilters.ts | 21 +- src/utilities/recordings/recordingListUrl.ts | 53 +- src/utilities/recordings/recordingPageData.ts | 76 +- .../recordings/recordingViewModels.ts | 107 +- tests/e2e/campaignSpeechAcervo.e2e.spec.ts | 189 +- tests/int/recording.int.spec.ts | 214 +- tests/unit/assemblyAiDiarize.unit.spec.ts | 134 + tests/unit/recording.unit.spec.ts | 14 + tests/unit/recordingDiarization.unit.spec.ts | 177 + tests/unit/recordingListFilters.unit.spec.ts | 44 + tests/unit/recordingListUrl.unit.spec.ts | 62 + 38 files changed, 19479 insertions(+), 97 deletions(-) create mode 100644 docs/changelog/2026-09-19-c200.md create mode 100644 docs/plans/acervo-separacao-por-pessoa-impl.md create mode 100644 src/app/(campaign)/campanha/(app)/comunicacao/acervo/gravacoes/[id]/falantes/route.ts create mode 100644 src/components/campaign/recording/RecordingPersonFilter.tsx create mode 100644 src/components/campaign/recording/RecordingSpeakerDialog.tsx create mode 100644 src/components/campaign/recording/RecordingSpeakerTranscript.tsx create mode 100644 src/components/campaign/recording/RecordingTranscriptSegmentButton.tsx create mode 100644 src/lib/recordingDiarization.ts create mode 100644 src/migrations/20260919_032729_add_recording_speakers.json create mode 100644 src/migrations/20260919_032729_add_recording_speakers.ts create mode 100644 src/utilities/ai/assemblyAiDiarize.ts create mode 100644 tests/unit/assemblyAiDiarize.unit.spec.ts create mode 100644 tests/unit/recordingDiarization.unit.spec.ts create mode 100644 tests/unit/recordingListFilters.unit.spec.ts diff --git a/docs/changelog/2026-09-19-c200.md b/docs/changelog/2026-09-19-c200.md new file mode 100644 index 00000000..65fbca59 --- /dev/null +++ b/docs/changelog/2026-09-19-c200.md @@ -0,0 +1 @@ +**C200 — Separar falas por pessoa nas gravações enviadas (2026-09-19):** as gravações do C199 saíam com a transcrição em bloco único e a assessoria não conseguia responder "o que fulano disse nesta gravação?". Entregou-se a camada de quem-falou-o-quê com agrupamento **acústico** e identificação **humana** (sem voiceprint, sem match voz↔pessoa, sem `Contact`): (1) **diarização** — o job ganhou uma etapa opcional atrás de um seam injetável (`SpeakerDiarizer`); o áudio extraído inteiro (uma passada, para a numeração ser global) vai ao provedor recomendado **AssemblyAI** `universal-2` + `speaker_labels` (US$0,17/h; chave `ASSEMBLYAI_API_KEY` ainda não configurada — sem chave/falha, a transcrição continua em bloco, nunca finge separação), e os turnos são alinhados aos segmentos do Whisper por sobreposição máxima (empate pelo ponto médio, sem sobreposição → turno mais próximo) com numeração `speaker-N` por primeira aparição; (2) **schema/migration** `20260919_032729_add_recording_speakers` — `recording_segment.speaker_key`, `recording.speaker_labels` (`{speakerKey,label}[]`), `recording.speaker_names` (`hasMany` derivado por hook, base da faceta) e `recording.speaker_labels_dropped`; o SQL duplicado de `reel.source_hash` que o snapshot defasado do C199 gerava foi removido à mão e o snapshot desta migration o corrige; (3) **reprocessamento não perde rótulo**: `reconcileSpeakerLabels` (puro) só mantém um rótulo em casamento mútuo 1:1 por sobreposição de tempo — split/merge descartam com aviso `speakerLabelsDropped` renderizado no próprio banner; (4) **faceta "Pessoa"** na fonte enviadas: param repetível `person` no contrato da lista (`contains` case-insensitive sobre `speaker_names`), chips removíveis (design cena 3), chip "aparece nesta gravação" no card e preservação do filtro ao submeter a busca; (5) **ação "Identificar falante"** (`POST .../gravacoes/[id]/falantes`) — rotula um agrupamento validando que a chave ainda existe na gravação, gate fail-closed do acervo (a escrita usa o predicado do dono; `advisor`/`leader` negados) e limpa o aviso de reconciliação; (6) **design C portado classe-a-classe** das cenas 1–5 (blocos por falante com avatar/estado/edição, diálogo de identificação, aviso âmbar de imprecisão sempre visível, mobile compacto); (7) **testes**: 17 unit novos (alinhamento/numeração/reconciliação/URL/where/provedor com fetch mockado), 4 int novos (faceta no join real, matriz de papéis da action, job com diarizador fake preservando rótulo e fallback com aviso) e 5 e2e novos na vertical; migração aplicada só em banco local, nenhuma gravação antiga reprocessada automaticamente. diff --git a/docs/plans/acervo-separacao-por-pessoa-impl.md b/docs/plans/acervo-separacao-por-pessoa-impl.md new file mode 100644 index 00000000..df15eaea --- /dev/null +++ b/docs/plans/acervo-separacao-por-pessoa-impl.md @@ -0,0 +1,239 @@ +# Impl: C200 — Separar falas por pessoa nas gravações enviadas + +Status: aprovado +Atualizado em: 2026-09-19 +Issue: #1167 +Intenção: docs/plans/acervo-separacao-por-pessoa.md +Appetite restante: herdado (~2–3 dias eng; um outcome verificável — a assessoria identifica quem fala numa gravação enviada e filtra o acervo pelas falas de uma pessoa) + +## Leitura da intenção + +- **Outcome:** gravações novas do acervo (fonte "Gravações enviadas", C199) passam a ter a transcrição **dividida por agrupamento acústico** ("Falante 1", "Falante 2", …); a assessoria ouve e usa "Identificar falante" para nomear um agrupamento, valendo para todas as falas dele naquela gravação; a lista ganha a faceta **"Pessoa"** que recorta as gravações onde aquele rótulo aparece. A busca geral continua cobrindo a transcrição inteira. +- **O que NÃO negociar:** agrupamento é acústico e a identificação é **humana** (nada de voiceprint, match voz↔pessoa, inferência ou sugestão automática); rótulo é texto por gravação, **sem segundo cadastro de pessoa** (`Contact` intocado); rótulo não preenchido permanece "Falante N"; sem `Consent` novo (mídia interna de staff); sem score de confiança de "quem falou" na UI; gate fail-closed do acervo (`communicator` + `coordinator`/`candidate`; `advisor`/`leader` negados); nada de backfill automático das gravações antigas; sem UI nova de reprocessar/diarizar fora do design aprovado. +- **O que reavaliar** (hipóteses da intenção): + - "O import substitui os segmentos em bloco — um rótulo só sobrevive se o reprocessamento carregar/re-aplicar" (intenção): confirmado; a reconciliação é decisão desta impl (D6). Nota de realidade: hoje o lifecycle **não tem** caminho `ready → processing` (retry só `failed`, e falha não grava segmentos), então a reconciliação é um contrato do job testado no int, pronto para a ação futura de reprocessar/diarizar (§D8). + - "A fonte enviadas já tem o padrão de chips/omnibox" (achado do explorador): **não tem** — hoje é só `CampaignSearchInput`. A faceta nova reusa o **`CampaignHeaderFilterPopover`** (multi-seleção) com adaptação local, sem importar o omnibox da Câmara. + - "Reusar o pipeline de transcrição por chunks para diarizar": não serve — chaves por chunk não se ligam entre chunks (ver D2). + - "Provedor de diarização" (questão em aberto da intenção): resolvido em D1/D2 (provedor externo, áudio inteiro numa passada) e registrado como **GATE humano de custo/chave**. + +## Abordagem recomendada + +```mermaid +flowchart TD + U[Upload C199] --> J[recordingJob] + J -->|"ffmpeg: mp3 mono 16k / chunks 20min"| W["Deep Infra Whisper
deepInfraTranscribeSegments"] + W --> M[mergeChunkTranscriptions] + J -->|"se há provedor: full.mp3 numa passada ffmpeg"| D["AssemblyAI universal-2
language_code=pt, speaker_labels"] + D -->|"utterances speaker/start/end"| A["assignSpeakerKeys
alinhamento por sobreposicao"] + M --> A + A --> T[["tx: recordingSegment.speakerKey
+ recording.speakerLabels reconciliadas"]] + D -.->|"falha, timeout ou sem chave"| F["fallback honesto:
transcricao sem agrupamento"] + F --> T + T --> R[(recording / recordingSegment)] + L["Lista ?source=enviadas&person=..."] -->|"speakerNames contains"| R + P["Detalhe /gravacoes/id"] -->|"blocos por falante"| R + DLG["Dialogo Identificar falante"] -->|"POST /gravacoes/id/falantes"| AC[labelRecordingSpeakerForActor] + AC --> R +``` + +**Opções consideradas:** A | B | C em cada decisão abaixo. +**Recomendação:** manter o Whisper/Deep Infra como dono da transcrição e adicionar uma **etapa opcional de diarização** sobre o áudio inteiro (AssemblyAI, `universal-2`, `speaker_labels`), **alinhando os turnos aos segmentos existentes** por sobreposição de tempo; chave do agrupamento no `recordingSegment`, rótulos na `recording` com a faceta denormalizada em `speakerNames` (`hasMany text` + `contains`, precedente `Speech.keywords`); reconciliação conservadora de rótulos no job + aviso; sem provedor/chave, a transcrição sai **sem agrupamento** (nunca fingir separação). + +### Decisões de engenharia + +**D1 — Provedor de diarização (pesquisa web, números oficiais).** +`Opções: A) AssemblyAI | B) Deepgram | C) Gladia | D) Speechmatics | E) diarização local no homeserver | F) LLM de áudio | G) Deep Infra (atual)` + +`Recomendação: A — AssemblyAI é o único com preço/hora mais baixo entre os verificados, aceita arquivos de até 10 h (cobre plenária inteira numa passada), tem pt com WER ≤10% declarado, diarização explícita no modelo universal-2 e fluxo assíncrono com polling (submit → poll), que é exatamente a forma do job C199 (heartbeat por atualização do updatedAt).` + +| Provedor | Diarização | Preço oficial (pré-gravado) | pt-BR | Limite por arquivo | Forma do fluxo | +| ---------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| **AssemblyAI** | `speaker_labels: true` (`universal-2`, `universal-3-5-pro`) | **US$0,15/h** (`universal-2`) + aditivo diarização standard **+US$0,02/h = US$0,17/h** ≈ US$0,0028/min | `pt` com "high accuracy (≤10% WER)"; `universal-3-5-pro` cita dialeto pt-BR | **10 h** por arquivo; 5 GB via URL/2,2 GB no `/v2/upload` | `POST /v2/upload` → `POST /v2/transcript` → poll; `utterances[]` com `speaker`/`start`/`end` (ms) | +| Deepgram | `diarize_model=latest` | Nova-3 pré-gravado **US$0,0043/min = US$0,258/h** (diarização incluída no pré-gravado) | `pt-BR` na lista do nova-3 | não verificado nesta sessão | `POST /v1/listen` único e síncrono (ou callback) | +| Gladia | incluída (pyannoteAI Precision-2) | async **US$0,61/h** | 100+ idiomas | **135 min** / 1 000 MB por request | upload → `/v2/pre-recorded` → poll | +| Speechmatics | incluída | Melia 1 batch **a partir de US$0,129/h** (em _production preview_ oficial) | 55+ idiomas | batch, limites no portal | submit → poll | +| Deep Infra (atual) | **não diariza** — catálogo ASR verificado ao vivo (6 modelos, nenhum com diarização) | US$0,00045/min no Whisper large-v3 | pt | chunks de 20 min (C199) | já é o ASR do job | +| Local (pyannote/sherpa-onnx) | sim (montagem própria) | sem API; custo de ops/CPU/modelos | depende do modelo | — | processo/serviço Python no homeserver | +| LLM de áudio (Gemini/OpenAI) | **não é diarização acústica** | por token | sim | — | áudio inteiro ao LLM | + +- **A (recomendada):** `utilities/ai/assemblyAiDiarize.ts` como dono do provedor (forma de `deepInfraTranscribe.ts`), `ASSEMBLYAI_API_KEY` nova. A diarização roda só com chave; a política de delete do transcript remoto (`DELETE /v2/transcript/{id}`) é best-effort pós-leitura (higiene), e **jamais** se liga `speech_understanding.speaker_identification` (nome automático — proibido pelo guardrail). +- **Rejeitadas:** + - **B** porque custa ~50% mais por hora (US$0,258 vs US$0,17) e é chamada síncrona de horas — sem o polling nativo que dá heartbeat barato ao job; qualidade comparável e pt-BR ok, então fica como **alternativa de segunda escolha no gate**. + - **C** porque o async aceita no máximo **135 min/1 000 MB** por request (docs oficiais): uma plenária de horas exigiria fatiar e a numeração global entre fatias volta a ser um palpite (o problema que D2 elimina); além de ~3,6× o custo. + - **D** porque o modelo de menor preço (Melia 1) está em _production preview_ oficial e o tier de maior acurácia (Enhanced) não publica um rate card único e estável nas fontes oficiais consultadas; sem vantagem decisiva sobre A. + - **E** porque o homeserver não tem GPU e montar pyannote/sherpa-onnx adiciona runtime Python, download de pesos e um domínio de falha de ops sem seam de teste; revisitar só com custo/volume material ou mudança de política de dados. + - **F** porque LLM de áudio atribui falante probabilisticamente (alucina atribuição e nome), não entrega turnos com timestamp confiável e colidiria com a regra OPS123 (provider `openai/*` reservado a design no Teqo). + - **G** porque capacidades verificadas: nenhum dos 6 modelos ASR do catálogo Deep Infra diariza; uma chave nova no mesmo provedor não resolve. + +**D2 — Escopo da diarização: áudio inteiro vs por chunk de 20 min.** +`Opções: A) uma passada no áudio inteiro | B) por chunk de 20 min com numeração global por ordem de primeira aparição | C) por chunk com re-associação por embedding entre chunks` + +`Recomendação: A — o agrupamento é uma propriedade do áudio inteiro; diarizar por chunk gera chaves que não se ligam entre chunks e repetir "Falante 1" em cada chunk refere-se a pessoas diferentes (enganoso, contra o guardrail). O áudio extraído (mono 16 kHz, 32 kbps ≈ 14 MB/h) fica muito abaixo do limite de tamanho do provedor e dentro das 10 h de duração; acima de 10 h o job pula a diarização (fallback honesto).` + +- **Rejeitadas:** **B** porque "Falante N" por chunk com numeração global por ordem de aparição **não** garante a mesma pessoa (sem biometria não há re-associação) e mente para a assessoria; **C** porque re-associação por embedding é voiceprint — proibida pelo produto e pela intenção. + +**D3 — Papel do provedor: diarizar e alinhar vs substituir o ASR.** +`Opções: A) provedor só fornece turnos; alinhamento aos segmentos do Whisper | B) provedor passa a ser o ASR da gravação (utterances viram segmentos) | C) provedor só quando há chave; Whisper quando não há` + +`Recomendação: A — o Whisper continua dono do texto, do searchText e dos timestamps por segmento (C199 pinado); o provedor entra apenas como camada de agrupamento. Falha do provedor não afeta a transcrição, e o alinhamento vira uma função pura testável sem rede.` + +- **Rejeitadas:** **B** porque troca o dono do texto, muda o custo do caminho feliz (~10× por minuto), reescreve a busca e faria a falha do provedor derrubar a transcrição inteira; **C** porque duas qualidades de transcrição no mesmo acervo sem o usuário saber, e dois donos do texto quebrando pinos C199. + +**D4 — Modelo de dados dos rótulos.** +`Opções: A) estado na recording (array `speakerLabels`) + `speakerNames` denormalizado (hasMany text) + chave no segmento | B) rótulo copiado em cada segmento | C) collection nova de rótulos | D) campo JSON` + +`Recomendação: A — exatamente o que a tarefa fixou: recordingSegment.speakerKey (chave do agrupamento) + estado de rótulos na recording; a faceta "Pessoa" usa speakerNames (text hasMany → text[] no Postgres; precedente Activity.tags/Speech.keywords) com contains (ILIKE), funcionando no Payload 3.82.` + +- A: `speakerLabels: { speakerKey, label }[]` (readOnly no admin; uma linha por agrupamento identificado) e hook `deriveSpeakerNames` preenchendo `speakerNames` (dedupe case-insensitive, ordem de aparição) — espelho de `RecordingSegment.deriveSearchText`. `speakerLabelsDropped: boolean` guarda o aviso de reconciliação (D6). Segmentos guardam só a chave. +- **Rejeitadas:** **B** porque renomear um agrupamento tocaria N segmentos e a faceta viraria um join caro; **C** é collection nova para um estado 1:N simples (custo de access/index/migration sem ganho) e foge da direção da tarefa; **D** não tem validação/índice nem precedente de query de faceta. + +**D5 — Semântica da faceta "Pessoa".** +`Opções: A) `{ speakerNames: { contains: valor } }`OR entre valores selecionados | B)`{ speakerNames: { in: valores } }` exato | C) normalizar rótulo em slug na gravação` + +`Recomendação: A — o precedente da casa (speechListFilters.ts:70, Speech.keywords contains) é case-insensitive e tolera variação ("Solla" casa "Dep. Jorge Solla"); a URL guarda o texto literal, a curadoria varia o rótulo e o recorte não fragmenta.` + +- **Rejeitadas:** **B** porque a variante livre ("Solla" vs "Dep. Jorge Solla") fragmentaria a faceta (anti-goal de curadoria acidental); **C** porque cria uma segunda representação do rótulo e um merge implícito que a intenção cortou. + +**D6 — Preservação de rótulos ao reprocessar.** +`Opções: A) reconciliação por sobreposição de tempo, conservadora 1:1, com flag + aviso | B) casar por igualdade de speakerKey | C) aceitar a perda silenciosa | D) bloquear o reprocessamento quando há rótulo` + +`Recomendação: A — sem biometria não há re-associação entre execuções; carregar um rótulo só quando um agrupamento novo casa 1:1 por tempo com um antigo é o contrato honesto. Quem não casa fica sem nome e o job liga speakerLabelsDropped, que rende o aviso no detalhe.` + +- A: o job lê labels + segmentos antigos **antes** de apagar, computa `reconcileSpeakerLabels` (puro: sobreposição acumulada por par, 1:1, ordenado por primeira aparição) dentro da mesma transação e grava `speakerLabels` reconciliadas + `speakerLabelsDropped`. O aviso vive **dentro** do banner âmbar da Cena 4 (uma linha condicional — ver §UI). +- **Rejeitadas:** **B** porque a chave é por execução e a mesma chave pode referir outra pessoa (o aceite manda re-vincular com aviso, não fingir); **C** viola o aceite; **D** porque o reprocessamento de hoje só existe para `failed` (que não tem rótulo) e, na ação futura, bloquear sem alternativa é pior que reconciliar e avisar. + +**D7 — Fallback sem provedor/chave/timeout.** +`Opções: A) transcrição sem agrupamento (estado C199, sem "Falante") | B) falhar a gravação | C) criar um "Falante 1" único` + +`Recomendação: A — o acervo continua útil e a ausência de blocos não afirma nada; é exatamente o detalhe C199 já aprovado, sem markup novo.` + +- **Rejeitadas:** **B** porque faz a transcrição (que não depende do provedor) depender de um custo humano ainda não aprovado; **C** porque sugere um falante único onde há vários — fingir separação, proibido. + +**D8 — Reprocessar/diarizar gravações antigas.** +`Opções: A) ação explícita por gravação agora | B) backfill automático | C) sem ação agora; follow-up com gatilho | D) UI de reprocessar junto com este item` + +`Recomendação: C — a tarefa veta UI nova de reprocessar fora do design e a intenção veta backfill. A reconciliação (D6) fica pronta no job para quando a ação existir.` + +- **Gatilho do follow-up:** chave do provedor ativa em produção **e** (pedido operacional de diarizar o acervo antigo **ou** passarem ~30 gravações novas sem diarização). Aí o designer estende o artefato **antes** do markup (trigger a). **Débito absorvido pelo D8 (triagem do /simplify):** quando a ação de reprocessar existir, a semântica de limpar `speakerLabelsDropped` no rótulo precisa considerar rótulos ainda perdidos (hoje inalcançável: não há `ready → processing`). +- **Débito absorvido pelo C202:** a action de rótulo gateia no action-level com `canReadCommunicationCatalog` (a collection já exige `canUploadRecording`); a troca pelo predicado de escrita do dono foi adicionada ao plano `docs/plans/gates-gravacoes-predicados.md` (#1186). +- **Rejeitadas:** **B** porque o custo e o risco de mexer em rótulos não justificam varrer tudo; **A/D** porque contrariam a restrição de superfície do item. + +## Componentes / mudanças + +- **`src/lib/recordingDiarization.ts`** (novo, puro/client-safe): `DiarizedTurn`, `assignSpeakerKeys` (sobreposição máxima; empate pelo ponto médio; sem sobreposição → turno mais próximo; numeração global `speaker-1…` por primeira aparição), `reconcileSpeakerLabels` (D6), `speakerNamesFromLabels`, `recordingSpeakerDefaultLabel(n)` → `Falante ${n}`, `RECORDING_DIARIZATION_MAX_SECONDS = 10h` (limite do provedor citado em comentário). Mesmo padrão de dono dos puros de `lib/recordingTranscription.ts`. +- **`src/lib/recordingTranscription.ts`** (editar): `buildRecordingFullAudioFfmpegArgs` (mp3 mono 16k/32k sem segmentação) ao lado do args de chunks; merge/searchText intocados. +- **`src/lib/recording.ts`** (editar): `RECORDING_SPEAKER_LABEL_MAX_LENGTH = 120`, mensagens pt-BR do diálogo/action, nada de status novo. +- **`src/utilities/ai/assemblyAiDiarize.ts`** (novo, `server-only`): dono do provedor (molde `deepInfraTranscribe.ts:98-137`) — upload → transcript (`language_code: 'pt'`, `speaker_labels: true`, sem identificação automática) → poll com deadline de 60 min e intervalo de 5 s; normalização ms→s; nunca lança (`{ ok, turns }` / `{ ok:false, error, status }`); `DELETE` best-effort do transcript; chave ausente → 503. Exporta `assemblyAiDiarize` e `configuredSpeakerDiarizer()` (null sem chave). +- **`src/utilities/recordings/recordingJob.ts`** (editar): `runRecordingJob(payload, id, transcribe = deepInfraTranscribeSegments, diarize = configuredSpeakerDiarizer())`; extrai `full.mp3` só quando `diarize` existe e a duração ≤ 10 h, chama o provedor (heartbeat `step:'transcribing'`), alinha com `assignSpeakerKeys`, e na transação já existente (`recordingJob.ts:191-227`) lê o estado antigo, apaga/recria segmentos com `speakerKey` e grava `speakerLabels`/`speakerLabelsDropped`. Falha/timeout do provedor **não** marca `failed`. +- **`src/collections/Recording.ts`** (editar): `speakerLabels` (array `{ speakerKey, label }`, readOnly), `speakerNames` (`text` `hasMany`, readOnly), `speakerLabelsDropped` (`checkbox`, readOnly) + hook `deriveSpeakerNames` (fallback `originalDoc` como `deriveSearchText`). +- **`src/collections/RecordingSegment.ts`** (editar): `speakerKey` (`text`, opcional, readOnly). +- **`src/utilities/recordings/recordingListUrl.ts`** (editar): param `person` (repetível) no `RecordingListState`, parse com `allParamValues` + trim + cap 120 + dedupe, serialize com `append`, `toggleRecordingPerson`/`removeRecordingPerson` puros (page reset) — o "adapter" da faceta mora aqui em vez de um módulo espelho de `speechOmnibox.ts` (depth check). +- **`src/utilities/recordings/recordingListFilters.ts`** (editar): `buildRecordingListWhere` AND-e `searchText like` + `{ or: persons.map(p => ({ speakerNames: { contains: p } })) }`. +- **`src/utilities/recordings/recordingViewModels.ts`** (editar): `RecordingSpeakerGroupViewModel` (`key`, `index`, `defaultLabel`, `label`, `isIdentified`, `segments`), `RecordingDetailViewModel.speakerGroups` + `speakerLabelsDropped`; `RecordingListItemViewModel.matchedPersons`; agrupamento por primeira aparição; caminho sem chave renderiza a lista plana C199. +- **`src/utilities/recordings/recordingPageData.ts`** (editar): selects (`speakerNames`, `speakerLabels`, `speakerLabelsDropped`, `speakerKey`), `loadRecordingFilterOptions` (select de `speakerNames` com `pagination:false`, dedupe e sort pt-BR — molde `speechPageData.ts:85-121`), `matchedPersons` por linha (puro `contains` case-insensitive espelhando o where). +- **`src/app/(campaign)/campanha/actions/recording.ts`** (editar): `labelRecordingSpeakerForActor({ recordingId, speakerKey, label })` — gate pelo **predicado de escrita do dono** (C202, §Dependências), valida que a chave existe em algum segmento da gravação, faz read-modify-write do array `speakerLabels` e limpa `speakerLabelsDropped` (o ato humano reconhece/revisa); 1 collection → sem transação. +- **`src/lib/schemas/recording.ts`** (editar): `recordingSpeakerLabelRequestSchema` (id, `speakerKey` `^speaker-\d{1,3}$`, label 1..120 trim) + mensagens seguras. +- **Rotas** (`.../acervo/gravacoes/[id]/falantes/route.ts`, novo): `POST` via `campaignJsonMutationRoute` (sem allowlist de conventions — rota wrapper, não crua), molde de `[id]/retry/route.ts`; `types.ts` ganha `RecordingSpeakerLabelResponse`; `lib/campaignPaths.ts` ganha `campaignRecordingSpeakersHref`. +- **UI** (`src/components/campaign/recording/`): `RecordingSpeakerTranscript.tsx` (novo, client; blocos + banner âmbar + botões), `RecordingSpeakerDialog.tsx` (novo, client; molde `SpeechCutDeleteDialog` + `postCampaignJson`), `RecordingPersonFilter.tsx` (novo, client; `CampaignHeaderFilterPopover` + `useCampaignListFilterNavigation`), edições em `RecordingDetailPlayer.tsx` (renderiza grupos quando existem; senão o `
    ` atual), `RecordingResultCard.tsx` (chip de pessoa) e `acervo/page.tsx` (hidden inputs de `person` no form de busca, linha da faceta, vazio com filtro). +- **Manifest e2e** (`scripts/lib/e2e-affected-manifest.mjs`): **sem mudanças esperadas** — `src/utilities/ai` (linhas 261-265) já mapeia `campaignSpeechAcervo`, e `src/utilities/recordings`/`src/components/campaign/recording`/`src/lib/recording` (322-336) já cobrem os arquivos novos; confirmar no gate. +- **Migration:** `add_recording_speakers` (abaixo). +- **Access / Consent:** sem `Consent` novo; `utilities/access/recordings.ts` intocado neste item (C202 é o dono da ampliação de gate); a action de rótulo é a única superfície nova e usa o predicado de escrita do dono. +- **UI:** Impeccable **C** — port do artefato + shells (`CampaignHeaderFilterPopover`, `CampaignListPendingBoundary`, `CampaignListEmptyState`, `CampaignListFooter`, `Dialog`, `Alert`, `Spinner`); shape→craft→critique→polish no ciclo. + +### Dados → forma (se aplicável) + +- **N/A por decisão da intenção** (`acervo-separacao-por-pessoa.md:59`): filtro por pessoa é recorte de conteúdo, não métrica/agregado; nenhuma confiança numérica é exposta (o produto proíbe score de "quem falou"). Nada a apresentar; nenhuma forma nova. + +## UI e port do design (fonte de verdade: `acervo-separacao-por-pessoa-ui-design.html`) + +**Coberto pelo artefato — port classe-a-classe:** + +1. **Cena 1 (detalhe desktop, agrupado):** grid `grid-cols-[minmax(0,1.05fr)_minmax(430px,.95fr)]`, mídia à esquerda (player + `Baixar` + banner informativo **literal** "Os falantes são agrupados automaticamente pelo áudio. A identificação de cada pessoa é feita pela equipe.") e coluna direita com heading uppercase "Transcrição por falante", hint "Clique para posicionar" e `article.speaker-block` por grupo: head (`size-7` avatar, `Falante N`/rótulo, sub "Agrupamento sem identificação"/"Identificação feita pela equipe", botão outline "Identificar falante"/"Editar identificação") e linhas `seg` (grid `3.25rem 1fr`, `data-start-seconds`, ativo com `bg-muted`, hora em `text-primary` quando ativo, texto com `SpeechHighlightParts`). +2. **Cena 2 (diálogo):** `Dialog` com título "Identificar falante", descrição `Dê um nome ao agrupamento “Falante N”.` (sempre o rótulo default), caixa informativa "Ouça um trecho e informe quem é a pessoa. O acervo não sugere nem identifica pessoas automaticamente.", label "Nome do falante", placeholder "Digite o nome do falante", helper "O nome será aplicado a todas as falas deste agrupamento nesta gravação.", botões "Cancelar"/"Salvar identificação"; erro inline (molde retry/delete), `router.refresh()` no sucesso. +3. **Cena 3 (lista com faceta):** mantém `RecordingsSearchForm` (`CampaignSearchInput`, C199) **com hidden inputs dos `person` ativos** (submeter a busca não pode perder o filtro) e adiciona a linha de chips (botão outline "Pessoa: {rótulo}" com X) via `CampaignHeaderFilterPopover` multi-seleção; card com o chip `{rótulo}` + "aparece nesta gravação" sob o excerpt. +4. **Cena 4 (aviso de imprecisão):** banner âmbar com título "A separação por falante pode estar imprecisa" e corpo "O agrupamento é automático e aproximado. Ruído e falas sobrepostas podem colocar trechos no agrupamento errado. A equipe continua responsável por identificar cada falante." — **sempre visível no detalhe agrupado** (sem score de qualidade, D1/anti-goal; o artefato não define gatilho). A linha condicional de reconciliação (D6) entra **dentro desse mesmo banner** quando `speakerLabelsDropped`: "Esta gravação foi reprocessada e algumas identificações não puderam ser mantidas. Confira os falantes." — uma frase, sem componente/superfície nova. +5. **Cena 5 (mobile 390 px):** mesmo detalhe em coluna única, head compacto do bloco (avatar + nome + botão "Identificar"/ícone de edição com `aria-label` "Editar identificação"), banner informativo curto **literal** "Agrupamento automático e aproximado. A identificação é feita pela equipe." e hint "Toque para posicionar". + +**Decisões de port fora do literal (baratas, revisáveis):** + +- **Subtítulo da página na Cena 3** ("Gravações enviadas"): mantém o subtítulo aprovado do C199 ("Busque nas falas da Câmara ou nas gravações da equipe.") + alternador; a Cena 3 é shorthand do estado da fonte e mudar o chrome regride o pin do C199 sem ganho de produto. +- **Vazio com filtro de pessoa**: reusa `CampaignListEmptyState` com a copy da casa ("Tente outro termo, remova filtros ou limpe a busca.") — estado não desenhado, shell aprovada. +- **Fallback sem agrupamento**: renderiza o detalhe C199 **como está** (sem copy nova de "não diarizado"). A tarefa manda disparar o designer se precisarmos de superfície nova — **não precisamos**; se surgir demanda de explicar a ausência, é trigger a (designer antes do markup), registrado como follow-up (§D8). +- Dialog sobre grupo já identificado reusa a copy da Cena 2 com o rótulo default; o trigger é que muda ("Editar identificação"). +- `role="tablist"` inexistente aqui; o alternador C199 fica intocado. + +## Migration + +- Nome: `pnpm migrate:create add_recording_speakers` → gera coluna `recording_segment.speaker_key` (`text`), tabela `recording_speaker_labels` (`id`, `_order`, `_parent_id` FK `recording` cascade, `speaker_key`, `label`), coluna `recording.speaker_names` (`text[]`) e `recording.speaker_labels_dropped` (`boolean`). **Revisar o SQL**; sem índices novos (a faceta é `contains` sobre `text[]` em tabela pequena e o `Speech.keywords` não tem índice dedicado). Referência de forma: `20260715_163458_initial` (arrays) e `20260918_201009_add_reel`. +- **HARD-STOP de aprovação humana explícita, mesmo em sessão `--auto`, e local-only:** criar/aplicar a migração apenas no banco local do worktree (`pnpm migrate`), **nunca** tocar banco remoto/prod e **nunca** editar migração antiga. Parar, apresentar o SQL gerado e aguardar aprovação antes de seguir para o job/UI. + +## Invariantes + +- Local API em contexto de usuário sempre com `overrideAccess: false` + `user` (lista, detalhe, action de rótulo); bypass admin só no pipeline que é dono do row, com comentário (forma `recordingJob.ts:54-56`). +- Escrita multi-collection em transação com `req: { transactionID }` (`withPayloadTransaction`): substituição de segmentos + estado de rótulos + `searchText`/status na mesma transação; a action de rótulo é 1 collection. +- Sem `Consent` novo; sem `Contact`; rótulo é texto; nenhuma URL pública; `/arquivo` e matriz de papéis intocados. +- Gate fail-closed do acervo; a escrita nova usa o predicado de escrita do dono (C202) — nunca `canReadCommunicationCatalog` gateando escrita; `advisor`/`leader` negados (matriz no int/e2e). +- Sem biometria/voiceprint/unificação entre gravações; sem score/confiança na UI; sem inferência de identidade. +- "Edit the owner, don't twin": puros no `lib` dono da pipeline, provedor no `utilities/ai` dono, where/estado no `recordings/`, sem módulo top-level novo em `src/utilities/`. +- Identificadores em inglês; copy/labels pt-BR; valores de URL em pt-BR só os existentes (`source=enviadas`, `falantes`). +- `pnpm generate:types` após o schema; `pnpm gate:fast` durante; `pnpm push` como entrega; **este plano e `docs/changelog/2026-09-19-c200.md` entram no commit da entrega**. + +## Fases verificáveis + +1. **Puros + schema + migration (HARD-STOP)** (~1 dia). + - `lib/recordingDiarization.ts`, `lib/recordingTranscription.ts` (+full), `recordings/recordingListUrl|Filters|ViewModels|PageData`, `lib/recording.ts`, collections + hook; `pnpm generate:types`. + - Unit: alinhamento (sobreposição, empate, sem sobreposição→mais próximo, vazio), numeração global por primeira aparição, reconciliação (1:1 carrega; split/merge/orfão dropa + flag), `speakerNames` derivado, guard de 10 h, ffmpeg args do full, parse/serialize `person` (multi/dedupe/cap), where `contains`+q AND, `matchedPersons`. + - Int: matriz de papéis e persistência das três colunas novas; **validação da query da faceta `contains` em `text[]` no Payload 3.82** (precedente `Speech.keywords` e `Activity.tags`). + - **Migration `add_recording_speakers` — HARD-STOP humano, local-only.** +2. **Provedor + job + ação** (~1 dia). + - `assemblyAiDiarize` (seam injetável na assinatura do job), job com reconciliação na transação, action/rota/schema/paths. + - Int: job com diarizador fake (feliz multi-chunk → `speakerKey` por segmento + labels preservadas; falha do provedor → `ready` sem agrupamento; reconciliação com fixtures antigas; chave inexistente → fallback), `labelRecordingSpeakerForActor` (papéis; chave desconhecida; label vazio/longo; limpa `speakerLabelsDropped`), sem chamada real de rede. +3. **UI** (~1 dia). + - Cenas 1–5 + hidden inputs + faceta + cards; ação de rótulo no diálogo. + - Unit: view models dos grupos (ordem, `Falante N`, rótulo aplicado, flag), cena/copy constants; e2e cobre o markup. +4. **Gates + GATE humano** (~0.5 dia). + - Estender `tests/e2e/campaignSpeechAcervo.e2e.spec.ts` (describe C199): fixture `ready` com segmentos com `speakerKey` + `speakerLabels`; detalhe renderiza blocos/"Falante N"/rótulo/aviso; lista `?person=` recorta e o card mostra o chip; fallback sem `speakerKey` mantém `data-start-seconds` do C199; advisor negado na rota nova. + - `pnpm gate:fast`; `pnpm test:e2e:affected`; manifest/conventions (confirmar sem mudança); **smoke manual do provedor com 1 gravação real** (chave aprovada); changelog; push. + +Quota: ~3–3,5 dias eng, dentro do appetite herdado; se apertar, o corte é o polish do port (a estrutura das cenas 1/2/3 fica antes do acabamento). + +## Rabbit holes / Não escopo (engenharia) + +- Voiceprint/embedding para re-associar falantes entre execuções/gravações; unificação automática de "mesma pessoa". +- Cadastro/diretório de pessoas ou vozes; qualquer uso de `Contact`. +- Edição/correção de transcrição, alinhamento por palavra, refino de sobreposição. +- Backfill de diarização do acervo antigo e UI de "Diarizar" (follow-up com gatilho, §D8). +- Multi-provedor/adapter genérico de diarização (uma interface, um provedor — sem volatilidade que justifique mais). +- Webhook/callback do provedor (o poll do job basta; callback criaria superfície pública nova). +- Score/confiança de falante na UI (proibido); detecção automática de "qualidade ruim". +- Fila/worker, tempo real, streaming, cortes por falante, busca semântica (C192) e merge de fontes. + +## Riscos e mitigação + +- **Custo/chave do provedor não aprovados (GATE).** Sem chave, tudo degrada para a transcrição sem agrupamento (D7); a feature nova só liga após a aprovação — nenhum teste depende de rede. +- **`speaker_labels` não suportado para `pt` na conta real.** Os docs oficiais da AssemblyAI listam `pt` com alta acurácia no `universal-2` e diarização nos modelos `universal-2`/`universal-3-5-pro`, mas a matriz feature×idioma não é pública: **smoke manual na fase 4** antes do rollout; falha honesta = fallback. +- **Qualidade do agrupamento em plenária ruidosa/sobreposta.** Banner da Cena 4 sempre visível + identificação humana + nenhum claim automático; a copy do provedor recomenda ≥30 s de fala por pessoa (citada nos docs). +- **Diarização > 10 h ou timeout de poll.** Guard de duração + deadline de 60 min com heartbeat; resultado = fallback sem agrupamento, nunca `failed`. +- **Alinhamento errado na fronteira de turnos.** Atribuição por sobreposição máxima com desempate pelo ponto médio; unidade cobre; aviso de imprecisão cobre o resto. +- **Reconciliação conservadora demais (rótulo perdido).** Só perde quando não há mapeamento 1:1; `speakerLabelsDropped` dispara o aviso; teste int garante carregar no caso trivial (mesmos tempos). +- **Corrida no read-modify-write de `speakerLabels`.** Aceita para ferramenta interna de equipe única (uma gravação editada por vez na prática); se doer, collection dedicada de rótulos é o follow-up. +- **Faceta cara conforme o catálogo cresce.** Select de `speakerNames` + dedupe é O(n) com n pequeno; gatilho: acervo > ~2 000 gravações ou opções > ~200 (pré-computar opções/`distinct`). +- **Regressão do C199.** O caminho sem `speakerKey` mantém o markup exato do `
      ` atual; os testes C199 (unit/int/e2e) continuam a rede. +- **Vazamento de dados ao provedor externo.** Áudio de eventos públicos da equipe, sem PII de terceiros; delete best-effort do transcript remoto; política de retenção validada no gate. + +## GATE humano — provedor, custo e chave (assumido; validar antes do rollout) + +- **Assumido:** provedor **AssemblyAI** (`universal-2` + `speaker_labels`), custo **US$0,17/h** de áudio (US$0,0028/min) — fonte oficial: pricing AssemblyAI (Universal-2 US$0,15/h; diarização async standard +US$0,02/h) e docs de Speaker Diarization/limites (10 h, 5 GB; upload 2,2 GB). +- **Custo projetado:** 10 h/mês ≈ US$1,70; 50 h/mês ≈ US$8,50; 200 h/mês ≈ US$34,00. +- **Pendente de aprovação humana (mesmo com `--auto`):** criar conta/chave na AssemblyAI, definir `ASSEMBLYAI_API_KEY` no env de produção (e staging se desejado), aceitar o envio do áudio a terceiro e o orçamento acima; rodar o smoke real. **Nada disso bloqueia a implementação** (seam injetável + testes sem rede); bloqueia só o rollout. +- **Alternativa registrada no gate:** Deepgram (US$0,0043/min = US$0,258/h, diarização incluída, pt-BR) — se a aprovação preferir chamada síncrona única; a troca exige só um novo dono em `utilities/ai/` (a interface do seam não muda). + +## Aceite de engenharia + +- [ ] Aceite de produto da intenção coberto: transcrição dividida por "Falante N" nas gravações novas com provedor; "Identificar falante" vale para todo o agrupamento; faceta "Pessoa" recorta a fonte enviadas; busca geral intocada; rótulo vazio permanece "Falante N"; nunca sugere/infere identidade; reprocessar não perde rótulo sem re-vinculação avisada; antigas não são reprocessadas automaticamente. +- [ ] Invariantes AGENTS/engineering-standards: `overrideAccess:false` + `user`; transação multi-collection; sem `Consent`/`Contact`; `advisor`/`leader` fail-closed; sem biometria/score; sem módulo top-level novo em `utilities/`; migração nova sem editar antigas; plano + changelog no commit; `pnpm push`. +- [ ] Testes de domínio onde access/write paths mudam: puros da diarização/reconciliação/URL/where/view models no unit; job com seam injetado + action de rótulo com matriz de papéis + query da faceta no int; markup do detalhe/liste no e2e existente. + +## Self-score de decision-quality + +**4,5/5.** As decisões caras têm alternativas honestas e rejeições ancoradas: provedor com número oficial de quatro candidatos + limitações reais (135 min da Gladia, preview da Melia, 10 h/5 GB da AssemblyAI), escopo da diarização (áudio inteiro vs chunk — o modo chunk mente e foi rejeitado), papel do provedor (não substitui o ASR dono), modelo dos rótulos com precedente de query validado (`hasMany` + `contains` no Payload 3.82), semântica da faceta, reconciliação conservadora com aviso e fallback honesto. Cabe no appetite herdado (fases somam ~3–3,5 dias, tracer nos puros+schema cedo) e reusa shells/donos existentes (`CampaignHeaderFilterPopover`, `withPayloadTransaction`, `campaignJsonMutationRoute`, moldes de `deepInfraTranscribe`, `SpeechCutDeleteDialog`, `speechListFilters`). Perde 0,5 porque duas coisas ficam fora do código e viram parada dura/validação: a **migração** (HARD-STOP explícito, local-only) e o **custo/chave do provedor** (assumido; gate humano com smoke real), além de duas escolhas de port que dependem de ratificação no gate (banner da Cena 4 sempre visível sem score; subtítulo do C199 mantido na Cena 3). diff --git a/docs/plans/gates-gravacoes-predicados.md b/docs/plans/gates-gravacoes-predicados.md index 02c136e3..da16acd0 100644 --- a/docs/plans/gates-gravacoes-predicados.md +++ b/docs/plans/gates-gravacoes-predicados.md @@ -18,6 +18,7 @@ Expor no dono (`utilities/access/recordings.ts`) um predicado de papel compartil - `POST .../gravacoes/enviar` (`route.ts` — gate cru de sessão e origem). - `retryRecordingForActor`, `deleteRecordingForActor` (`actions/recording.ts`). +- `labelRecordingSpeakerForActor` (`actions/recording.ts`; absorvido do C200 #1167 — a action gateia no action-level com `canReadCommunicationCatalog` enquanto a collection usa `canUploadRecording`). - `getRecordingStatusesForActor` é leitura: mantém `canReadCommunicationCatalog`. Rejeitadas: duplicar o corpo dos predicados nas rotas (gêmeo); criar um quarto predicado (o dono já tem os três). diff --git a/src/app/(campaign)/campanha/(app)/comunicacao/acervo/gravacoes/[id]/falantes/route.ts b/src/app/(campaign)/campanha/(app)/comunicacao/acervo/gravacoes/[id]/falantes/route.ts new file mode 100644 index 00000000..0f5366f6 --- /dev/null +++ b/src/app/(campaign)/campanha/(app)/comunicacao/acervo/gravacoes/[id]/falantes/route.ts @@ -0,0 +1,32 @@ +import { NextResponse } from 'next/server' + +import { labelRecordingSpeakerForActor } from '@/app/(campaign)/campanha/actions/recording' +import { + RECORDING_FORBIDDEN_MESSAGE, + RECORDING_GENERIC_ERROR_MESSAGE, + RECORDING_NOT_FOUND_MESSAGE, + RECORDING_SPEAKER_UNKNOWN_MESSAGE, + recordingSpeakerLabelRequestSchema, +} from '@/lib/schemas/recording' +import { campaignJsonMutationRoute } from '@/utilities/campaignJsonMutationRoute' + +import type { RecordingSpeakerLabelResponse } from '../../types' + +export const dynamic = 'force-dynamic' + +/** C200 — the team identifies one speaker cluster of the recording (human). */ +export const POST = campaignJsonMutationRoute( + { + bodySchema: recordingSpeakerLabelRequestSchema, + safeMessages: [ + RECORDING_FORBIDDEN_MESSAGE, + RECORDING_NOT_FOUND_MESSAGE, + RECORDING_SPEAKER_UNKNOWN_MESSAGE, + ], + genericMessage: RECORDING_GENERIC_ERROR_MESSAGE, + }, + async (body) => { + const result = await labelRecordingSpeakerForActor(body) + return NextResponse.json({ status: 'success', ...result }) + }, +) diff --git a/src/app/(campaign)/campanha/(app)/comunicacao/acervo/gravacoes/[id]/page.tsx b/src/app/(campaign)/campanha/(app)/comunicacao/acervo/gravacoes/[id]/page.tsx index 57f020f9..2b2b7087 100644 --- a/src/app/(campaign)/campanha/(app)/comunicacao/acervo/gravacoes/[id]/page.tsx +++ b/src/app/(campaign)/campanha/(app)/comunicacao/acervo/gravacoes/[id]/page.tsx @@ -116,6 +116,8 @@ export default async function RecordingDetailPage({ downloadHref={recording.downloadHref} failureMessage={recording.failureMessage} segments={recording.segments} + speakerGroups={recording.speakerGroups} + speakerLabelsDropped={recording.speakerLabelsDropped} initialSeconds={initialSeconds} /> diff --git a/src/app/(campaign)/campanha/(app)/comunicacao/acervo/gravacoes/types.ts b/src/app/(campaign)/campanha/(app)/comunicacao/acervo/gravacoes/types.ts index 7fd333db..defb74bc 100644 --- a/src/app/(campaign)/campanha/(app)/comunicacao/acervo/gravacoes/types.ts +++ b/src/app/(campaign)/campanha/(app)/comunicacao/acervo/gravacoes/types.ts @@ -19,3 +19,8 @@ export type RecordingDeleteResponse = export type RecordingRetryResponse = | { status: 'success'; recording: RecordingViewModel } | { status: 'error'; message: string } + +/** Wire contract of `POST .../gravacoes/[id]/falantes` (C200) — label a cluster. */ +export type RecordingSpeakerLabelResponse = + | { status: 'success'; labeled: true } + | { status: 'error'; message: string } diff --git a/src/app/(campaign)/campanha/(app)/comunicacao/acervo/page.tsx b/src/app/(campaign)/campanha/(app)/comunicacao/acervo/page.tsx index c7a26486..8bfa9187 100644 --- a/src/app/(campaign)/campanha/(app)/comunicacao/acervo/page.tsx +++ b/src/app/(campaign)/campanha/(app)/comunicacao/acervo/page.tsx @@ -5,6 +5,7 @@ import { redirect } from 'next/navigation' import { getPayload } from 'payload' import { AcervoSourceToggle } from '@/components/campaign/recording/AcervoSourceToggle' +import { RecordingPersonFilter } from '@/components/campaign/recording/RecordingPersonFilter' import { RecordingResultList } from '@/components/campaign/recording/RecordingResultList' import { RecordingStatusRefresher } from '@/components/campaign/recording/RecordingStatusRefresher' import { RecordingUploadDialog } from '@/components/campaign/recording/RecordingUploadDialog' @@ -77,9 +78,19 @@ const AcervoHeader = ({ source }: { source: 'camara' | 'enviadas' }) => ( ) -const RecordingsSearchForm = ({ query }: { query?: string }) => ( +const RecordingsSearchForm = ({ + query, + people, +}: { + query?: string + people?: readonly string[] +}) => (
      + {/* The "Pessoa" facet survives a new textual search. */} + {(people ?? []).map((person) => ( + + ))} {/* The shared input owns the accessible label; this is its visible twin. */}

      Buscar nas gravações enviadas

      > }) => { const hasSearch = Boolean(data.state.q) + const hasPeople = (data.state.people?.length ?? 0) > 0 + const hasFilters = hasSearch || hasPeople return ( - + + + ({ id: row.id, status: row.status }))} @@ -116,21 +131,27 @@ const RecordingsSource = ({ ) : ( - {hasSearch ? ( + {hasFilters ? ( ) : ( diff --git a/src/app/(campaign)/campanha/actions/recording.ts b/src/app/(campaign)/campanha/actions/recording.ts index 8d6edb94..755ee7e9 100644 --- a/src/app/(campaign)/campanha/actions/recording.ts +++ b/src/app/(campaign)/campanha/actions/recording.ts @@ -12,14 +12,17 @@ import { RECORDING_FORBIDDEN_MESSAGE, RECORDING_NOT_FOUND_MESSAGE, RECORDING_RETRY_NOT_FAILED_MESSAGE, + RECORDING_SPEAKER_UNKNOWN_MESSAGE, recordingDeleteRequestSchema, recordingRetryRequestSchema, + recordingSpeakerLabelRequestSchema, recordingStatusRequestSchema, type RecordingStatusRequest, } from '@/lib/schemas/recording' import type { CampaignUser } from '@/payload-types' import { getCampaignActionContext } from '@/utilities/campaignActionContext' import { onPayloadTransactionCommit, withPayloadTransaction } from '@/utilities/payloadTransaction' +import { acquireTextAdvisoryLocks } from '@/utilities/postgresTransactionLocks' import { reapStaleRecording } from '@/utilities/recordings/recordingJob' import { startRecordingJobInBackground } from '@/utilities/recordings/recordingScheduler' @@ -39,6 +42,7 @@ const recordingSelect = { durationSeconds: true, media: true, updatedAt: true, + speakerLabels: true, } as const const loadRecordingForActor = async ( @@ -138,6 +142,87 @@ export const getRecordingStatusesForActor = async ( return result.docs.map(toRecordingViewModel) } +/** + * C200 — names one acoustic cluster of a recording. Human curation only: the + * key must belong to a segment of this recording (a stale dialog cannot invent + * a group), and the label is text on the recording — never a `Contact`. The + * write triggers the collection's `deriveSpeakerNames` hook, which is what the + * "Pessoa" facet filters on. + * + * Runs inside a transaction under an advisory lock of the recording and + * re-reads the labels after acquiring it: two curators labeling different + * clusters at the same time would otherwise each write back a stale array and + * silently lose one label. The lock closes that read-modify-write race. + */ +export const labelRecordingSpeakerForActor = async (input: { + recordingId: number + speakerKey: string + label: string +}): Promise<{ labeled: true }> => { + const parsed = recordingSpeakerLabelRequestSchema.parse(input) + const { payload, actor } = await getCampaignActionContext() + + if (!canReadCommunicationCatalog(actor.role)) throw new Error(RECORDING_FORBIDDEN_MESSAGE) + + const current = await loadRecordingForActor(payload, actor, parsed.recordingId) + if (!current) throw new Error(RECORDING_NOT_FOUND_MESSAGE) + + await withPayloadTransaction(payload, async ({ req }) => { + await acquireTextAdvisoryLocks(payload, req, [`recording-speakers:${parsed.recordingId}`]) + + const fresh = await payload.find({ + collection: 'recording', + where: { id: { equals: parsed.recordingId } }, + depth: 0, + limit: 1, + pagination: false, + select: { speakerLabels: true }, + req, + user: actor, + overrideAccess: false, + }) + const recording = fresh.docs[0] + if (!recording) throw new Error(RECORDING_NOT_FOUND_MESSAGE) + + const segment = await payload.find({ + collection: 'recordingSegment', + where: { + and: [ + { recording: { equals: parsed.recordingId } }, + { speakerKey: { equals: parsed.speakerKey } }, + ], + }, + depth: 0, + limit: 1, + pagination: false, + select: { speakerKey: true }, + req, + user: actor, + overrideAccess: false, + }) + if (!segment.docs[0]) throw new Error(RECORDING_SPEAKER_UNKNOWN_MESSAGE) + + const labels = [...(recording.speakerLabels ?? [])] + const index = labels.findIndex((entry) => entry.speakerKey === parsed.speakerKey) + const entry = { speakerKey: parsed.speakerKey, label: parsed.label } + if (index >= 0) labels[index] = entry + else labels.push(entry) + + await payload.update({ + collection: 'recording', + id: parsed.recordingId, + data: { speakerLabels: labels, speakerLabelsDropped: false }, + depth: 0, + select: recordingSelect, + req, + user: actor, + overrideAccess: false, + }) + }) + + return { labeled: true } +} + /** * Hard-deletes one recording: the row and its transcript go in one transaction * (the collection's `beforeDelete` cascade owns the segments); the private media diff --git a/src/collections/Recording.ts b/src/collections/Recording.ts index e54ca980..ac1620fd 100644 --- a/src/collections/Recording.ts +++ b/src/collections/Recording.ts @@ -1,13 +1,19 @@ -import type { CollectionBeforeDeleteHook, CollectionConfig } from 'payload' +import type { + CollectionBeforeDeleteHook, + CollectionBeforeValidateHook, + CollectionConfig, +} from 'payload' import { RECORDING_MEDIA_SLUG, + RECORDING_SPEAKER_LABEL_MAX_LENGTH, RECORDING_STATUSES, RECORDING_STEPS, RECORDING_TITLE_MAX_LENGTH, recordingStatusLabels, recordingStepLabels, } from '@/lib/recording' +import { speakerNamesFromLabels } from '@/lib/recordingDiarization' import { canDeleteRecording, canReadRecording, @@ -46,6 +52,21 @@ const deleteRecordingSegments: CollectionBeforeDeleteHook = async ({ id, req }) }) } +/** + * C200 — `speakerNames` is derived from the human labels so the "Pessoa" facet + * filters one denormalized array instead of joining segments. Mirrors + * `RecordingSegment.deriveSearchText`: `originalDoc` covers every update that + * does not touch the labels (status, searchText, step). + */ +const deriveSpeakerNames: CollectionBeforeValidateHook = ({ data, originalDoc }) => { + if (!data) return data + // An explicit array (even empty) is the new truth; only an untouched field + // falls back to the stored document. + const labels = data.speakerLabels === undefined ? originalDoc?.speakerLabels : data.speakerLabels + data.speakerNames = speakerNamesFromLabels(labels) + return data +} + export const Recording: CollectionConfig = { slug: 'recording', labels: { @@ -66,6 +87,7 @@ export const Recording: CollectionConfig = { delete: canDeleteRecording, }, hooks: { + beforeValidate: [deriveSpeakerNames], beforeChange: [stampCampaignCreatedBy], beforeDelete: [deleteRecordingSegments], }, @@ -133,6 +155,52 @@ export const Recording: CollectionConfig = { description: 'Concatenação normalizada dos segmentos (sem acentos, minúsculas).', }, }, + { + name: 'speakerLabels', + type: 'array', + label: 'Falantes identificados', + admin: { + readOnly: true, + description: + 'Rótulos humanos por agrupamento acústico (chave → nome). Identificação sempre humana; o acervo nunca sugere ou infere pessoas.', + }, + fields: [ + { + name: 'speakerKey', + type: 'text', + label: 'Agrupamento', + required: true, + }, + { + name: 'label', + type: 'text', + label: 'Nome do falante', + required: true, + maxLength: RECORDING_SPEAKER_LABEL_MAX_LENGTH, + }, + ], + }, + { + name: 'speakerNames', + type: 'text', + label: 'Pessoas (busca)', + hasMany: true, + admin: { + readOnly: true, + description: + 'Derivado dos rótulos (sem duplicatas, sem diferenciar maiúsculas) para a faceta "Pessoa".', + }, + }, + { + name: 'speakerLabelsDropped', + type: 'checkbox', + label: 'Identificações perdidas no reprocessamento', + admin: { + readOnly: true, + description: + 'Ligado quando um reprocessamento reordenou os agrupamentos e algum rótulo não pôde ser re-vinculado.', + }, + }, { name: 'error', type: 'textarea', diff --git a/src/collections/RecordingSegment.ts b/src/collections/RecordingSegment.ts index 3a9b3ed5..d94dbfeb 100644 --- a/src/collections/RecordingSegment.ts +++ b/src/collections/RecordingSegment.ts @@ -7,7 +7,9 @@ import { canReadRecording, payloadAdminOnly } from '@/utilities/campaignAccess' * C199 — searchable ASR segment of an uploaded recording: one timestamped block * of the transcript with the trigram-search index (`recording_segment_search_text_trgm_idx`, * hand-written migration). Admin-hidden: the product surface is the recording - * detail with its clickable transcript. + * detail with its clickable transcript. C200 adds `speakerKey`: the anonymous + * acoustic cluster ("Falante N") the segment was assigned to; null when the + * recording has no diarization (the detail renders the plain transcript then). */ const deriveSearchText: CollectionBeforeValidateHook = ({ data, originalDoc }) => { @@ -52,6 +54,15 @@ export const RecordingSegment: CollectionConfig = { label: 'Ordem', required: true, }, + { + name: 'speakerKey', + type: 'text', + label: 'Agrupamento de falante', + admin: { + readOnly: true, + description: 'Chave do agrupamento acústico ("Falante N") do trecho; vazio sem diarização.', + }, + }, { name: 'startSeconds', type: 'number', diff --git a/src/components/campaign/recording/RecordingDetailPlayer.tsx b/src/components/campaign/recording/RecordingDetailPlayer.tsx index 83da58b1..724dc4a7 100644 --- a/src/components/campaign/recording/RecordingDetailPlayer.tsx +++ b/src/components/campaign/recording/RecordingDetailPlayer.tsx @@ -1,14 +1,23 @@ 'use client' -import { CircleAlertIcon, DownloadIcon, PlayIcon, UploadIcon } from 'lucide-react' +import { CircleAlertIcon, DownloadIcon, InfoIcon, PlayIcon, UploadIcon } from 'lucide-react' import { useRef, useState } from 'react' import { RecordingRetryButton } from '@/components/campaign/recording/RecordingRetryButton' -import { SpeechHighlightParts } from '@/components/campaign/speech/SpeechHighlightParts' +import { RecordingSpeakerTranscript } from '@/components/campaign/recording/RecordingSpeakerTranscript' +import { RecordingTranscriptSegmentButton } from '@/components/campaign/recording/RecordingTranscriptSegmentButton' import { Button } from '@/components/ui/button' -import { canRetryRecording, type RecordingStatus } from '@/lib/recording' +import { + canRetryRecording, + RECORDING_SPEAKER_INFO_BANNER, + RECORDING_SPEAKER_INFO_BANNER_MOBILE, + type RecordingStatus, +} from '@/lib/recording' import { cn } from '@/lib/utils' -import type { RecordingDetailSegmentViewModel } from '@/utilities/recordings/recordingViewModels' +import type { + RecordingDetailSegmentViewModel, + RecordingSpeakerGroupViewModel, +} from '@/utilities/recordings/recordingViewModels' type RecordingDetailPlayerProps = { recordingId: number @@ -17,6 +26,10 @@ type RecordingDetailPlayerProps = { downloadHref: string failureMessage: string | null segments: readonly RecordingDetailSegmentViewModel[] + /** C200 — groups when the whole transcript is keyed; empty otherwise. */ + speakerGroups: readonly RecordingSpeakerGroupViewModel[] + /** C200 — true when a reprocessing dropped some identification. */ + speakerLabelsDropped: boolean /** `?t=` of the search hit; null when the detail was opened without a term. */ initialSeconds: number | null } @@ -35,6 +48,8 @@ export const RecordingDetailPlayer = ({ downloadHref, failureMessage, segments, + speakerGroups, + speakerLabelsDropped, initialSeconds, }: RecordingDetailPlayerProps) => { const videoRef = useRef(null) @@ -48,6 +63,7 @@ export const RecordingDetailPlayer = ({ const [retrying, setRetrying] = useState(false) const hasFile = status !== 'uploading' const showTranscript = status === 'ready' && segments.length > 0 + const grouped = speakerGroups.length > 0 const seekTo = (seconds: number) => { const video = videoRef.current @@ -157,6 +173,16 @@ export const RecordingDetailPlayer = ({

      ) : null} + {grouped ? ( +
      +
      + ) : null} + {status === 'ready' && segments.length === 0 ? (

      A transcrição desta gravação está vazia — o arquivo continua disponível para download. @@ -178,48 +204,44 @@ export const RecordingDetailPlayer = ({ return (

      {mediaColumn} -
      -
      -

      - Transcrição -

      - Clique para posicionar -
      -
        - {segments.map((segment) => { - const active = activeStart === segment.startSeconds - return ( + {grouped ? ( + + ) : ( +
        +
        +

        + Transcrição +

        + Clique para posicionar +
        +
          + {segments.map((segment) => (
        1. - +
        2. - ) - })} -
        -
        + ))} +
      +
      + )}
      ) } diff --git a/src/components/campaign/recording/RecordingPersonFilter.tsx b/src/components/campaign/recording/RecordingPersonFilter.tsx new file mode 100644 index 00000000..aab8dcbe --- /dev/null +++ b/src/components/campaign/recording/RecordingPersonFilter.tsx @@ -0,0 +1,87 @@ +'use client' + +import { FunnelIcon, XIcon } from 'lucide-react' +import { useOptimistic } from 'react' + +import { + CampaignHeaderFilterPopover, + type CampaignHeaderFilterRow, +} from '@/components/campaign/shared/CampaignHeaderFilterPopover' +import { useCampaignListFilterNavigation } from '@/components/campaign/shared/useCampaignListFilterNavigation' +import { foldSpeakerName } from '@/lib/recordingDiarization' +import { + buildRecordingListHref, + removeRecordingPerson, + toggleRecordingPerson, + type RecordingListState, +} from '@/utilities/recordings/recordingListUrl' + +const PERSON_FILTER_ID = 'recording-filter-person' + +/** + * C200 — the "Pessoa" facet of the uploaded recordings (design scene 3): one + * removable chip per selected label plus the multi-select popover backed by the + * labels actually present. Options come from the server (`speakerNames`), never + * from a free-form directory — the label is text, not a `Contact`. + */ +export const RecordingPersonFilter = ({ + state, + options, +}: { + state: RecordingListState + options: readonly string[] +}) => { + const { navigate } = useCampaignListFilterNavigation({ + state, + toHref: (next) => buildRecordingListHref(next, 1), + }) + const [viewState, setOptimisticState] = useOptimistic(state) + const selected = viewState.people ?? [] + + const optionRows: CampaignHeaderFilterRow[] = options.map((option) => { + const next = toggleRecordingPerson(viewState, option) + return { + value: option, + label: option, + href: buildRecordingListHref(next, 1), + selected: selected.some((person) => foldSpeakerName(person) === foldSpeakerName(option)), + checkbox: true, + onChoose: () => setOptimisticState(next), + } + }) + + const clear = { + href: buildRecordingListHref({ ...viewState, page: 1, people: undefined }, 1), + onChoose: () => setOptimisticState({ ...viewState, page: 1, people: undefined }), + } + + return ( +
      + 1 ? `Pessoa: ${selected.length}` : 'Pessoa'} + active={selected.length > 0} + closeOnChoose={false} + emptyLabel="Nenhuma pessoa identificada nas gravações." + optionRows={optionRows} + clear={selected.length ? clear : undefined} + /> + + {selected.map((person) => ( + + ))} +
      + ) +} diff --git a/src/components/campaign/recording/RecordingResultCard.tsx b/src/components/campaign/recording/RecordingResultCard.tsx index fde414e0..52889dbb 100644 --- a/src/components/campaign/recording/RecordingResultCard.tsx +++ b/src/components/campaign/recording/RecordingResultCard.tsx @@ -1,4 +1,4 @@ -import { ChevronRightIcon, VideoIcon } from 'lucide-react' +import { ChevronRightIcon, UserIcon, VideoIcon } from 'lucide-react' import Link from 'next/link' import { RecordingStatusBadge } from '@/components/campaign/recording/RecordingStatusBadge' @@ -65,6 +65,21 @@ export const RecordingResultCard = ({ recording }: { recording: RecordingListIte {STATE_COPY[recording.status]}

      )} + + {recording.matchedPersons.length > 0 ? ( +
      + {recording.matchedPersons.map((person) => ( + + + ))} + aparece nesta gravação +
      + ) : null} + + + + + {RECORDING_SPEAKER_DIALOG_TITLE} + Dê um nome ao agrupamento “{group.defaultLabel}”. + + +
      +
      + + { + event.preventDefault() + void submit() + }} + > + + setLabel(event.target.value)} + maxLength={RECORDING_SPEAKER_LABEL_MAX_LENGTH} + placeholder={RECORDING_SPEAKER_LABEL_PLACEHOLDER} + className="min-h-11 text-sm" + autoFocus + disabled={submitting} + /> +

      {RECORDING_SPEAKER_DIALOG_HELP}

      + + {error ? ( + + {error} + + ) : null} + +
      + + +
      + +
      + + ) +} diff --git a/src/components/campaign/recording/RecordingSpeakerTranscript.tsx b/src/components/campaign/recording/RecordingSpeakerTranscript.tsx new file mode 100644 index 00000000..1dfca065 --- /dev/null +++ b/src/components/campaign/recording/RecordingSpeakerTranscript.tsx @@ -0,0 +1,103 @@ +'use client' + +import { InfoIcon, UserIcon } from 'lucide-react' + +import { RecordingSpeakerDialog } from '@/components/campaign/recording/RecordingSpeakerDialog' +import { RecordingTranscriptSegmentButton } from '@/components/campaign/recording/RecordingTranscriptSegmentButton' +import { + RECORDING_SPEAKER_BANNER_BODY, + RECORDING_SPEAKER_BANNER_DROPPED, + RECORDING_SPEAKER_BANNER_TITLE, + RECORDING_SPEAKER_GROUP_IDENTIFIED, + RECORDING_SPEAKER_GROUP_UNIDENTIFIED, +} from '@/lib/recording' +import { cn } from '@/lib/utils' +import type { RecordingSpeakerGroupViewModel } from '@/utilities/recordings/recordingViewModels' + +type RecordingSpeakerTranscriptProps = { + recordingId: number + groups: readonly RecordingSpeakerGroupViewModel[] + labelsDropped: boolean + /** `?t=` search hit or the segment under the player's current time. */ + activeStart: number | null + onSeek: (seconds: number) => void +} + +/** + * C200 — the grouped transcript of the recording detail (design scenes 1, 4 and + * 5): anonymous clusters in first-appearance order, the always-visible + * imprecision warning (the grouping is automatic; the team identifies) and the + * "Identificar falante" affordance per group. + */ +export const RecordingSpeakerTranscript = ({ + recordingId, + groups, + labelsDropped, + activeStart, + onSeek, +}: RecordingSpeakerTranscriptProps) => ( +
      +
      +
      + +
      +

      + Transcrição por falante +

      + + Toque para posicionar + Clique para posicionar + +
      + +
      + {groups.map((group) => ( +
      +
      +
      + + +
      +
      + {group.label ?? group.defaultLabel} +
      +

      + {group.isIdentified + ? RECORDING_SPEAKER_GROUP_IDENTIFIED + : RECORDING_SPEAKER_GROUP_UNIDENTIFIED} +

      +
      +
      + +
      + + {group.segments.map((segment) => ( + + ))} +
      + ))} +
      +
      +) diff --git a/src/components/campaign/recording/RecordingTranscriptSegmentButton.tsx b/src/components/campaign/recording/RecordingTranscriptSegmentButton.tsx new file mode 100644 index 00000000..a0d4cab9 --- /dev/null +++ b/src/components/campaign/recording/RecordingTranscriptSegmentButton.tsx @@ -0,0 +1,51 @@ +'use client' + +import { SpeechHighlightParts } from '@/components/campaign/speech/SpeechHighlightParts' +import { cn } from '@/lib/utils' +import type { RecordingDetailSegmentViewModel } from '@/utilities/recordings/recordingViewModels' + +/** + * C200 — the clickable transcript line shared by the plain C199 list and the + * grouped C200 blocks: seeks the player to the segment and renders the search + * highlight. `variant` keeps the two approved paddings/sizes without a twin. + */ +export const RecordingTranscriptSegmentButton = ({ + segment, + active, + variant = 'plain', + onSeek, +}: { + segment: RecordingDetailSegmentViewModel + active: boolean + variant?: 'plain' | 'grouped' + onSeek: (seconds: number) => void +}) => ( + +) diff --git a/src/lib/campaignPaths.ts b/src/lib/campaignPaths.ts index 9d686958..3305e219 100644 --- a/src/lib/campaignPaths.ts +++ b/src/lib/campaignPaths.ts @@ -73,4 +73,8 @@ export const campaignRecordingRetryHref = (id: number): string => export const campaignRecordingDeleteHref = (id: number): string => `${campaignRecordingDetailHref(id)}/apagar` +/** C200 — human identification of one speaker cluster of a recording. */ +export const campaignRecordingSpeakersHref = (id: number): string => + `${campaignRecordingDetailHref(id)}/falantes` + export const CAMPAIGN_UPDATES_HREF = '/campanha/atualizacoes' as const diff --git a/src/lib/recording.ts b/src/lib/recording.ts index a2e3da47..eed402a4 100644 --- a/src/lib/recording.ts +++ b/src/lib/recording.ts @@ -30,6 +30,11 @@ export const isRecordingStatus = (value: unknown): value is RecordingStatus => /** One owner for the persisted field limits (collection, schema and dialog). */ export const RECORDING_TITLE_MAX_LENGTH = 200 +/** C200 — the human label the team types for one speaker cluster. */ +export const RECORDING_SPEAKER_LABEL_MAX_LENGTH = 120 +export const RECORDING_SPEAKER_LABEL_REQUIRED_MESSAGE = 'Informe o nome do falante.' +export const RECORDING_SPEAKER_LABEL_LONG_MESSAGE = `O nome do falante deve ter até ${RECORDING_SPEAKER_LABEL_MAX_LENGTH} caracteres.` + /** Upload ceiling: hours of plenary are the real case, 4 GiB is the guard. */ export const RECORDING_MAX_BYTES = 4 * 1024 * 1024 * 1024 export const RECORDING_MAX_SIZE_LABEL = '4 GB' @@ -90,6 +95,31 @@ export const canServeRecordingMedia = (status: RecordingStatus): boolean => stat /** The retry button only applies to a failed transcription. */ export const canRetryRecording = (status: RecordingStatus): boolean => status === 'failed' +/** C200 — dialog copy of the human identification step (design scenes 2/5). */ +export const RECORDING_SPEAKER_DIALOG_TITLE = 'Identificar falante' +export const RECORDING_SPEAKER_DIALOG_INTRO = + 'Ouça um trecho e informe quem é a pessoa. O acervo não sugere nem identifica pessoas automaticamente.' +export const RECORDING_SPEAKER_DIALOG_HELP = + 'O nome será aplicado a todas as falas deste agrupamento nesta gravação.' +export const RECORDING_SPEAKER_LABEL_FIELD_LABEL = 'Nome do falante' +export const RECORDING_SPEAKER_LABEL_PLACEHOLDER = 'Digite o nome do falante' +export const RECORDING_SPEAKER_SAVE_LABEL = 'Salvar identificação' + +/** C200 — the transcript banner of the grouped detail (design scenes 1/4/5). */ +export const RECORDING_SPEAKER_INFO_BANNER = + 'Os falantes são agrupados automaticamente pelo áudio. A identificação de cada pessoa é feita pela equipe.' +export const RECORDING_SPEAKER_INFO_BANNER_MOBILE = + 'Agrupamento automático e aproximado. A identificação é feita pela equipe.' +export const RECORDING_SPEAKER_BANNER_TITLE = 'A separação por falante pode estar imprecisa' +export const RECORDING_SPEAKER_BANNER_BODY = + 'O agrupamento é automático e aproximado. Ruído e falas sobrepostas podem colocar trechos no agrupamento errado. A equipe continua responsável por identificar cada falante.' +export const RECORDING_SPEAKER_BANNER_DROPPED = + 'Esta gravação foi reprocessada e algumas identificações não puderam ser mantidas. Confira os falantes.' +export const RECORDING_SPEAKER_GROUP_UNIDENTIFIED = 'Agrupamento sem identificação' +export const RECORDING_SPEAKER_GROUP_IDENTIFIED = 'Identificação feita pela equipe' +export const RECORDING_SPEAKER_IDENTIFY_ACTION = 'Identificar falante' +export const RECORDING_SPEAKER_EDIT_ACTION = 'Editar identificação' + /** Errors of the upload form (client validation). Wire messages live in schemas. */ export const RECORDING_FILE_REQUIRED_MESSAGE = 'Escolha um arquivo de vídeo para enviar.' export const RECORDING_TITLE_REQUIRED_MESSAGE = 'Informe um título para a gravação.' diff --git a/src/lib/recordingDiarization.ts b/src/lib/recordingDiarization.ts new file mode 100644 index 00000000..91b79ce1 --- /dev/null +++ b/src/lib/recordingDiarization.ts @@ -0,0 +1,241 @@ +/** + * C200 — pure speaker-grouping rules of an uploaded recording. The diarization + * provider returns anonymous turns (acoustic clusters); this module assigns + * each transcript segment to the turn it overlaps and numbers the clusters + * globally by first appearance. Identification is human: nothing here matches a + * voice to a person — the labels are text the team types. + * + * No I/O and no `server-only`: the job, the view models and the unit tests + * share this module. `reconcileSpeakerLabels` is the reprocessing contract: a + * new run cannot re-associate a voice (no biometrics), so a label survives only + * when an old cluster maps 1:1 to a new one by time overlap; the rest is + * reported as dropped so the detail can warn instead of silently forgetting. + */ + +/** One anonymous speaker turn as the provider returns it, in absolute seconds. */ +export type DiarizedTurn = { + speaker: string + startSeconds: number + endSeconds: number +} + +/** A segment with the cluster key it was assigned to; null if no turn covers it. */ +export type SpeakerKeyed = { + speakerKey: string | null +} + +/** A label the team typed for one cluster key of one recording. */ +export type RecordingSpeakerLabel = { + speakerKey: string + label: string +} + +/** A span used by the alignment/reconciliation math (segment or cluster). */ +export type TimedSpan = { + startSeconds: number + endSeconds: number +} + +/** + * Overlap in seconds between two spans; zero when they do not touch. A + * degenerate span (end before start) is clamped to its start, never negative. + */ +const overlapSeconds = (a: TimedSpan, b: TimedSpan): number => + Math.max(0, Math.min(a.endSeconds, b.endSeconds) - Math.max(a.startSeconds, b.startSeconds)) + +/** Gap in seconds between two spans; zero when they overlap or touch. */ +const gapSeconds = (a: TimedSpan, b: TimedSpan): number => + Math.max(0, Math.max(a.startSeconds, b.startSeconds) - Math.min(a.endSeconds, b.endSeconds)) + +/** Closest boundary distance used to break overlap ties by midpoint proximity. */ +const midpointDistance = (a: TimedSpan, b: TimedSpan): number => + Math.abs((a.startSeconds + a.endSeconds) / 2 - (b.startSeconds + b.endSeconds) / 2) + +/** + * Picks the turn of one segment: the largest overlap wins, ties go to the turn + * whose midpoint sits closest to the segment's, and a segment that overlaps no + * turn at all falls back to the nearest turn by gap (the audio is contiguous, + * so an isolated segment belongs to the closest voice). Invalid turns are + * ignored by the caller. + */ +const pickTurn = (segment: TimedSpan, turns: readonly DiarizedTurn[]): number => { + let best = -1 + let bestOverlap = 0 + let bestDistance = Number.POSITIVE_INFINITY + for (const [turnIndex, turn] of turns.entries()) { + const overlap = overlapSeconds(segment, turn) + const distance = midpointDistance(segment, turn) + const wins = + overlap > bestOverlap || (overlap > 0 && overlap === bestOverlap && distance < bestDistance) + if (wins) { + best = turnIndex + bestOverlap = overlap + bestDistance = distance + } + } + if (bestOverlap > 0) return best + + let nearest = 0 + let nearestGap = Number.POSITIVE_INFINITY + for (const [turnIndex, turn] of turns.entries()) { + const gap = gapSeconds(segment, turn) + if (gap < nearestGap) { + nearest = turnIndex + nearestGap = gap + } + } + return nearest +} + +/** + * Assigns each segment a `speaker-N` key following the acoustic turns. The + * numbering is global to the recording and follows the first appearance of the + * cluster in segment order — two runs over the same audio produce the same + * keys, and a segment with no usable turn keeps a null key (the detail renders + * the plain transcript then). Pure: tests do not touch the provider. + */ +export const assignSpeakerKeys = ( + segments: readonly Segment[], + turns: readonly DiarizedTurn[], +): (Segment & SpeakerKeyed)[] => { + const validTurns = turns.filter( + (turn) => + typeof turn.speaker === 'string' && + turn.speaker.length > 0 && + Number.isFinite(turn.startSeconds) && + Number.isFinite(turn.endSeconds), + ) + if (validTurns.length === 0) { + return segments.map((segment) => ({ ...segment, speakerKey: null })) + } + + const clusterKeys = new Map() + const keyOfTurn = (turn: DiarizedTurn): string => { + const existing = clusterKeys.get(turn.speaker) + if (existing) return existing + const key = `speaker-${clusterKeys.size + 1}` + clusterKeys.set(turn.speaker, key) + return key + } + + return segments.map((segment) => { + const turn = validTurns[pickTurn(segment, validTurns)] + return { ...segment, speakerKey: turn ? keyOfTurn(turn) : null } + }) +} + +/** First-appearance order of the keys in a keyed segment list. */ +export const speakerKeysInOrder = (segments: readonly SpeakerKeyed[]): string[] => { + const keys: string[] = [] + const seen = new Set() + for (const segment of segments) { + if (!segment.speakerKey || seen.has(segment.speakerKey)) continue + seen.add(segment.speakerKey) + keys.push(segment.speakerKey) + } + return keys +} + +/** + * Reprocessing contract (D6): a previous label is kept only on a mutual 1:1 + * time-overlap match — one old cluster maps to exactly one new cluster and that + * new cluster receives exactly one old label. A split (one old cluster became + * two) or a merge (two old clusters became one) is uncertainty, so the affected + * labels are dropped and reported instead of naming the wrong voice. Output + * labels follow the new segment order. + */ +export const reconcileSpeakerLabels = ({ + previousLabels, + previousSegments, + nextSegments, +}: { + previousLabels: readonly RecordingSpeakerLabel[] + previousSegments: readonly (TimedSpan & SpeakerKeyed)[] + nextSegments: readonly (TimedSpan & SpeakerKeyed)[] +}): { labels: RecordingSpeakerLabel[]; dropped: boolean } => { + if (previousLabels.length === 0) return { labels: [], dropped: false } + + const nextKeys = speakerKeysInOrder(nextSegments) + if (nextKeys.length === 0) return { labels: [], dropped: true } + + const previousKeys = speakerKeysInOrder(previousSegments) + const overlapByPair = new Map() + for (const previous of previousSegments) { + if (!previous.speakerKey) continue + for (const next of nextSegments) { + if (!next.speakerKey) continue + const pair = `${previous.speakerKey}\u0000${next.speakerKey}` + overlapByPair.set(pair, (overlapByPair.get(pair) ?? 0) + overlapSeconds(previous, next)) + } + } + + const labelByPreviousKey = new Map(previousLabels.map((entry) => [entry.speakerKey, entry.label])) + const claimantsOfNextKey = (nextKey: string): string[] => + previousKeys.filter( + (previousKey) => (overlapByPair.get(`${previousKey}\u0000${nextKey}`) ?? 0) > 0, + ) + + const kept = new Map() + for (const nextKey of nextKeys) { + const claimants = claimantsOfNextKey(nextKey) + const [previousKey] = claimants + if (claimants.length !== 1 || !previousKey) continue + const label = labelByPreviousKey.get(previousKey) + if (!label) continue + // Mutual uniqueness: the same old cluster must not also map elsewhere. + const matchesOfPrevious = nextKeys.filter( + (candidate) => (overlapByPair.get(`${previousKey}\u0000${candidate}`) ?? 0) > 0, + ) + if (matchesOfPrevious.length !== 1) continue + kept.set(nextKey, label) + } + + const labels = [...kept.entries()].map(([speakerKey, label]) => ({ speakerKey, label })) + return { labels, dropped: previousLabels.length > labels.length } +} + +/** + * Distinct labels of a recording, case-insensitive and trimmed — the + * denormalized `speakerNames` used by the "Pessoa" facet. Order of first + * appearance; empty labels never count. + */ +export const speakerNamesFromLabels = ( + labels: readonly RecordingSpeakerLabel[] | null | undefined, +): string[] => { + const names: string[] = [] + const seen = new Set() + for (const entry of labels ?? []) { + const name = typeof entry?.label === 'string' ? entry.label.trim() : '' + if (!name) continue + const folded = foldSpeakerName(name) + if (seen.has(folded)) continue + seen.add(folded) + names.push(name) + } + return names +} + +/** The default public name of one cluster: always numbered, never a person. */ +export const recordingSpeakerDefaultLabel = (index: number): string => `Falante ${index}` + +/** + * Case-fold used by every speaker-name comparison: the facet query is + * case-insensitive (ILIKE), so parsing, dedupe and chip matching share ONE + * fold to never disagree. + */ +export const foldSpeakerName = (value: string): string => value.toLocaleLowerCase('pt-BR') + +/** + * Case-insensitive containment of one selected "Pessoa" term in one label — + * the same match the facet query runs (`contains` → ILIKE), so the card chip + * and the result set never disagree. + */ +export const speakerNameMatches = (name: string, filter: string): boolean => + foldSpeakerName(name).includes(foldSpeakerName(filter)) + +/** + * Provider ceiling for one diarization call (AssemblyAI accepts up to 10 h per + * file). Longer recordings skip the step and keep the honest single-block + * transcript instead of guessing speakers. + */ +export const RECORDING_DIARIZATION_MAX_SECONDS = 10 * 60 * 60 diff --git a/src/lib/recordingTranscription.ts b/src/lib/recordingTranscription.ts index 084c9b15..6703f29c 100644 --- a/src/lib/recordingTranscription.ts +++ b/src/lib/recordingTranscription.ts @@ -34,17 +34,11 @@ export type RecordingTranscriptSegment = { } /** - * Extracts a mono 16 kHz MP3 and splits it into fixed chunks in one ffmpeg - * pass. Args are returned as an array — never a shell string; `outputPattern` - * carries the `%03d` sequence ffmpeg replaces per chunk. + * The shared extraction prefix of both ffmpeg passes: mono 16 kHz MP3, at the + * bitrate the ASR provider and the diarization provider both accept. Args are + * returned as an array — never a shell string. */ -export const buildRecordingAudioFfmpegArgs = ({ - inputPath, - outputPattern, -}: { - inputPath: string - outputPattern: string -}): string[] => [ +const recordingAudioArgs = (inputPath: string): string[] => [ '-nostdin', '-hide_banner', '-y', @@ -59,6 +53,20 @@ export const buildRecordingAudioFfmpegArgs = ({ 'libmp3lame', '-b:a', RECORDING_AUDIO_BITRATE, +] + +/** + * Extracts a mono 16 kHz MP3 and splits it into fixed chunks in one ffmpeg + * pass. `outputPattern` carries the `%03d` sequence ffmpeg replaces per chunk. + */ +export const buildRecordingAudioFfmpegArgs = ({ + inputPath, + outputPattern, +}: { + inputPath: string + outputPattern: string +}): string[] => [ + ...recordingAudioArgs(inputPath), '-f', 'segment', '-segment_time', @@ -68,6 +76,20 @@ export const buildRecordingAudioFfmpegArgs = ({ outputPattern, ] +/** + * Extracts the whole recording as one mono 16 kHz MP3 (C200): the diarization + * provider needs the entire audio in a single call so the speaker numbering is + * global — a per-chunk run would renumber speakers that never relate across + * chunks. Same audio parameters as the chunked pass. + */ +export const buildRecordingFullAudioFfmpegArgs = ({ + inputPath, + outputPath, +}: { + inputPath: string + outputPath: string +}): string[] => [...recordingAudioArgs(inputPath), outputPath] + /** * Merges every chunk into one ordered transcript: each segment is shifted by * its chunk offset, empty text is dropped and a degenerate span (end before diff --git a/src/lib/schemas/recording.ts b/src/lib/schemas/recording.ts index 201fef58..e574776c 100644 --- a/src/lib/schemas/recording.ts +++ b/src/lib/schemas/recording.ts @@ -1,6 +1,12 @@ import { z } from 'zod' -import { RECORDING_TITLE_MAX_LENGTH, RECORDING_TITLE_REQUIRED_MESSAGE } from '@/lib/recording' +import { + RECORDING_SPEAKER_LABEL_LONG_MESSAGE, + RECORDING_SPEAKER_LABEL_MAX_LENGTH, + RECORDING_SPEAKER_LABEL_REQUIRED_MESSAGE, + RECORDING_TITLE_MAX_LENGTH, + RECORDING_TITLE_REQUIRED_MESSAGE, +} from '@/lib/recording' import { trimmedOptionalText } from '@/lib/schemas/primitives' /** The acervo gate refused the actor — one literal shared with the vertical. */ @@ -49,6 +55,23 @@ export const recordingRetryRequestSchema = z.object({ recordingId: z.number().int().positive(), }) +/** C200 — the team names one acoustic cluster of a recording. */ +export const RECORDING_SPEAKER_UNKNOWN_MESSAGE = + 'Este agrupamento de falantes não existe mais nesta gravação.' + +export const recordingSpeakerLabelRequestSchema = z.object({ + recordingId: z.number().int().positive(), + speakerKey: z + .string() + .trim() + .regex(/^speaker-\d{1,3}$/, RECORDING_SPEAKER_UNKNOWN_MESSAGE), + label: z + .string() + .trim() + .min(1, RECORDING_SPEAKER_LABEL_REQUIRED_MESSAGE) + .max(RECORDING_SPEAKER_LABEL_MAX_LENGTH, RECORDING_SPEAKER_LABEL_LONG_MESSAGE), +}) + /** Delete one recording (row + segments + private media). */ export const recordingDeleteRequestSchema = z.object({ recordingId: z.number().int().positive(), diff --git a/src/migrations/20260919_032729_add_recording_speakers.json b/src/migrations/20260919_032729_add_recording_speakers.json new file mode 100644 index 00000000..57fe3c41 --- /dev/null +++ b/src/migrations/20260919_032729_add_recording_speakers.json @@ -0,0 +1,16760 @@ +{ + "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.reel": { + "name": "reel", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "feature": { + "name": "feature", + "type": "enum_reel_feature", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source_hash": { + "name": "source_hash", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "enum_reel_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "video_id": { + "name": "video_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "video_with_audio_id": { + "name": "video_with_audio_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "narration_audio_id": { + "name": "narration_audio_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "captions_id": { + "name": "captions_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cover_id": { + "name": "cover_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "transcript": { + "name": "transcript", + "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": { + "reel_source_hash_idx": { + "name": "reel_source_hash_idx", + "columns": [ + { + "expression": "source_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reel_status_idx": { + "name": "reel_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reel_video_idx": { + "name": "reel_video_idx", + "columns": [ + { + "expression": "video_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reel_video_with_audio_idx": { + "name": "reel_video_with_audio_idx", + "columns": [ + { + "expression": "video_with_audio_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reel_narration_audio_idx": { + "name": "reel_narration_audio_idx", + "columns": [ + { + "expression": "narration_audio_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reel_captions_idx": { + "name": "reel_captions_idx", + "columns": [ + { + "expression": "captions_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reel_cover_idx": { + "name": "reel_cover_idx", + "columns": [ + { + "expression": "cover_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reel_created_by_idx": { + "name": "reel_created_by_idx", + "columns": [ + { + "expression": "created_by_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reel_updated_at_idx": { + "name": "reel_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reel_created_at_idx": { + "name": "reel_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reel_video_id_reel_media_id_fk": { + "name": "reel_video_id_reel_media_id_fk", + "tableFrom": "reel", + "tableTo": "reel_media", + "columnsFrom": [ + "video_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "reel_video_with_audio_id_reel_media_id_fk": { + "name": "reel_video_with_audio_id_reel_media_id_fk", + "tableFrom": "reel", + "tableTo": "reel_media", + "columnsFrom": [ + "video_with_audio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "reel_narration_audio_id_reel_media_id_fk": { + "name": "reel_narration_audio_id_reel_media_id_fk", + "tableFrom": "reel", + "tableTo": "reel_media", + "columnsFrom": [ + "narration_audio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "reel_captions_id_reel_media_id_fk": { + "name": "reel_captions_id_reel_media_id_fk", + "tableFrom": "reel", + "tableTo": "reel_media", + "columnsFrom": [ + "captions_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "reel_cover_id_reel_media_id_fk": { + "name": "reel_cover_id_reel_media_id_fk", + "tableFrom": "reel", + "tableTo": "reel_media", + "columnsFrom": [ + "cover_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "reel_created_by_id_campaign_user_id_fk": { + "name": "reel_created_by_id_campaign_user_id_fk", + "tableFrom": "reel", + "tableTo": "campaign_user", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reel_media": { + "name": "reel_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": { + "reel_media_updated_at_idx": { + "name": "reel_media_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reel_media_created_at_idx": { + "name": "reel_media_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reel_media_filename_idx": { + "name": "reel_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.recording_speaker_labels": { + "name": "recording_speaker_labels", + "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 + }, + "speaker_key": { + "name": "speaker_key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "recording_speaker_labels_order_idx": { + "name": "recording_speaker_labels_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "recording_speaker_labels_parent_id_idx": { + "name": "recording_speaker_labels_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recording_speaker_labels_parent_id_fk": { + "name": "recording_speaker_labels_parent_id_fk", + "tableFrom": "recording_speaker_labels", + "tableTo": "recording", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recording": { + "name": "recording", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "recorded_at": { + "name": "recorded_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "enum_recording_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'uploading'" + }, + "step": { + "name": "step", + "type": "enum_recording_step", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "media_id": { + "name": "media_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "duration_seconds": { + "name": "duration_seconds", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "search_text": { + "name": "search_text", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "speaker_labels_dropped": { + "name": "speaker_labels_dropped", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "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": { + "recording_recorded_at_idx": { + "name": "recording_recorded_at_idx", + "columns": [ + { + "expression": "recorded_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "recording_status_idx": { + "name": "recording_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "recording_media_idx": { + "name": "recording_media_idx", + "columns": [ + { + "expression": "media_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "recording_created_by_idx": { + "name": "recording_created_by_idx", + "columns": [ + { + "expression": "created_by_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "recording_updated_at_idx": { + "name": "recording_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "recording_created_at_idx": { + "name": "recording_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recording_media_id_recording_media_id_fk": { + "name": "recording_media_id_recording_media_id_fk", + "tableFrom": "recording", + "tableTo": "recording_media", + "columnsFrom": [ + "media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "recording_created_by_id_campaign_user_id_fk": { + "name": "recording_created_by_id_campaign_user_id_fk", + "tableFrom": "recording", + "tableTo": "campaign_user", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recording_texts": { + "name": "recording_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": { + "recording_texts_order_parent": { + "name": "recording_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": { + "recording_texts_parent_fk": { + "name": "recording_texts_parent_fk", + "tableFrom": "recording_texts", + "tableTo": "recording", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recording_media": { + "name": "recording_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": { + "recording_media_updated_at_idx": { + "name": "recording_media_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "recording_media_created_at_idx": { + "name": "recording_media_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "recording_media_filename_idx": { + "name": "recording_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.recording_segment": { + "name": "recording_segment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "recording_id": { + "name": "recording_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "speaker_key": { + "name": "speaker_key", + "type": "varchar", + "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 + }, + "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": { + "recording_segment_recording_idx": { + "name": "recording_segment_recording_idx", + "columns": [ + { + "expression": "recording_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "recording_segment_updated_at_idx": { + "name": "recording_segment_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "recording_segment_created_at_idx": { + "name": "recording_segment_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recording_segment_recording_id_recording_id_fk": { + "name": "recording_segment_recording_id_recording_id_fk", + "tableFrom": "recording_segment", + "tableTo": "recording", + "columnsFrom": [ + "recording_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 + }, + "reel_id": { + "name": "reel_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "reel_media_id": { + "name": "reel_media_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "recording_id": { + "name": "recording_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "recording_media_id": { + "name": "recording_media_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "recording_segment_id": { + "name": "recording_segment_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_reel_id_idx": { + "name": "payload_locked_documents_rels_reel_id_idx", + "columns": [ + { + "expression": "reel_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_reel_media_id_idx": { + "name": "payload_locked_documents_rels_reel_media_id_idx", + "columns": [ + { + "expression": "reel_media_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_recording_id_idx": { + "name": "payload_locked_documents_rels_recording_id_idx", + "columns": [ + { + "expression": "recording_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_recording_media_id_idx": { + "name": "payload_locked_documents_rels_recording_media_id_idx", + "columns": [ + { + "expression": "recording_media_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_recording_segment_id_idx": { + "name": "payload_locked_documents_rels_recording_segment_id_idx", + "columns": [ + { + "expression": "recording_segment_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_reel_fk": { + "name": "payload_locked_documents_rels_reel_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "reel", + "columnsFrom": [ + "reel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_reel_media_fk": { + "name": "payload_locked_documents_rels_reel_media_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "reel_media", + "columnsFrom": [ + "reel_media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_recording_fk": { + "name": "payload_locked_documents_rels_recording_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "recording", + "columnsFrom": [ + "recording_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_recording_media_fk": { + "name": "payload_locked_documents_rels_recording_media_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "recording_media", + "columnsFrom": [ + "recording_media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_recording_segment_fk": { + "name": "payload_locked_documents_rels_recording_segment_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "recording_segment", + "columnsFrom": [ + "recording_segment_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_reel_feature": { + "name": "enum_reel_feature", + "schema": "public", + "values": [ + "cards" + ] + }, + "public.enum_reel_status": { + "name": "enum_reel_status", + "schema": "public", + "values": [ + "draft", + "published", + "unpublished" + ] + }, + "public.enum_recording_status": { + "name": "enum_recording_status", + "schema": "public", + "values": [ + "uploading", + "processing", + "ready", + "failed" + ] + }, + "public.enum_recording_step": { + "name": "enum_recording_step", + "schema": "public", + "values": [ + "extracting", + "transcribing", + "saving" + ] + }, + "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": "61a1f946-2455-495c-8629-2fa15e4151e6", + "prevId": "00000000-0000-0000-0000-000000000000" +} \ No newline at end of file diff --git a/src/migrations/20260919_032729_add_recording_speakers.ts b/src/migrations/20260919_032729_add_recording_speakers.ts new file mode 100644 index 00000000..b1f1463a --- /dev/null +++ b/src/migrations/20260919_032729_add_recording_speakers.ts @@ -0,0 +1,51 @@ +import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres' + +/** + * C200 — speaker grouping of the uploaded recordings: the anonymous cluster key + * on each segment, the human labels on the recording (one row per identified + * cluster) and the flag a reprocessing sets when a label could not be + * re-associated. `recording_texts` is Payload's join table for the derived + * `speakerNames` (`hasMany text`). + * + * The generated diff also carried `reel.source_hash` because the C199 snapshot + * (20260919_003440) was taken before the sibling `20260919_002605` landed — the + * column already exists in every environment, so that SQL was removed here by + * hand. The snapshot JSON of THIS migration is current, so the phantom diff + * does not come back. + */ +export async function up({ db, payload, req }: MigrateUpArgs): Promise { + await db.execute(sql` + CREATE TABLE "recording_speaker_labels" ( + "_order" integer NOT NULL, + "_parent_id" integer NOT NULL, + "id" varchar PRIMARY KEY NOT NULL, + "speaker_key" varchar NOT NULL, + "label" varchar NOT NULL + ); + + CREATE TABLE "recording_texts" ( + "id" serial PRIMARY KEY NOT NULL, + "order" integer NOT NULL, + "parent_id" integer NOT NULL, + "path" varchar NOT NULL, + "text" varchar + ); + + ALTER TABLE "recording" ADD COLUMN "speaker_labels_dropped" boolean; + ALTER TABLE "recording_segment" ADD COLUMN "speaker_key" varchar; + ALTER TABLE "recording_speaker_labels" ADD CONSTRAINT "recording_speaker_labels_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."recording"("id") ON DELETE cascade ON UPDATE no action; + ALTER TABLE "recording_texts" ADD CONSTRAINT "recording_texts_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."recording"("id") ON DELETE cascade ON UPDATE no action; + CREATE INDEX "recording_speaker_labels_order_idx" ON "recording_speaker_labels" USING btree ("_order"); + CREATE INDEX "recording_speaker_labels_parent_id_idx" ON "recording_speaker_labels" USING btree ("_parent_id"); + CREATE INDEX "recording_texts_order_parent" ON "recording_texts" USING btree ("order","parent_id");`) +} + +export async function down({ db, payload, req }: MigrateDownArgs): Promise { + await db.execute(sql` + ALTER TABLE "recording_speaker_labels" DISABLE ROW LEVEL SECURITY; + ALTER TABLE "recording_texts" DISABLE ROW LEVEL SECURITY; + DROP TABLE "recording_speaker_labels" CASCADE; + DROP TABLE "recording_texts" CASCADE; + ALTER TABLE "recording" DROP COLUMN "speaker_labels_dropped"; + ALTER TABLE "recording_segment" DROP COLUMN "speaker_key";`) +} diff --git a/src/migrations/index.ts b/src/migrations/index.ts index d84eb49e..ba7f40fe 100644 --- a/src/migrations/index.ts +++ b/src/migrations/index.ts @@ -75,6 +75,7 @@ import * as migration_20260915_142058_activity_google_import_link from './202609 import * as migration_20260918_201009_add_reel from './20260918_201009_add_reel'; import * as migration_20260919_002605_add_reel_source_hash from './20260919_002605_add_reel_source_hash'; import * as migration_20260919_003440_add_recording from './20260919_003440_add_recording'; +import * as migration_20260919_032729_add_recording_speakers from './20260919_032729_add_recording_speakers'; export const migrations = [ { @@ -455,11 +456,16 @@ export const migrations = [ { up: migration_20260919_002605_add_reel_source_hash.up, down: migration_20260919_002605_add_reel_source_hash.down, - name: '20260919_002605_add_reel_source_hash' + name: '20260919_002605_add_reel_source_hash', }, { up: migration_20260919_003440_add_recording.up, down: migration_20260919_003440_add_recording.down, - name: '20260919_003440_add_recording' + name: '20260919_003440_add_recording', + }, + { + up: migration_20260919_032729_add_recording_speakers.up, + down: migration_20260919_032729_add_recording_speakers.down, + name: '20260919_032729_add_recording_speakers' }, ]; diff --git a/src/payload-types.ts b/src/payload-types.ts index 9f83e547..241cc3db 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -1175,6 +1175,24 @@ export interface Recording { * Concatenação normalizada dos segmentos (sem acentos, minúsculas). */ searchText?: string | null; + /** + * Rótulos humanos por agrupamento acústico (chave → nome). Identificação sempre humana; o acervo nunca sugere ou infere pessoas. + */ + speakerLabels?: + | { + speakerKey: string; + label: string; + id?: string | null; + }[] + | null; + /** + * Derivado dos rótulos (sem duplicatas, sem diferenciar maiúsculas) para a faceta "Pessoa". + */ + speakerNames?: string[] | null; + /** + * Ligado quando um reprocessamento reordenou os agrupamentos e algum rótulo não pôde ser re-vinculado. + */ + speakerLabelsDropped?: boolean | null; /** * Motivo interno da falha; nunca vai à pessoa com o detalhe cru. */ @@ -1215,6 +1233,10 @@ export interface RecordingSegment { id: number; recording: number | Recording; order: number; + /** + * Chave do agrupamento acústico ("Falante N") do trecho; vazio sem diarização. + */ + speakerKey?: string | null; startSeconds: number; endSeconds: number; text: string; @@ -2425,6 +2447,15 @@ export interface RecordingSelect { media?: T; durationSeconds?: T; searchText?: T; + speakerLabels?: + | T + | { + speakerKey?: T; + label?: T; + id?: T; + }; + speakerNames?: T; + speakerLabelsDropped?: T; error?: T; createdBy?: T; updatedAt?: T; @@ -2455,6 +2486,7 @@ export interface RecordingMediaSelect { export interface RecordingSegmentSelect { recording?: T; order?: T; + speakerKey?: T; startSeconds?: T; endSeconds?: T; text?: T; diff --git a/src/utilities/ai/assemblyAiDiarize.ts b/src/utilities/ai/assemblyAiDiarize.ts new file mode 100644 index 00000000..cfc6ba0c --- /dev/null +++ b/src/utilities/ai/assemblyAiDiarize.ts @@ -0,0 +1,178 @@ +import 'server-only' + +import { openAsBlob } from 'node:fs' + +import type { DiarizedTurn } from '@/lib/recordingDiarization' + +/** + * C200 — the diarization provider: AssemblyAI's async pre-recorded API with + * `speaker_labels` (acoustic clustering only). The recording is sent as ONE + * file so the speaker numbering is global; the voice is never matched to a + * person and the account-level speaker identification feature is deliberately + * never enabled — naming is human (design scenes 1/2). + * + * Never throws: every failure becomes `{ ok: false }` and the transcription job + * degrades to the honest single-block transcript (no speaker grouping) instead + * of failing the recording. The remote transcript/audio is deleted best-effort + * after the read. + * + * Provider notes (verified 2026-09-19): Universal-2 at US$0,15/h + async + * diarization add-on US$0,02/h; up to 10 h / 5 GB per file; `speech_models` is + * the plural request field (the singular is deprecated). + */ + +export const ASSEMBLYAI_UPLOAD_URL = 'https://api.assemblyai.com/v2/upload' +export const ASSEMBLYAI_TRANSCRIPT_URL = 'https://api.assemblyai.com/v2/transcript' +const ASSEMBLYAI_MODEL = 'universal-2' +const ASSEMBLYAI_LANGUAGE = 'pt' + +const UPLOAD_TIMEOUT_MS = 10 * 60_000 +const POLL_REQUEST_TIMEOUT_MS = 30_000 +const DIARIZATION_DEADLINE_MS = 60 * 60_000 +const POLL_INTERVAL_MS = 5_000 + +const DIARIZE_ERROR = 'Não foi possível separar os falantes da gravação.' + +type DiarizeRequestOptions = { + /** Called after each poll — lets the job keep its heartbeat alive. */ + onProgress?: () => Promise | void + /** Poll cadence; only tests change it (default 5 s). */ + pollIntervalMs?: number +} + +type DiarizeResult = + | { ok: true; turns: DiarizedTurn[] } + | { ok: false; error: string; status: number } + +/** The seam the transcription job consumes; tests inject a fake. */ +export type SpeakerDiarizer = ( + file: Blob, + options?: DiarizeRequestOptions, +) => Promise + +const sleep = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)) + +const authHeaders = (apiKey: string, json = false): Record => ({ + authorization: apiKey, + ...(json ? { 'content-type': 'application/json' } : {}), +}) + +/** `A`→`speaker-A`… provider cluster ids stay opaque; the pure module numbers them. */ +const normalizeUtterances = (json: unknown): DiarizedTurn[] => { + const data = json && typeof json === 'object' ? (json as Record) : {} + const utterances = Array.isArray(data.utterances) ? data.utterances : [] + const turns: DiarizedTurn[] = [] + for (const raw of utterances) { + const utterance = raw && typeof raw === 'object' ? (raw as Record) : {} + const speaker = typeof utterance.speaker === 'string' ? utterance.speaker.trim() : '' + const startMs = Number(utterance.start) + const endMs = Number(utterance.end) + if (!speaker || !Number.isFinite(startMs) || !Number.isFinite(endMs)) continue + turns.push({ + speaker, + startSeconds: Math.max(0, startMs / 1000), + endSeconds: Math.max(0, endMs / 1000), + }) + } + return turns +} + +/** Best-effort delete of the remote transcript (privacy hygiene, never fatal). */ +const deleteRemoteTranscript = async (apiKey: string, transcriptId: string): Promise => { + try { + await fetch(`${ASSEMBLYAI_TRANSCRIPT_URL}/${transcriptId}`, { + method: 'DELETE', + headers: authHeaders(apiKey), + signal: AbortSignal.timeout(POLL_REQUEST_TIMEOUT_MS), + }) + } catch { + // Best-effort: a failed delete never affects the recording. + } +} + +/** + * Uploads the audio, submits the diarization transcript and polls until it is + * ready. `file` is a Blob opened from the extracted MP3 (the job owns the temp + * file); the provider call itself keeps no copy after the best-effort delete. + */ +export const assemblyAiDiarize: SpeakerDiarizer = async ( + file, + { onProgress, pollIntervalMs = POLL_INTERVAL_MS } = {}, +) => { + const apiKey = process.env.ASSEMBLYAI_API_KEY + if (!apiKey) { + return { ok: false, error: 'Separação de falantes está indisponível no momento.', status: 503 } + } + + let transcriptId: string | null = null + try { + const uploadResponse = await fetch(ASSEMBLYAI_UPLOAD_URL, { + method: 'POST', + headers: authHeaders(apiKey), + body: file, + signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MS), + }) + if (!uploadResponse.ok) return { ok: false, error: DIARIZE_ERROR, status: 502 } + + const uploaded = (await uploadResponse.json()) as { upload_url?: unknown } + const uploadUrl = typeof uploaded.upload_url === 'string' ? uploaded.upload_url : '' + if (!uploadUrl) return { ok: false, error: DIARIZE_ERROR, status: 502 } + + const createResponse = await fetch(ASSEMBLYAI_TRANSCRIPT_URL, { + method: 'POST', + headers: authHeaders(apiKey, true), + body: JSON.stringify({ + audio_url: uploadUrl, + language_code: ASSEMBLYAI_LANGUAGE, + speaker_labels: true, + speech_models: [ASSEMBLYAI_MODEL], + }), + signal: AbortSignal.timeout(POLL_REQUEST_TIMEOUT_MS), + }) + if (!createResponse.ok) return { ok: false, error: DIARIZE_ERROR, status: 502 } + + const created = (await createResponse.json()) as { id?: unknown; status?: unknown } + transcriptId = typeof created.id === 'string' ? created.id : null + if (!transcriptId) return { ok: false, error: DIARIZE_ERROR, status: 502 } + + const deadline = Date.now() + DIARIZATION_DEADLINE_MS + for (;;) { + await sleep(pollIntervalMs) + await onProgress?.() + + const pollResponse = await fetch(`${ASSEMBLYAI_TRANSCRIPT_URL}/${transcriptId}`, { + headers: authHeaders(apiKey), + signal: AbortSignal.timeout(POLL_REQUEST_TIMEOUT_MS), + }) + if (!pollResponse.ok) return { ok: false, error: DIARIZE_ERROR, status: 502 } + + const transcript = (await pollResponse.json()) as { status?: unknown } + if (transcript.status === 'completed') { + await deleteRemoteTranscript(apiKey, transcriptId) + return { ok: true, turns: normalizeUtterances(transcript) } + } + if (transcript.status === 'error') { + await deleteRemoteTranscript(apiKey, transcriptId) + return { ok: false, error: DIARIZE_ERROR, status: 502 } + } + if (Date.now() > deadline) { + await deleteRemoteTranscript(apiKey, transcriptId) + return { ok: false, error: DIARIZE_ERROR, status: 504 } + } + } + } catch { + if (transcriptId) await deleteRemoteTranscript(apiKey, transcriptId) + return { ok: false, error: DIARIZE_ERROR, status: 502 } + } +} + +/** + * The configured provider, or null when the key is absent. The job treats null + * as "no diarization step": the transcript is still saved, without groups. + */ +export const configuredSpeakerDiarizer = (): SpeakerDiarizer | null => + process.env.ASSEMBLYAI_API_KEY ? assemblyAiDiarize : null + +/** Opens the extracted MP3 as a Blob without loading hours of audio in memory. */ +export const diarizeInputBlob = (path: string): Promise => + openAsBlob(path, { type: 'audio/mpeg' }) diff --git a/src/utilities/recordings/recordingJob.ts b/src/utilities/recordings/recordingJob.ts index 62439d68..4d2c00ec 100644 --- a/src/utilities/recordings/recordingJob.ts +++ b/src/utilities/recordings/recordingJob.ts @@ -10,13 +10,24 @@ import { RECORDING_MEDIA_SLUG, type RecordingStep, } from '@/lib/recording' +import { + assignSpeakerKeys, + reconcileSpeakerLabels, + RECORDING_DIARIZATION_MAX_SECONDS, +} from '@/lib/recordingDiarization' import { buildRecordingAudioFfmpegArgs, + buildRecordingFullAudioFfmpegArgs, mergeChunkTranscriptions, RECORDING_AUDIO_CHUNK_SECONDS, recordingSearchText, } from '@/lib/recordingTranscription' import type { Recording } from '@/payload-types' +import { + configuredSpeakerDiarizer, + diarizeInputBlob, + type SpeakerDiarizer, +} from '@/utilities/ai/assemblyAiDiarize' import { deepInfraTranscribeSegments, type TranscribeSegmentsResult, @@ -107,12 +118,16 @@ const chunkFilesIn = async (tempDir: string): Promise => { /** * The whole pipeline of one recording. Never throws: an orphan job (server * restart) is covered by the lazy reaper, and any error marks the row `failed` - * so the retry reuses it. + * so the retry reuses it. C200 adds the optional diarization pass: with a + * configured provider the whole extracted audio is sent once, the turns are + * aligned to the whisper segments and the human labels are reconciled by time + * overlap; a provider failure degrades to the single-block transcript. */ export const runRecordingJob = async ( payload: Payload, recordingId: number, transcribe: ChunkTranscriber = deepInfraTranscribeSegments, + diarize: SpeakerDiarizer | null = configuredSpeakerDiarizer(), ): Promise => { let tempDir: string | null = null let currentStep: RecordingStep = 'extracting' @@ -155,6 +170,22 @@ export const runRecordingJob = async ( return } + // C200: the provider accepts up to 10 h per file; a longer recording skips + // the step honestly (a rough upper bound is enough to refuse, and the + // chunk count overestimates the real duration by at most one chunk). + const diarizationEligible = + diarize !== null && + chunkFiles.length * RECORDING_AUDIO_CHUNK_SECONDS <= RECORDING_DIARIZATION_MAX_SECONDS + const fullAudioPath = join(tempDir, 'full.mp3') + if (diarizationEligible) { + await runFfmpeg( + buildRecordingFullAudioFfmpegArgs({ inputPath, outputPath: fullAudioPath }), + RECORDING_AUDIO_CHUNK_SECONDS, + NO_AUDIO_MESSAGE, + RECORDING_FFMPEG_TIMEOUT_MS, + ) + } + await markStep('transcribing') const chunks: { offsetSeconds: number @@ -187,8 +218,63 @@ export const runRecordingJob = async ( return } + // C200 — optional speaker grouping. The provider seam never fails the + // recording: every failure (or a missing key) keeps the plain transcript. + let keyedSegments: ((typeof segments)[number] & { speakerKey: string | null })[] = segments.map( + (segment) => ({ ...segment, speakerKey: null }), + ) + if (diarizationEligible && diarize) { + try { + const result = await diarize(await diarizeInputBlob(fullAudioPath), { + onProgress: async () => { + await updateRecordingSystem(payload, recordingId, { step: 'transcribing' }) + }, + }) + if (result.ok) keyedSegments = assignSpeakerKeys(segments, result.turns) + } catch { + // Defensive: the provider contract is "never throws"; if it does, the + // transcript still gets saved without groups. + } + } + await markStep('saving') await withPayloadTransaction(payload, async ({ req }) => { + // Fresh reads inside the transaction: the labels a curator wrote while + // the transcription ran must be the ones reconciled, not the snapshot + // from the top of the job. + const freshRecording = await payload.find({ + collection: 'recording', + where: { id: { equals: recordingId } }, + depth: 0, + limit: 1, + pagination: false, + select: { speakerLabels: true }, + req, + // Intentional admin bypass: the pipeline owns the row state. + overrideAccess: true, + }) + const previous = await payload.find({ + collection: 'recordingSegment', + where: { recording: { equals: recordingId } }, + depth: 0, + limit: 0, + pagination: false, + sort: 'order', + select: { speakerKey: true, startSeconds: true, endSeconds: true }, + req, + // Intentional admin bypass: reconciling the transcript the job owns. + overrideAccess: true, + }) + const { labels, dropped } = reconcileSpeakerLabels({ + previousLabels: freshRecording.docs[0]?.speakerLabels ?? [], + previousSegments: previous.docs.map((segment) => ({ + speakerKey: segment.speakerKey ?? null, + startSeconds: segment.startSeconds, + endSeconds: segment.endSeconds, + })), + nextSegments: keyedSegments, + }) + await payload.delete({ collection: 'recordingSegment', where: { recording: { equals: recordingId } }, @@ -196,7 +282,7 @@ export const runRecordingJob = async ( // Intentional admin bypass: replacing segments owned by the recording. overrideAccess: true, }) - for (const segment of segments) { + for (const segment of keyedSegments) { await payload.create({ collection: 'recordingSegment', // `searchText` is filled by the collection's own beforeValidate hook. @@ -206,6 +292,7 @@ export const runRecordingJob = async ( startSeconds: segment.startSeconds, endSeconds: segment.endSeconds, text: segment.text, + speakerKey: segment.speakerKey, }), req, // Intentional admin bypass: the transcript belongs to the recording row. @@ -221,6 +308,8 @@ export const runRecordingJob = async ( error: null, durationSeconds: Math.round(durationSeconds), searchText: recordingSearchText(segments), + speakerLabels: labels, + speakerLabelsDropped: dropped, }, req, ) diff --git a/src/utilities/recordings/recordingListFilters.ts b/src/utilities/recordings/recordingListFilters.ts index e280bbf3..2ec8bef0 100644 --- a/src/utilities/recordings/recordingListFilters.ts +++ b/src/utilities/recordings/recordingListFilters.ts @@ -1,16 +1,29 @@ /** * C199 — Payload `where` of the uploaded-recordings list: one textual branch * over the normalized `recording.searchText` (the concatenation of the - * transcript segments). No facets: the Câmara facets do not apply to a - * recording. A recording still processing has no `searchText` and is honestly - * out of the search until the transcript is saved. + * transcript segments). C200 adds the "Pessoa" facet: any selected label that + * appears in the recording's derived `speakerNames`. A recording still + * processing has no `searchText` and is honestly out of the search until the + * transcript is saved. */ import type { Where } from 'payload' import { normalizeForSearch } from '@/lib/speechSearch' +import { collapseListWhereOrBranches } from '@/utilities/campaignListUrl' import type { RecordingListState } from '@/utilities/recordings/recordingListUrl' export const buildRecordingListWhere = (state: RecordingListState): Where => { + const branches: Where[] = [] + const normalized = normalizeForSearch(state.q ?? '') - return normalized ? { searchText: { like: normalized } } : {} + if (normalized) branches.push({ searchText: { like: normalized } }) + + const peopleBranch = collapseListWhereOrBranches( + (state.people ?? []).map((person) => ({ speakerNames: { contains: person } })), + ) + if (peopleBranch) branches.push(peopleBranch) + + if (branches.length === 0) return {} + if (branches.length === 1) return branches[0] ?? {} + return { and: branches } } diff --git a/src/utilities/recordings/recordingListUrl.ts b/src/utilities/recordings/recordingListUrl.ts index 31816486..bd1cc2b7 100644 --- a/src/utilities/recordings/recordingListUrl.ts +++ b/src/utilities/recordings/recordingListUrl.ts @@ -6,7 +6,10 @@ */ import { CAMPAIGN_COMMUNICATION_ACERVO } from '@/lib/campaignPaths' import { isContactSearchQueryReady } from '@/lib/contactSearchQuery' +import { RECORDING_SPEAKER_LABEL_MAX_LENGTH } from '@/lib/recording' +import { foldSpeakerName } from '@/lib/recordingDiarization' import { + allParamValues, buildListHref, firstValue, normalizedText, @@ -22,29 +25,52 @@ export type AcervoSource = 'camara' | 'enviadas' const ACERVO_SOURCE_PARAM = 'source' const ACERVO_SOURCE_ENVIADAS = 'enviadas' +const ACERVO_PERSON_PARAM = 'person' -const recordingListParamNames = [ACERVO_SOURCE_PARAM, 'q', 'page'] as const +const recordingListParamNames = [ACERVO_SOURCE_PARAM, 'q', 'page', ACERVO_PERSON_PARAM] as const const recordingListParamNameSet = new Set(recordingListParamNames) export type RecordingListState = { source: 'enviadas' page: number q?: string + /** Selected "Pessoa" labels; repeated `person` params, literal text. */ + people?: string[] } /** `enviadas` only for the explicit value; anything else is the Câmara default. */ export const parseAcervoSource = (params: RawSearchParams): AcervoSource => firstValue(params[ACERVO_SOURCE_PARAM]) === ACERVO_SOURCE_ENVIADAS ? 'enviadas' : 'camara' +/** + * Parses the repeated `person` filter: trims, drops empty and oversized values + * (a label never exceeds the field limit, so an oversized token can only be a + * hand-typed miss) and dedupes case-insensitively keeping the first spelling. + */ +export const parseRecordingPeople = (raw: string | string[] | undefined): string[] => { + const people: string[] = [] + const seen = new Set() + for (const value of allParamValues(raw)) { + if (value.length > RECORDING_SPEAKER_LABEL_MAX_LENGTH) continue + const folded = foldSpeakerName(value) + if (seen.has(folded)) continue + seen.add(folded) + people.push(value) + } + return people +} + export const parseRecordingListParams = (params: RawSearchParams): RecordingListState => { const rawPage = strictDecimalInteger(firstValue(params.page)) const rawQ = normalizedText(firstValue(params.q)) const q = rawQ && isContactSearchQueryReady(rawQ) ? rawQ : undefined + const people = parseRecordingPeople(params[ACERVO_PERSON_PARAM]) return { source: 'enviadas', page: rawPage ?? 1, ...(q ? { q } : {}), + ...(people.length > 0 ? { people } : {}), } } @@ -56,10 +82,35 @@ const recordingListSearchParams = ( // `source=enviadas` is always serialized — it is what selects the source. params.set(ACERVO_SOURCE_PARAM, ACERVO_SOURCE_ENVIADAS) if (state.q) params.set('q', state.q) + for (const person of state.people ?? []) params.append(ACERVO_PERSON_PARAM, person) if (page > 1) params.set('page', String(page)) return params } +/** The URL of one "Pessoa" facet option with `person` toggled (page resets). */ +export const toggleRecordingPerson = ( + state: RecordingListState, + value: string, +): RecordingListState => { + const current = state.people ?? [] + const folded = foldSpeakerName(value) + const selected = current.some((person) => foldSpeakerName(person) === folded) + const people = selected + ? current.filter((person) => foldSpeakerName(person) !== folded) + : [...current, value] + return { ...state, page: 1, ...(people.length > 0 ? { people } : { people: undefined }) } +} + +/** The URL that removes one selected "Pessoa" (chips with an X; page resets). */ +export const removeRecordingPerson = ( + state: RecordingListState, + value: string, +): RecordingListState => { + const folded = foldSpeakerName(value) + const people = (state.people ?? []).filter((person) => foldSpeakerName(person) !== folded) + return { ...state, page: 1, ...(people.length > 0 ? { people } : { people: undefined }) } +} + export const buildRecordingListHref = (state: RecordingListState, page: number): string => buildListHref(state, recordingListSearchParams, CAMPAIGN_COMMUNICATION_ACERVO, page) diff --git a/src/utilities/recordings/recordingPageData.ts b/src/utilities/recordings/recordingPageData.ts index d8cde2f3..e113cbfc 100644 --- a/src/utilities/recordings/recordingPageData.ts +++ b/src/utilities/recordings/recordingPageData.ts @@ -2,6 +2,7 @@ import 'server-only' import type { Payload } from 'payload' +import { foldSpeakerName } from '@/lib/recordingDiarization' import { RECORDING_NOT_FOUND_MESSAGE } from '@/lib/schemas/recording' import { normalizeForSearch } from '@/lib/speechSearch' import type { CampaignUser } from '@/payload-types' @@ -33,29 +34,76 @@ const recordingListSelect = { step: true, recordedAt: true, durationSeconds: true, + speakerNames: true, } as const const recordingDetailSelect = { ...recordingListSelect, error: true, + speakerLabels: true, + speakerLabelsDropped: true, } as const const segmentSelect = { startSeconds: true, endSeconds: true, text: true, + speakerKey: true, } as const const toSegmentRecord = (segment: { startSeconds: number endSeconds: number text: string + speakerKey?: string | null }): RecordingSegmentRecord => ({ startSeconds: segment.startSeconds, endSeconds: segment.endSeconds, text: segment.text, + speakerKey: segment.speakerKey ?? null, }) +/** C200 — the options the "Pessoa" facet offers. */ +export type RecordingFilterOptions = { + people: string[] +} + +/** + * C200 — distinct labels across the recordings the actor can read: the facet + * only ever lists what is reachable, and the derived `speakerNames` column is + * the single source. Small acervo now; if the list grows past a few thousand + * rows this becomes a `SELECT DISTINCT` (registered as a revisit trigger in the + * impl plan). + */ +export const loadRecordingFilterOptions = async ( + payload: Payload, + user: CampaignUser, +): Promise => { + const result = await payload.find({ + collection: 'recording', + depth: 0, + limit: 0, + pagination: false, + select: { speakerNames: true }, + user, + overrideAccess: false, + }) + + const people: string[] = [] + const seen = new Set() + for (const recording of result.docs) { + for (const name of recording.speakerNames ?? []) { + const trimmed = name.trim() + const folded = foldSpeakerName(trimmed) + if (!trimmed || seen.has(folded)) continue + seen.add(folded) + people.push(trimmed) + } + } + + return { people: people.sort((left, right) => left.localeCompare(right, 'pt-BR')) } +} + /** * The excerpt segment of each listed recording: ONE matching segment per row * (the search runs `LIKE` on the joined `recording.searchText`, so a page of @@ -120,6 +168,7 @@ const loadMatchedSegments = async ( export type RecordingsPageData = { rows: RecordingListItemViewModel[] state: RecordingListState + filterOptions: RecordingFilterOptions redirectHref?: string totalDocs: number totalPages: number @@ -134,17 +183,20 @@ export const loadRecordingsPageData = async ( const canonicalUrl = resolveRecordingListUrl(rawSearchParams) const state = canonicalUrl.state - const result = await payload.find({ - collection: 'recording', - depth: 0, - limit: recordingPageSize, - page: state.page, - sort: '-createdAt', - where: buildRecordingListWhere(state), - select: recordingListSelect, - user, - overrideAccess: false, - }) + const [result, filterOptions] = await Promise.all([ + payload.find({ + collection: 'recording', + depth: 0, + limit: recordingPageSize, + page: state.page, + sort: '-createdAt', + where: buildRecordingListWhere(state), + select: recordingListSelect, + user, + overrideAccess: false, + }), + loadRecordingFilterOptions(payload, user), + ]) const resolvedUrl = resolveRecordingListUrl(rawSearchParams, result.totalPages) const recordings = result.docs @@ -163,9 +215,11 @@ export const loadRecordingsPageData = async ( recording, matchedSegments: segmentsByRecording.get(recording.id) ?? [], query: state.q, + people: state.people, }), ), state: resolvedUrl.state, + filterOptions, redirectHref: resolvedUrl.redirectHref ?? canonicalUrl.redirectHref, totalDocs: result.totalDocs, totalPages: result.totalPages, diff --git a/src/utilities/recordings/recordingViewModels.ts b/src/utilities/recordings/recordingViewModels.ts index 563db30c..2489c6b2 100644 --- a/src/utilities/recordings/recordingViewModels.ts +++ b/src/utilities/recordings/recordingViewModels.ts @@ -1,13 +1,15 @@ /** * C199 — view models of the uploaded recordings. Pure: the loader hands raw - * Payload rows and the view model decides labels, the matching excerpt and the - * links the list/detail render. + * Payload rows and the view model decides labels, the matching excerpt, the + * speaker groups and the links the list/detail render. C200 adds the anonymous + * cluster grouping ("Falante N") and the human labels the team typed. */ import { recordingFailureMessage, toRecordingViewModel, type RecordingViewModel, } from '@/lib/recording' +import { recordingSpeakerDefaultLabel, speakerNameMatches } from '@/lib/recordingDiarization' import { formatSpeechClock } from '@/lib/speechClock' import { buildHighlightedExcerpt, @@ -22,6 +24,8 @@ export type RecordingSegmentRecord = { startSeconds: number endSeconds: number text: string + /** C200 — anonymous cluster key; null/absent on recordings without diarization. */ + speakerKey?: string | null } type RecordingBaseRecord = { @@ -33,15 +37,24 @@ type RecordingBaseRecord = { durationSeconds?: number | null } -export type RecordingListRecord = RecordingBaseRecord +export type RecordingListRecord = RecordingBaseRecord & { + /** C200 — derived distinct labels; drives the card chip under a "Pessoa" filter. */ + speakerNames?: string[] | null +} export type RecordingDetailRecord = RecordingBaseRecord & { /** Raw internal cause of a failure; never handed to the client as-is. */ error?: string | null + /** C200 — human labels by cluster key. */ + speakerLabels?: { speakerKey: string; label: string }[] | null + /** C200 — true when a reprocessing could not keep some label. */ + speakerLabelsDropped?: boolean | null } export type RecordingListItemViewModel = RecordingViewModel & { excerpt: SpeechHighlightedExcerpt | null + /** Selected "Pessoa" labels that this recording indeed contains. */ + matchedPersons: string[] /** * Detail link that seeks the player to the matching segment when the search * found one (`?t=`), carrying the term for transcript highlighting (`?q=`). @@ -56,10 +69,74 @@ export type RecordingDetailSegmentViewModel = { parts: SpeechHighlightPart[] } +/** + * C200 — one anonymous speaker cluster of the detail transcript: the keyed + * segments in order, the numbered default label and the human label when the + * team identified the group. + */ +export type RecordingSpeakerGroupViewModel = { + key: string + index: number + defaultLabel: string + label: string | null + isIdentified: boolean + segments: RecordingDetailSegmentViewModel[] +} + export type RecordingDetailViewModel = RecordingViewModel & { /** Honest cause of a failure; null unless `status === 'failed'`. */ failureMessage: string | null segments: RecordingDetailSegmentViewModel[] + /** Empty when the recording has no (or a partial) speaker grouping. */ + speakerGroups: RecordingSpeakerGroupViewModel[] + /** C200 — surfaced by the imprecision banner when a label was dropped. */ + speakerLabelsDropped: boolean +} + +const toDetailSegment = ( + segment: RecordingSegmentRecord, + query?: string, +): RecordingDetailSegmentViewModel => ({ + startSeconds: segment.startSeconds, + endSeconds: segment.endSeconds, + startLabel: formatSpeechClock(segment.startSeconds), + parts: splitHighlightedParts(segment.text, query ?? ''), +}) + +/** + * Groups the transcript by cluster key. A recording only renders groups when + * EVERY segment is keyed: a half-diarized transcript would put an invented + * boundary next to a real one, so the partial case falls back to the plain + * list. Labels are looked up by key; an unlabeled group keeps "Falante N". + */ +const buildSpeakerGroups = ( + segments: readonly RecordingSegmentRecord[], + labelsByKey: ReadonlyMap, + query?: string, +): RecordingSpeakerGroupViewModel[] => { + if (segments.length === 0 || segments.some((segment) => !segment.speakerKey)) return [] + + const groups = new Map() + for (const segment of segments) { + const key = segment.speakerKey + if (!key) continue + const group = groups.get(key) + const view = toDetailSegment(segment, query) + if (group) group.push(view) + else groups.set(key, [view]) + } + + return [...groups.entries()].map(([key, groupSegments], index) => { + const label = labelsByKey.get(key)?.trim() || null + return { + key, + index, + defaultLabel: recordingSpeakerDefaultLabel(index + 1), + label, + isIdentified: label !== null, + segments: groupSegments, + } + }) } const withSeek = (href: string, segment: RecordingSegmentRecord | undefined, query?: string) => { @@ -80,28 +157,36 @@ export const toRecordingListItemViewModel = ({ recording, matchedSegments, query, + people, }: { recording: RecordingListRecord /** Segments that matched the query (or the row's first one as fallback). */ matchedSegments: readonly RecordingSegmentRecord[] query?: string + /** C200 — the selected "Pessoa" labels of the list state. */ + people?: readonly string[] }): RecordingListItemViewModel => { const base = toRecordingViewModel(recording) const isReady = base.status === 'ready' const q = query?.trim() const segment = isReady && q ? pickMatchingSegment(matchedSegments, q) : undefined const excerpt = segment ? buildHighlightedExcerpt(segment.text, q ?? '') : null + const speakerNames = recording.speakerNames ?? [] return { ...base, excerpt, + matchedPersons: (people ?? []).filter((person) => + speakerNames.some((name) => speakerNameMatches(name, person)), + ), watchHref: withSeek(base.detailHref, segment, q), } } /** - * Detail view of one recording: the base view plus the failure message and the - * clickable transcript segments (highlighted by the `?q=` of the search). + * Detail view of one recording: the base view plus the failure message, the + * clickable transcript segments (highlighted by the `?q=` of the search) and + * the speaker groups when the whole transcript is keyed. */ export const toRecordingDetailViewModel = ({ recording, @@ -114,6 +199,9 @@ export const toRecordingDetailViewModel = ({ }): RecordingDetailViewModel => { const base = toRecordingViewModel(recording) const isFailed = base.status === 'failed' + const labelsByKey = new Map( + (recording.speakerLabels ?? []).map((entry) => [entry.speakerKey, entry.label]), + ) return { ...base, @@ -123,11 +211,8 @@ export const toRecordingDetailViewModel = ({ step: base.step, }) : null, - segments: segments.map((segment) => ({ - startSeconds: segment.startSeconds, - endSeconds: segment.endSeconds, - startLabel: formatSpeechClock(segment.startSeconds), - parts: splitHighlightedParts(segment.text, query ?? ''), - })), + segments: segments.map((segment) => toDetailSegment(segment, query)), + speakerGroups: buildSpeakerGroups(segments, labelsByKey, query), + speakerLabelsDropped: recording.speakerLabelsDropped === true, } } diff --git a/tests/e2e/campaignSpeechAcervo.e2e.spec.ts b/tests/e2e/campaignSpeechAcervo.e2e.spec.ts index 172fa7d9..5387da2c 100644 --- a/tests/e2e/campaignSpeechAcervo.e2e.spec.ts +++ b/tests/e2e/campaignSpeechAcervo.e2e.spec.ts @@ -521,7 +521,18 @@ test.describe('communication vertical (C154/C162)', () => { test.describe('uploaded recordings (C199)', () => { const createRecording = async ( campaign: { fixtures: CampaignE2EOwnership }, - input: { marker: string; status?: 'processing' | 'ready' | 'failed'; withSegments?: boolean }, + input: { + marker: string + status?: 'processing' | 'ready' | 'failed' + withSegments?: boolean + segments?: { + startSeconds: number + endSeconds: number + text: string + speakerKey?: string | null + }[] + speakerLabels?: { speakerKey: string; label: string }[] + }, ) => { const bytes = Buffer.from(`recording-${input.marker}`) const media = await campaign.fixtures.payload.create({ @@ -541,11 +552,35 @@ test.describe('communication vertical (C154/C162)', () => { status: input.status ?? 'ready', recordedAt: '2026-09-01T00:00:00.000Z', media: media.id, + ...(input.speakerLabels ? { speakerLabels: input.speakerLabels } : {}), }, depth: 0, }) - if (input.withSegments ?? true) { + if (input.segments) { + for (const [index, segment] of input.segments.entries()) { + await campaign.fixtures.payload.create({ + collection: 'recordingSegment', + data: hookFilledCreateData<'recordingSegment'>({ + recording: recording.id, + order: index + 1, + startSeconds: segment.startSeconds, + endSeconds: segment.endSeconds, + text: segment.text, + speakerKey: segment.speakerKey ?? null, + }), + depth: 0, + }) + } + await campaign.fixtures.payload.update({ + collection: 'recording', + id: recording.id, + data: { + searchText: normalizeForSearch(input.segments.map((segment) => segment.text).join(' ')), + }, + depth: 0, + }) + } else if (input.withSegments ?? true) { await campaign.fixtures.payload.create({ collection: 'recordingSegment', data: hookFilledCreateData<'recordingSegment'>({ @@ -685,5 +720,155 @@ test.describe('communication vertical (C154/C162)', () => { const denied = await advisorRequest.post(uploadUrl) expect(denied.status()).toBe(403) }) + + test('the grouped detail renders the speaker blocks, the warning and the identify action', async ({ + campaign, + campaignRequest, + }) => { + const marker = campaign.fixtures.value('gravacaofalantes') + const { recording } = await createRecording(campaign, { + marker, + segments: [ + { + startSeconds: 12, + endSeconds: 20, + text: `A plenária discutiu a merenda ${marker}.`, + speakerKey: 'speaker-1', + }, + { + startSeconds: 21, + endSeconds: 30, + text: 'O relator respondeu em seguida.', + speakerKey: 'speaker-2', + }, + ], + speakerLabels: [{ speakerKey: 'speaker-1', label: 'Dep. Jorge Solla' }], + }) + + const user = await campaign.fixtures.createCampaignUser('communicator') + const request = await campaignRequest(user, user.password) + const response = await request.get( + `/campanha/comunicacao/acervo/gravacoes/${recording.id}?t=12&q=${marker}`, + ) + expect(response.status()).toBe(200) + const html = rendered(await response.text()) + + expect(html).toContain('Transcrição por falante') + expect(html).toContain('Dep. Jorge Solla') + expect(html).toContain('Identificação feita pela equipe') + expect(html).toContain('Falante 2') + expect(html).toContain('Agrupamento sem identificação') + expect(html).toContain('Editar identificação') + expect(html).toContain('Identificar falante') + expect(html).toContain('A separação por falante pode estar imprecisa') + expect(html).toContain('data-start-seconds="12"') + expect(html).toContain('data-start-seconds="21"') + expect(html).toContain(' { + const marker = campaign.fixtures.value('gravacaopessoa') + const { recording } = await createRecording(campaign, { + marker, + segments: [ + { + startSeconds: 12, + endSeconds: 20, + text: `A plenária discutiu a merenda ${marker}.`, + speakerKey: 'speaker-1', + }, + ], + speakerLabels: [{ speakerKey: 'speaker-1', label: `Dep. ${marker}` }], + }) + const { recording: other } = await createRecording(campaign, { + marker: `${marker}outra`, + withSegments: false, + }) + + const user = await campaign.fixtures.createCampaignUser('communicator') + const request = await campaignRequest(user, user.password) + const response = await request.get( + `/campanha/comunicacao/acervo?source=enviadas&person=${encodeURIComponent(`dep. ${marker}`)}`, + ) + expect(response.status()).toBe(200) + const html = rendered(await response.text()) + + expect(html).toContain(`Gravação ${marker}`) + expect(html).not.toContain(`Gravação ${marker}outra`) + expect(html).toContain('Pessoa:') + expect(html).toContain('aparece nesta gravação') + expect(html).toContain(`/campanha/comunicacao/acervo/gravacoes/${recording.id}`) + expect(html).not.toContain(`/campanha/comunicacao/acervo/gravacoes/${other.id}`) + }) + + test('labels a cluster through the JSON route and denies the non-acervo roles', async ({ + campaign, + campaignRequest, + }) => { + const marker = campaign.fixtures.value('gravacaorotular') + const { recording } = await createRecording(campaign, { + marker, + segments: [ + { + startSeconds: 12, + endSeconds: 20, + text: 'Fala única do agrupamento.', + speakerKey: 'speaker-1', + }, + ], + }) + const endpoint = `/campanha/comunicacao/acervo/gravacoes/${recording.id}/falantes` + + const user = await campaign.fixtures.createCampaignUser('communicator') + const request = await campaignRequest(user, user.password) + const response = await request.post(endpoint, { + data: { recordingId: recording.id, speakerKey: 'speaker-1', label: `Dep. ${marker}` }, + }) + expect(response.status()).toBe(200) + expect((await response.json()).status).toBe('success') + + const updated = await campaign.fixtures.payload.findByID({ + collection: 'recording', + id: recording.id, + depth: 0, + }) + expect(updated.speakerNames).toEqual([`Dep. ${marker}`]) + + const unknown = await request.post(endpoint, { + data: { recordingId: recording.id, speakerKey: 'speaker-9', label: 'Fantasma' }, + }) + expect(unknown.status()).toBe(400) + + const advisor = await campaign.fixtures.createCampaignUser('advisor') + const advisorRequest = await campaignRequest(advisor, advisor.password) + const denied = await advisorRequest.post(endpoint, { + data: { recordingId: recording.id, speakerKey: 'speaker-1', label: 'Negado' }, + }) + // The JSON mutation wrapper maps a safe domain message to 400 (same + // envelope as the other `/campanha/**` mutation routes). + expect(denied.status()).toBe(400) + }) + + test('a recording without diarization keeps the plain transcript', async ({ + campaign, + campaignRequest, + }) => { + const marker = campaign.fixtures.value('gravacaoSemFalantes') + const { recording } = await createRecording(campaign, { marker }) + + const user = await campaign.fixtures.createCampaignUser('communicator') + const request = await campaignRequest(user, user.password) + const response = await request.get( + `/campanha/comunicacao/acervo/gravacoes/${recording.id}?t=12&q=${marker}`, + ) + const html = rendered(await response.text()) + + expect(html).toContain('data-start-seconds="12"') + expect(html).not.toContain('Transcrição por falante') + expect(html).not.toContain('A separação por falante pode estar imprecisa') + }) }) }) diff --git a/tests/int/recording.int.spec.ts b/tests/int/recording.int.spec.ts index 6c9d3ddc..2ecf8b22 100644 --- a/tests/int/recording.int.spec.ts +++ b/tests/int/recording.int.spec.ts @@ -29,9 +29,14 @@ import { GET } from '@/app/(campaign)/campanha/(app)/comunicacao/acervo/gravacoe import { deleteRecordingForActor, getRecordingStatusesForActor, + labelRecordingSpeakerForActor, retryRecordingForActor, } from '@/app/(campaign)/campanha/actions/recording' import { RECORDING_MAX_BYTES } from '@/lib/recording' +import { + RECORDING_FORBIDDEN_MESSAGE, + RECORDING_SPEAKER_UNKNOWN_MESSAGE, +} from '@/lib/schemas/recording' import { normalizeForSearch } from '@/lib/speechSearch' import type { CampaignUser, RecordingMedia } from '@/payload-types' import config from '@/payload.config' @@ -41,7 +46,10 @@ import { RECORDING_STALE_MS, runRecordingJob, } from '@/utilities/recordings/recordingJob' -import { loadRecordingsPageData } from '@/utilities/recordings/recordingPageData' +import { + loadRecordingFilterOptions, + loadRecordingsPageData, +} from '@/utilities/recordings/recordingPageData' import { receiveRecordingUpload } from '@/utilities/recordings/recordingUpload' import { installCampaignFixtures } from '../helpers/campaignFixtures' @@ -90,11 +98,18 @@ const createRecording = async ({ status = 'ready', title = 'Plenária da Comissão', segments = [], + speakerLabels, withMedia = true, }: { status?: 'uploading' | 'processing' | 'ready' | 'failed' title?: string - segments?: { startSeconds: number; endSeconds: number; text: string }[] + segments?: { + startSeconds: number + endSeconds: number + text: string + speakerKey?: string | null + }[] + speakerLabels?: { speakerKey: string; label: string }[] withMedia?: boolean } = {}) => { const media = withMedia ? await createMedia(`${title}.mp4`) : null @@ -104,6 +119,7 @@ const createRecording = async ({ title, status, ...(media ? { media: media.id } : {}), + ...(speakerLabels ? { speakerLabels } : {}), }, overrideAccess: true, }) @@ -118,6 +134,7 @@ const createRecording = async ({ startSeconds: segment.startSeconds, endSeconds: segment.endSeconds, text: segment.text, + speakerKey: segment.speakerKey ?? null, }), overrideAccess: true, }) @@ -555,4 +572,197 @@ describe('uploaded recordings (C199)', () => { expect(unfiltered.rows.length).toBeGreaterThanOrEqual(2) expect(unfiltered.rows.every((row) => row.excerpt === null)).toBe(true) }) + + it('filters recordings by a human label and lists the facet options', async () => { + const marker = `solla${Date.now().toString(36)}` + const { recording } = await createRecording({ + title: `Plenária com vozes ${marker}`, + segments: [ + { startSeconds: 0, endSeconds: 5, text: 'Primeira fala.', speakerKey: 'speaker-1' }, + { startSeconds: 5, endSeconds: 10, text: 'Segunda fala.', speakerKey: 'speaker-2' }, + ], + speakerLabels: [{ speakerKey: 'speaker-1', label: `Dep. ${marker}` }], + }) + const { recording: other } = await createRecording({ + title: `Sem falantes ${marker}`, + segments: [], + }) + + // The facet matches the same way the dialog chip does: case-insensitive + // containment over the derived names, against the real join table. + const filtered = await loadRecordingsPageData(payload, communicator, { + source: 'enviadas', + person: [`dep. ${marker}`], + }) + expect(filtered.rows.map((row) => row.id)).toContain(recording.id) + expect(filtered.rows.map((row) => row.id)).not.toContain(other.id) + expect(filtered.rows.find((row) => row.id === recording.id)?.matchedPersons).toEqual([ + `dep. ${marker}`, + ]) + + const options = await loadRecordingFilterOptions(payload, communicator) + expect(options.people).toContain(`Dep. ${marker}`) + }) + + it('labels one cluster through the action and rebuilds the facet names', async () => { + const marker = `falante${Date.now().toString(36)}` + const { recording } = await createRecording({ + title: `Debate ${marker}`, + segments: [{ startSeconds: 0, endSeconds: 5, text: 'Fala única.', speakerKey: 'speaker-1' }], + }) + + getCampaignUserMock.mockResolvedValue(communicator) + await labelRecordingSpeakerForActor({ + recordingId: recording.id, + speakerKey: 'speaker-1', + label: `Dep. ${marker}`, + }) + + const updated = await payload.findByID({ + collection: 'recording', + id: recording.id, + depth: 0, + overrideAccess: true, + }) + expect(updated.speakerLabels).toEqual([ + expect.objectContaining({ speakerKey: 'speaker-1', label: `Dep. ${marker}` }), + ]) + expect(updated.speakerNames).toEqual([`Dep. ${marker}`]) + + for (const denied of [advisor, leader]) { + getCampaignUserMock.mockResolvedValue(denied) + await expect( + labelRecordingSpeakerForActor({ + recordingId: recording.id, + speakerKey: 'speaker-1', + label: 'Quem quer que seja', + }), + ).rejects.toThrow(RECORDING_FORBIDDEN_MESSAGE) + } + + getCampaignUserMock.mockResolvedValue(communicator) + await expect( + labelRecordingSpeakerForActor({ + recordingId: recording.id, + speakerKey: 'speaker-9', + label: 'Agrupamento fantasma', + }), + ).rejects.toThrow(RECORDING_SPEAKER_UNKNOWN_MESSAGE) + }) + + it('groups the transcript with the injected diarizer and keeps a reconcilable label', async () => { + const { recording } = await createRecording({ + status: 'processing', + title: 'Plenária com falantes', + segments: [ + { startSeconds: 1, endSeconds: 4, text: 'Fala anterior A', speakerKey: 'speaker-1' }, + { startSeconds: 5, endSeconds: 9, text: 'Fala anterior B', speakerKey: 'speaker-2' }, + ], + speakerLabels: [{ speakerKey: 'speaker-1', label: 'Dep. Jorge Solla' }], + }) + + const restore = withEnv('FFMPEG_PATH', FAKE_FFMPEG) + try { + await runRecordingJob( + payload, + recording.id, + async () => ({ + ok: true, + segments: [ + { start: 1, end: 4, text: 'Bom dia a todas' }, + { start: 5, end: 9, text: 'e a todos' }, + ], + durationSeconds: 1_200, + }), + async (file, options) => { + expect(file).toBeInstanceOf(Blob) + await options?.onProgress?.() + return { + ok: true, + turns: [ + { speaker: 'A', startSeconds: 0, endSeconds: 4.5 }, + { speaker: 'B', startSeconds: 4.5, endSeconds: 10 }, + ], + } + }, + ) + } finally { + restore() + } + + const updated = await payload.findByID({ + collection: 'recording', + id: recording.id, + depth: 0, + overrideAccess: true, + }) + expect(updated.status).toBe('ready') + expect(updated.speakerLabels).toEqual([ + expect.objectContaining({ speakerKey: 'speaker-1', label: 'Dep. Jorge Solla' }), + ]) + expect(updated.speakerLabelsDropped).toBe(false) + + const segments = await payload.find({ + collection: 'recordingSegment', + where: { recording: { equals: recording.id } }, + depth: 0, + limit: 0, + pagination: false, + sort: 'order', + overrideAccess: true, + }) + expect(segments.docs).toHaveLength(4) + expect(segments.docs.map((segment) => segment.speakerKey)).toEqual([ + 'speaker-1', + 'speaker-2', + 'speaker-2', + 'speaker-2', + ]) + }) + + it('keeps the plain transcript and warns when the diarizer fails with labels present', async () => { + const { recording } = await createRecording({ + status: 'processing', + title: 'Debate sem provedor', + segments: [{ startSeconds: 1, endSeconds: 4, text: 'Fala', speakerKey: 'speaker-1' }], + speakerLabels: [{ speakerKey: 'speaker-1', label: 'Dep. Jorge Solla' }], + }) + + const restore = withEnv('FFMPEG_PATH', FAKE_FFMPEG) + try { + await runRecordingJob( + payload, + recording.id, + async () => ({ + ok: true, + segments: [{ start: 1, end: 4, text: 'Fala' }], + durationSeconds: 600, + }), + async () => ({ ok: false, error: 'provider down', status: 502 }), + ) + } finally { + restore() + } + + const updated = await payload.findByID({ + collection: 'recording', + id: recording.id, + depth: 0, + overrideAccess: true, + }) + expect(updated.status).toBe('ready') + expect(updated.speakerLabels ?? []).toEqual([]) + expect(updated.speakerLabelsDropped).toBe(true) + + const segments = await payload.find({ + collection: 'recordingSegment', + where: { recording: { equals: recording.id } }, + depth: 0, + limit: 0, + pagination: false, + sort: 'order', + overrideAccess: true, + }) + expect(segments.docs.every((segment) => !segment.speakerKey)).toBe(true) + }) }) diff --git a/tests/unit/assemblyAiDiarize.unit.spec.ts b/tests/unit/assemblyAiDiarize.unit.spec.ts new file mode 100644 index 00000000..5afbeb79 --- /dev/null +++ b/tests/unit/assemblyAiDiarize.unit.spec.ts @@ -0,0 +1,134 @@ +// @vitest-environment node + +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { + ASSEMBLYAI_TRANSCRIPT_URL, + ASSEMBLYAI_UPLOAD_URL, + assemblyAiDiarize, +} from '@/utilities/ai/assemblyAiDiarize' + +type FetchCall = { url: string; init: RequestInit | undefined } + +const jsonResponse = (body: unknown, status = 200): Response => + new Response(JSON.stringify(body), { status }) + +const audioBlob = (): Blob => new Blob([new Uint8Array([1, 2, 3])], { type: 'audio/mpeg' }) + +const installFetch = ( + handler: (url: string, init: RequestInit | undefined, call: number) => Response, +): FetchCall[] => { + const calls: FetchCall[] = [] + vi.stubGlobal( + 'fetch', + vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { + const url = String(input) + calls.push({ url, init }) + return handler(url, init, calls.length) + }), + ) + return calls +} + +afterEach(() => { + vi.unstubAllGlobals() + vi.unstubAllEnvs() +}) + +describe('AssemblyAI diarization provider (C200)', () => { + it('uploads, submits with speaker labels and normalizes ms to seconds', async () => { + vi.stubEnv('ASSEMBLYAI_API_KEY', 'test-key') + const calls = installFetch((url, init) => { + if (url === ASSEMBLYAI_UPLOAD_URL) + return jsonResponse({ upload_url: 'https://cdn/audio.mp3' }) + if (url === ASSEMBLYAI_TRANSCRIPT_URL && init?.method === 'POST') { + return jsonResponse({ id: 'tr_1', status: 'queued' }) + } + return jsonResponse({ + status: 'completed', + utterances: [ + { speaker: 'A', start: 1000, end: 2500 }, + { speaker: 'B', start: 2500, end: 4000 }, + { speaker: 'C', start: 'x', end: 10 }, + ], + }) + }) + + const progress = vi.fn() + const result = await assemblyAiDiarize(audioBlob(), { + pollIntervalMs: 1, + onProgress: progress, + }) + + expect(result).toEqual({ + ok: true, + turns: [ + { speaker: 'A', startSeconds: 1, endSeconds: 2.5 }, + { speaker: 'B', startSeconds: 2.5, endSeconds: 4 }, + ], + }) + const create = calls.find( + (call) => call.url === ASSEMBLYAI_TRANSCRIPT_URL && call.init?.method === 'POST', + ) + expect(create?.init?.body).toContain('"speaker_labels":true') + expect(create?.init?.body).toContain('"speech_models":["universal-2"]') + expect(create?.init?.body).toContain('"language_code":"pt"') + expect(new Headers(calls[0]?.init?.headers).get('authorization')).toBe('test-key') + expect(calls.some((call) => call.init?.method === 'DELETE')).toBe(true) + expect(progress).toHaveBeenCalled() + }) + + it('answers 503 without a key and never calls the provider', async () => { + vi.stubEnv('ASSEMBLYAI_API_KEY', '') + const calls = installFetch(() => jsonResponse({})) + + expect(await assemblyAiDiarize(audioBlob())).toEqual({ + ok: false, + error: 'Separação de falantes está indisponível no momento.', + status: 503, + }) + expect(calls).toHaveLength(0) + }) + + it('degrades to an error when the upload fails', async () => { + vi.stubEnv('ASSEMBLYAI_API_KEY', 'test-key') + const calls = installFetch(() => jsonResponse({}, 500)) + + const result = await assemblyAiDiarize(audioBlob()) + expect(result.ok).toBe(false) + expect(calls).toHaveLength(1) + }) + + it('degrades to an error and deletes the remote transcript on provider error', async () => { + vi.stubEnv('ASSEMBLYAI_API_KEY', 'test-key') + const calls = installFetch((url, init) => { + if (url === ASSEMBLYAI_UPLOAD_URL) + return jsonResponse({ upload_url: 'https://cdn/audio.mp3' }) + if (url === ASSEMBLYAI_TRANSCRIPT_URL && init?.method === 'POST') { + return jsonResponse({ id: 'tr_9', status: 'queued' }) + } + if (init?.method === 'DELETE') return jsonResponse({}, 200) + return jsonResponse({ status: 'error', error: 'provider exploded' }) + }) + + expect(await assemblyAiDiarize(audioBlob(), { pollIntervalMs: 1 })).toMatchObject({ ok: false }) + expect(calls.some((call) => call.init?.method === 'DELETE')).toBe(true) + }) + + it('accepts a completed transcript without utterances (honest empty turns)', async () => { + vi.stubEnv('ASSEMBLYAI_API_KEY', 'test-key') + installFetch((url, init) => { + if (url === ASSEMBLYAI_UPLOAD_URL) + return jsonResponse({ upload_url: 'https://cdn/audio.mp3' }) + if (url === ASSEMBLYAI_TRANSCRIPT_URL && init?.method === 'POST') { + return jsonResponse({ id: 'tr_2', status: 'queued' }) + } + return jsonResponse({ status: 'completed', utterances: [] }) + }) + + expect(await assemblyAiDiarize(audioBlob(), { pollIntervalMs: 1 })).toEqual({ + ok: true, + turns: [], + }) + }) +}) diff --git a/tests/unit/recording.unit.spec.ts b/tests/unit/recording.unit.spec.ts index 23388a57..ad1085a2 100644 --- a/tests/unit/recording.unit.spec.ts +++ b/tests/unit/recording.unit.spec.ts @@ -21,6 +21,7 @@ import { } from '@/lib/recording' import { buildRecordingAudioFfmpegArgs, + buildRecordingFullAudioFfmpegArgs, mergeChunkTranscriptions, RECORDING_AUDIO_CHUNK_SECONDS, recordingSearchText, @@ -180,4 +181,17 @@ describe('recording transcription (C199)', () => { expect(mergeChunkTranscriptions([])).toEqual([]) expect(recordingSearchText([])).toBe('') }) + + it('builds the single-file extraction for diarization (C200)', () => { + const args = buildRecordingFullAudioFfmpegArgs({ + inputPath: '/tmp/in.mkv', + outputPath: '/tmp/full.mp3', + }) + expect(args).toContain('-vn') + expect(args).toContain('libmp3lame') + // One output, no segmentation: speaker numbering needs the whole audio. + expect(args).not.toContain('-f') + expect(args).not.toContain('-segment_time') + expect(args.at(-1)).toBe('/tmp/full.mp3') + }) }) diff --git a/tests/unit/recordingDiarization.unit.spec.ts b/tests/unit/recordingDiarization.unit.spec.ts new file mode 100644 index 00000000..f342158d --- /dev/null +++ b/tests/unit/recordingDiarization.unit.spec.ts @@ -0,0 +1,177 @@ +// @vitest-environment node + +import { describe, expect, it } from 'vitest' + +import { + assignSpeakerKeys, + reconcileSpeakerLabels, + RECORDING_DIARIZATION_MAX_SECONDS, + recordingSpeakerDefaultLabel, + speakerKeysInOrder, + speakerNameMatches, + speakerNamesFromLabels, + type DiarizedTurn, +} from '@/lib/recordingDiarization' + +const segment = (startSeconds: number, endSeconds: number) => ({ + startSeconds, + endSeconds, + text: `trecho ${startSeconds}`, +}) + +const turn = (speaker: string, startSeconds: number, endSeconds: number): DiarizedTurn => ({ + speaker, + startSeconds, + endSeconds, +}) + +describe('recording speaker grouping (C200)', () => { + it('assigns each segment to the turn with the largest overlap', () => { + const segments = [segment(0, 10), segment(10, 20), segment(20, 30)] + const turns = [turn('A', 0, 11), turn('B', 12, 30)] + + const keyed = assignSpeakerKeys(segments, turns) + expect(keyed.map((entry) => entry.speakerKey)).toEqual(['speaker-1', 'speaker-2', 'speaker-2']) + // The original fields survive the assignment. + expect(keyed[1]).toMatchObject({ startSeconds: 10, endSeconds: 20, text: 'trecho 10' }) + }) + + it('breaks an overlap tie by the closest midpoint', () => { + const segments = [segment(0, 10)] + // Both turns overlap the same 5 s; B's midpoint is closer to the segment's. + const turns = [turn('B', 5, 20), turn('A', 0, 5)] + + expect(assignSpeakerKeys(segments, turns)[0]?.speakerKey).toBe('speaker-1') + expect( + assignSpeakerKeys([segment(0, 10)], [turn('A', 0, 5), turn('B', 5, 20)])[0], + ).toMatchObject({ + speakerKey: 'speaker-1', + }) + }) + + it('falls back to the nearest turn when there is no overlap at all', () => { + const segments = [segment(0, 10), segment(30, 40)] + const turns = [turn('A', 12, 20), turn('B', 45, 60)] + + expect(assignSpeakerKeys(segments, turns).map((entry) => entry.speakerKey)).toEqual([ + 'speaker-1', + 'speaker-2', + ]) + }) + + it('numbers clusters globally by first appearance, not by provider name', () => { + const segments = [segment(0, 5), segment(5, 10), segment(10, 15)] + const turns = [turn('spk_9', 10, 15), turn('spk_2', 0, 5), turn('spk_9', 0, 10)] + + expect(assignSpeakerKeys(segments, turns).map((entry) => entry.speakerKey)).toEqual([ + 'speaker-1', + 'speaker-2', + 'speaker-2', + ]) + }) + + it('keeps every key null without a usable turn and ignores invalid turns', () => { + expect(assignSpeakerKeys([segment(0, 5)], []).map((entry) => entry.speakerKey)).toEqual([null]) + expect( + assignSpeakerKeys([segment(0, 5)], [turn('', 0, 5), turn('A', Number.NaN, 5)]).map( + (entry) => entry.speakerKey, + ), + ).toEqual([null]) + }) + + it('lists the cluster keys in first-appearance order', () => { + expect( + speakerKeysInOrder([ + { speakerKey: 'speaker-2' }, + { speakerKey: null }, + { speakerKey: 'speaker-1' }, + { speakerKey: 'speaker-2' }, + ]), + ).toEqual(['speaker-2', 'speaker-1']) + }) + + it('reconciles labels 1:1 by time overlap and reports what was dropped', () => { + const previousSegments = [{ speakerKey: 'speaker-1', startSeconds: 0, endSeconds: 10 }] + const nextSegments = [{ speakerKey: 'speaker-1', startSeconds: 0, endSeconds: 10 }] + + expect( + reconcileSpeakerLabels({ + previousLabels: [{ speakerKey: 'speaker-1', label: 'Dep. Jorge Solla' }], + previousSegments, + nextSegments, + }), + ).toEqual({ labels: [{ speakerKey: 'speaker-1', label: 'Dep. Jorge Solla' }], dropped: false }) + + expect( + reconcileSpeakerLabels({ + previousLabels: [{ speakerKey: 'speaker-1', label: 'Dep. Jorge Solla' }], + previousSegments, + nextSegments: [{ speakerKey: 'speaker-1', startSeconds: 40, endSeconds: 50 }], + }), + ).toEqual({ labels: [], dropped: true }) + + expect( + reconcileSpeakerLabels({ + previousLabels: [], + previousSegments, + nextSegments, + }), + ).toEqual({ labels: [], dropped: false }) + }) + + it('never shares one new cluster between two old labels (merge)', () => { + // Two old clusters merged into one new cluster: keeping either label would + // name the other person's speech, so both are dropped. + const result = reconcileSpeakerLabels({ + previousLabels: [ + { speakerKey: 'speaker-1', label: 'Solla' }, + { speakerKey: 'speaker-2', label: 'Outra pessoa' }, + ], + previousSegments: [ + { speakerKey: 'speaker-1', startSeconds: 0, endSeconds: 5 }, + { speakerKey: 'speaker-2', startSeconds: 5, endSeconds: 10 }, + ], + nextSegments: [{ speakerKey: 'speaker-1', startSeconds: 0, endSeconds: 10 }], + }) + + expect(result).toEqual({ labels: [], dropped: true }) + }) + + it('never duplicates one label across two new clusters (split)', () => { + // One old cluster split into two new ones: the label cannot name both. + const result = reconcileSpeakerLabels({ + previousLabels: [{ speakerKey: 'speaker-1', label: 'Solla' }], + previousSegments: [{ speakerKey: 'speaker-1', startSeconds: 0, endSeconds: 10 }], + nextSegments: [ + { speakerKey: 'speaker-1', startSeconds: 0, endSeconds: 5 }, + { speakerKey: 'speaker-2', startSeconds: 5, endSeconds: 10 }, + ], + }) + + expect(result).toEqual({ labels: [], dropped: true }) + }) + + it('derives distinct speaker names case-insensitively, in order', () => { + expect( + speakerNamesFromLabels([ + { speakerKey: 'speaker-1', label: ' Dep. Jorge Solla ' }, + { speakerKey: 'speaker-2', label: 'dep. jorge solla' }, + { speakerKey: 'speaker-3', label: 'Presidente' }, + { speakerKey: 'speaker-4', label: ' ' }, + ]), + ).toEqual(['Dep. Jorge Solla', 'Presidente']) + expect(speakerNamesFromLabels(null)).toEqual([]) + }) + + it('matches a facet term the same way the ILIKE query does', () => { + expect(speakerNameMatches('Dep. Jorge Solla', 'solla')).toBe(true) + expect(speakerNameMatches('Dep. Jorge Solla', 'Dep. Jorge')).toBe(true) + expect(speakerNameMatches('Presidente', 'solla')).toBe(false) + }) + + it('names the default label with a number and pins the provider ceiling', () => { + expect(recordingSpeakerDefaultLabel(1)).toBe('Falante 1') + expect(recordingSpeakerDefaultLabel(12)).toBe('Falante 12') + expect(RECORDING_DIARIZATION_MAX_SECONDS).toBe(10 * 60 * 60) + }) +}) diff --git a/tests/unit/recordingListFilters.unit.spec.ts b/tests/unit/recordingListFilters.unit.spec.ts new file mode 100644 index 00000000..7dd2d923 --- /dev/null +++ b/tests/unit/recordingListFilters.unit.spec.ts @@ -0,0 +1,44 @@ +// @vitest-environment node + +import { describe, expect, it } from 'vitest' + +import { buildRecordingListWhere } from '@/utilities/recordings/recordingListFilters' + +describe('recording list filters (C199/C200)', () => { + it('returns an empty where without search or facet', () => { + expect(buildRecordingListWhere({ source: 'enviadas', page: 1 })).toEqual({}) + }) + + it('searches the normalized transcript text', () => { + expect(buildRecordingListWhere({ source: 'enviadas', page: 1, q: 'Comissão' })).toEqual({ + searchText: { like: 'comissao' }, + }) + }) + + it('filters one person through the derived names', () => { + expect( + buildRecordingListWhere({ source: 'enviadas', page: 1, people: ['Dep. Jorge Solla'] }), + ).toEqual({ speakerNames: { contains: 'Dep. Jorge Solla' } }) + }) + + it('ORs the selected people and ANDs them with the search', () => { + expect( + buildRecordingListWhere({ + source: 'enviadas', + page: 1, + q: 'merenda', + people: ['Solla', 'Presidente'], + }), + ).toEqual({ + and: [ + { searchText: { like: 'merenda' } }, + { + or: [ + { speakerNames: { contains: 'Solla' } }, + { speakerNames: { contains: 'Presidente' } }, + ], + }, + ], + }) + }) +}) diff --git a/tests/unit/recordingListUrl.unit.spec.ts b/tests/unit/recordingListUrl.unit.spec.ts index bee668c6..353136bd 100644 --- a/tests/unit/recordingListUrl.unit.spec.ts +++ b/tests/unit/recordingListUrl.unit.spec.ts @@ -7,7 +7,10 @@ import { buildRecordingListHref, parseAcervoSource, parseRecordingListParams, + parseRecordingPeople, + removeRecordingPerson, resolveRecordingListUrl, + toggleRecordingPerson, } from '@/utilities/recordings/recordingListUrl' describe('recording list URL (C199)', () => { @@ -68,4 +71,63 @@ describe('recording list URL (C199)', () => { expect(buildAcervoSourceHref('enviadas')).toBe('/campanha/comunicacao/acervo?source=enviadas') expect(buildAcervoSourceHref('camara')).toBe('/campanha/comunicacao/acervo') }) + + it('parses repeated `person` values: trim, oversized drop, case-insensitive dedupe', () => { + expect(parseRecordingPeople([' Solla ', 'solla', 'Presidente'])).toEqual([ + 'Solla', + 'Presidente', + ]) + expect(parseRecordingPeople(undefined)).toEqual([]) + expect(parseRecordingPeople('')).toEqual([]) + expect(parseRecordingPeople('x'.repeat(121))).toEqual([]) + }) + + it('canonicalizes and serializes the `person` facet with the query', () => { + const parsed = parseRecordingListParams({ + source: 'enviadas', + q: 'merenda', + person: ['Solla', 'Presidente'], + }) + expect(parsed.people).toEqual(['Solla', 'Presidente']) + + const canonical = resolveRecordingListUrl({ + source: 'enviadas', + q: 'merenda', + person: ['Solla', 'Presidente'], + }) + expect(canonical.href).toBe( + '/campanha/comunicacao/acervo?source=enviadas&q=merenda&person=Solla&person=Presidente', + ) + expect(canonical.redirectHref).toBeUndefined() + }) + + it('drops a duplicated facet value on canonicalization', () => { + const canonical = resolveRecordingListUrl({ + source: 'enviadas', + person: ['Solla', 'solla', 'x'.repeat(200)], + }) + expect(canonical.href).toBe('/campanha/comunicacao/acervo?source=enviadas&person=Solla') + expect(canonical.redirectHref).toBe('/campanha/comunicacao/acervo?source=enviadas&person=Solla') + }) + + it('toggles and removes one person, resetting to the first page', () => { + const base = { source: 'enviadas' as const, page: 3, q: 'merenda' } + + const added = toggleRecordingPerson(base, 'Solla') + expect(added).toEqual({ ...base, page: 1, people: ['Solla'] }) + + const removed = toggleRecordingPerson({ ...base, people: ['Solla'] }, 'solla') + expect(removed).toEqual({ ...base, page: 1, people: undefined }) + + expect(removeRecordingPerson({ ...base, people: ['Solla', 'Presidente'] }, 'Solla')).toEqual({ + ...base, + page: 1, + people: ['Presidente'], + }) + expect(removeRecordingPerson({ ...base, people: ['Solla'] }, 'Solla')).toEqual({ + ...base, + page: 1, + people: undefined, + }) + }) })