From 8318fb5fceff28dc0a93b00f0053f58850a30d88 Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Thu, 24 Sep 2026 02:36:34 -0300 Subject: [PATCH] =?UTF-8?q?feat(C215):=20ingest=C3=A3o=20de=20falas=20da?= =?UTF-8?q?=20internet=20no=20acervo=20=E2=80=94=20origem=20web,=20espelho?= =?UTF-8?q?=20privado=20e=20cat=C3=A1logo=20(Closes=20#1291)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + docs/changelog/2026-09-24-c215.md | 1 + docs/plans/acervo-falas-web-ingestao-impl.md | 242 + package.json | 1 + scripts/import-camara-speeches.mjs | 35 +- scripts/import-web-speeches.mjs | 240 + scripts/lib/camaraFetch.mjs | 7 +- scripts/lib/cli.mjs | 27 + scripts/lib/e2e-affected-manifest.mjs | 5 + scripts/lib/mediaBinaries.mjs | 149 + scripts/lib/reelFfmpeg.mjs | 145 +- scripts/lib/test-affected-core.mjs | 2 + scripts/lib/webSpeeches.mjs | 131 + src/collections/InternetSpeechMedia.ts | 44 + src/collections/Speech.ts | 132 +- src/lib/webSpeech.ts | 280 + ...20260924_042028_add_web_speech_origin.json | 18613 ++++++++++++++++ .../20260924_042028_add_web_speech_origin.ts | 71 + src/migrations/index.ts | 8 +- src/payload-types.ts | 93 +- src/payload.config.ts | 4 + src/utilities/ai/deepInfraTranscribe.ts | 7 + src/utilities/ai/tools/findSpeechExcerpts.ts | 9 +- src/utilities/media/downloadToFile.ts | 63 + src/utilities/media/ffmpeg.ts | 7 +- src/utilities/media/ytdlp.ts | 170 + src/utilities/speech/speechCoverage.ts | 1 + src/utilities/speech/speechImport.ts | 327 +- src/utilities/speech/speechListFilters.ts | 4 +- src/utilities/speech/speechPageData.ts | 3 + src/utilities/speech/webSpeechIngest.ts | 514 + tests/e2e/campaignSpeechAcervo.e2e.spec.ts | 1 + tests/e2e/campaignSpeechCut.e2e.spec.ts | 1 + tests/int/institutionSnapshot.int.spec.ts | 1 + tests/int/speechCatalog.int.spec.ts | 3 + tests/int/speechCoverage.int.spec.ts | 5 +- tests/int/themeSnapshot.int.spec.ts | 1 + tests/int/webSpeechImport.int.spec.ts | 205 + tests/int/webSpeechIngest.int.spec.ts | 355 + tests/unit/downloadToFile.unit.spec.ts | 104 + tests/unit/speechExcerptsTool.unit.spec.ts | 7 +- tests/unit/speechListFilters.unit.spec.ts | 23 +- tests/unit/webSpeech.unit.spec.ts | 147 + tests/unit/webSpeeches.unit.spec.ts | 147 + tests/unit/ytdlp.unit.spec.ts | 145 + 45 files changed, 22185 insertions(+), 297 deletions(-) create mode 100644 docs/changelog/2026-09-24-c215.md create mode 100644 docs/plans/acervo-falas-web-ingestao-impl.md create mode 100644 scripts/import-web-speeches.mjs create mode 100644 scripts/lib/mediaBinaries.mjs create mode 100644 scripts/lib/webSpeeches.mjs create mode 100644 src/collections/InternetSpeechMedia.ts create mode 100644 src/lib/webSpeech.ts create mode 100644 src/migrations/20260924_042028_add_web_speech_origin.json create mode 100644 src/migrations/20260924_042028_add_web_speech_origin.ts create mode 100644 src/utilities/media/downloadToFile.ts create mode 100644 src/utilities/media/ytdlp.ts create mode 100644 src/utilities/speech/webSpeechIngest.ts create mode 100644 tests/int/webSpeechImport.int.spec.ts create mode 100644 tests/int/webSpeechIngest.int.spec.ts create mode 100644 tests/unit/downloadToFile.unit.spec.ts create mode 100644 tests/unit/webSpeech.unit.spec.ts create mode 100644 tests/unit/webSpeeches.unit.spec.ts create mode 100644 tests/unit/ytdlp.unit.spec.ts diff --git a/.gitignore b/.gitignore index 370d766e..81745da7 100644 --- a/.gitignore +++ b/.gitignore @@ -78,6 +78,8 @@ next-env.d.ts /data/ai-fill/ # Câmara pilot media/JSON cache (pnpm camara:pilot — VOD MP4 + report, never commit) /data/camara/ +# C215 — web-speech ingestion batch cache + reports (pnpm falas-web:import) +/data/falas-web/ # Solla × Ceuci report HTML cache (scripts/build-solla-ceuci-salvador-report.mjs) /data/solla-ceuci-salvador/ diff --git a/docs/changelog/2026-09-24-c215.md b/docs/changelog/2026-09-24-c215.md new file mode 100644 index 00000000..a35655c6 --- /dev/null +++ b/docs/changelog/2026-09-24-c215.md @@ -0,0 +1 @@ +**C215 — Ingestão de falas da internet no acervo (2026-09-24):** a esteira que recebe um lote de achados da web (contrato do C218), baixa/espelha a mídia, transcreve com timestamps, classifica e grava de forma idempotente, com relatório honesto. O `speech` deixou de ser só Câmara: ganhou o discriminador `origin` (`camara` default | `web`) e os campos web nullable (`platform` youtube|instagram|radio|audio, `externalId`, `sourceUrl`, `title`, `channel`, `mirroredMedia`/`thumbnail` → nova collection privada `internetSpeechMedia`), numa migration aditiva (`20260924_042028_add_web_speech_origin`, `origin` NOT NULL DEFAULT `'camara'` — as linhas existentes ficam intactas) e a collection privada registrada no plugin S3 (`internetSpeechMedia: true`; sem isso, produção cairia no disco efêmero), com `read = canReadSpeech` (o gate do acervo, fail-closed), escrita `payloadAdminOnly` (o CLI usa o bypass justificado) e cascade no `beforeDelete` do `Speech` (mídia + capa). A identidade da fala web é namespaced (`web::`, nunca colide com o `sourceKey` da Câmara) e o contrato puro vive em `src/lib/webSpeech.ts` (taxonomia + labels pt-BR, schema zod do lote `{ generatedAt?, findings[] }` com `mediaUrl` obrigatória para rádio/áudio e `publishedAt` normalizado para o `speechAt` wall-clock, canonicalização de URL que colapsa youtu.be/shorts/live/embed e reels e remove tracking, e o `webSpeechSourceKey` estável). A aquisição tem dono único por ferramenta: `src/utilities/media/ytdlp.ts` (YouTube/Instagram via `YTDLP_PATH` → PATH, probe `--version` fail-closed com hint de `pipx install yt-dlp`, `--dump-json` preenchendo lacunas de título/canal/duração/id/capa e download capado em ≤720p H.264/AAC com `--merge-output-format mp4` e `--ffmpeg-location` do binário resolvido) e `src/utilities/media/downloadToFile.ts` (rádio/áudio direto, streaming com timeout); a resolução/probe/runner genérico de binários saiu de `scripts/lib/reelFfmpeg.mjs` para `scripts/lib/mediaBinaries.mjs` (o reel re-exporta — API e unit pins intactos) e o script resolve o ffmpeg com o fallback empacotado, setando `FFMPEG_PATH`. O pipeline `src/utilities/speech/webSpeechIngest.ts` (deps injetáveis para os specs) reusa os donos existentes — `deepInfraTranscribeSegments` + chunk/merge/`searchText` do C199, `classifySpeech` (gazetteer + LLM), `withPayloadTransaction` e o `upsertSpeechBundle` estendido com os campos web e `options.req` (o caminho Câmara segue idêntico) — com a capa em best-effort fora da transação (imagem quebrada nunca perde a fala) e limpeza best-effort do artefato substituído no `--reprocess`. O CLI `pnpm falas-web:import --findings [--out data/falas-web] [--limit n] [--dry-run] [--reprocess]` (relatórios gitignored em `data/falas-web/reports/`) tem as guardas do C155 (`assertLocalDatabase` + `FALAS_WEB_IMPORT_CONFIRM=1` para alvo não-local/produção; `--dry-run` é read-only e não exige a flag), dedupe por `sourceKey` no lote, item inválido vira falha do item (nunca derruba o lote) e o relatório declara achados/novos/atualizados/ignorados/falhas (com estágio e motivo)/tempo/segundos e custo de ASR/custo LLM — exit 1 quando nenhum achado entrou. Blindagem obrigatória das superfícies existentes: `origin: camara` no where da lista/opções da Câmara (`speechListFilters`/`speechPageData`), na cobertura (`speechCoverage`) e no tool do Sollinha (`findSpeechExcerpts`) — a fala web nasce invisível até o C216 abrir a fonte própria; o serving/player fica no C216 (`buildPrivateMediaResponse`) e o corte no C217 (`downloadPrivateMediaToFile`), nada de rota nova aqui e nenhuma URL pública de mídia. Sem UI (Impeccable A), sem PII/`Consent` novo (canal/autor é texto de origem; menções continuam texto), sem segundo cadastro de pessoa. Verificação: unit (`webSpeech` — schema/sourceKey/canonicalização/labels; `webSpeeches` — plano/dedupe/relatório; `ytdlp` — resolução/parse/args; pins de `speechListFilters` e `speechExcerptsTool` atualizados), int (`webSpeechImport` — round-trip dos campos web, idempotência com facetas/segmentos preservados, curadoria `manual` vencendo, cascade da mídia no delete; `webSpeechIngest` — pipeline com fakes de acquirer/ffmpeg/transcriber/classificador, segundo run só atualiza metadados, `--reprocess` troca o artefato e apaga o antigo, falhas honestas por estágio sem linha fantasma, thumbnail espelhada, achado de rádio com `mediaUrl`), smoke local do CLI (`--dry-run` com inválido/duplicado e execução real com yt-dlp ausente → relatório + exit 1), `pnpm gate:fast`, `pnpm test:int` e o e2e afetado local. O run real de um lote em produção é operação do C218 (o PR não escreve no acervo de produção). diff --git a/docs/plans/acervo-falas-web-ingestao-impl.md b/docs/plans/acervo-falas-web-ingestao-impl.md new file mode 100644 index 00000000..3e41a24b --- /dev/null +++ b/docs/plans/acervo-falas-web-ingestao-impl.md @@ -0,0 +1,242 @@ +# Impl: C215 — Ingestão de falas da internet no acervo (descoberta, espelho e catálogo) + +Status: executado +Atualizado em: 2026-09-24 +Issue: #1291 +Intenção: docs/plans/acervo-falas-web-ingestao.md +Appetite restante: herdado (~3–4 dias de engenharia + tempo de máquina; um outcome verificável — um lote de falas encontradas na web entra no catálogo com transcrição, facetas e mídia espelhada, reexecutável sem duplicar e com relatório honesto) + +## Leitura da intenção + +- **Outcome:** dado um lote de achados da web (plataforma + URL + metadados que a descoberta — C218 — já trouxe), cada fala vira registro no acervo com data, duração, transcrição com timestamps, temas/alcance/menções (mesmas facetas do "Falas da Câmara"), proveniência da classificação, origem (plataforma + URL) e canal/autor; a mídia é espelhada privada e preservada. Reexecutar não duplica nem sobrescreve curadoria manual e cada execução imprime relatório honesto (achados, novos, atualizados, ignorados, falhas, tempo e custo de ASR). +- **O que NÃO negociar (lockdowns):** + - Mídia espelhada é privada e preservada: nunca URL pública, nunca `/api/media/file`; leitura só pelo gate do acervo (`canReadSpeech`/`canReadCommunicationCatalog`, fail-closed; `advisor`/`leader` negados). + - Nenhum segundo pipeline de ASR/classificação/upsert e nenhum segundo cadastro de pessoa (menções continuam texto; sem `Contact`, sem `Consent` novo — não há PII nova). + - `sourceKey` da Câmara é identidade por contrato: a fala web nasce em namespace próprio (`web:*`) e não toca os registros existentes. + - Guardas de execução: `assertLocalDatabase` + `FALAS_WEB_IMPORT_CONFIRM=1` para alvo não-local/override; `--dry-run` sem flag. + - Não tocar feed do site público, Central de Conteúdos (C211 é dona da peça), publicação externa nem score de vaidade. + - Curadoria manual nunca sobrescrita (hook `preserveManualFacets` existente é o pino). +- **O que reavaliar (hipóteses da intenção vs. exploração):** + - A intenção deixou "ferramenta de download e destino do espelho" para a implementação: ficam **yt-dlp** (YouTube/Instagram) + download HTTP direto (rádio/áudio) e uma collection privada nova `internetSpeechMedia` — decisões D2/D4. + - "Modelo de dados" também estava em aberto: a exploração confirma que `Speech` não tem origem/plataforma/URL/canal/título/thumb/mídia espelhada e que **quatro superfícies leem `speech` sem discriminador** (`speechPageData`, `speechListFilters`, `speechCoverage`, `findSpeechExcerpts`) — sem blindagem a fala web vazaria para "Falas da Câmara" com crédito errado (D10). + - `speechPosterJob`/`speechCutJob` são by-id (VOD Câmara) e degradam sozinhos — não entram nesta fatia. + +## Abordagem recomendada + +```mermaid +flowchart LR + L["lote de achados (JSON)
generatedAt + findings[]"] --> CLI["scripts/import-web-speeches.mjs
zod + dedupe por sourceKey"] + CLI --> PLAN["plano do lote
novo · atualizar · ignorar"] + PLAN -->|youtube/instagram| YTDLP["yt-dlp --dump-json
YTDLP_PATH fail-closed"] + PLAN -->|radio/audio| HTTP["download HTTP do mediaUrl"] + YTDLP --> MEDIA[("internetSpeechMedia
upload privado S3/disco")] + HTTP --> MEDIA + MEDIA --> FFMPEG["ffmpeg: mp3 mono 16k + chunks
buildRecordingAudioFfmpegArgs"] + FFMPEG --> ASR["deepInfraTranscribeSegments
merge de offsets"] + ASR --> CLS["classifySpeech
gazetteer + LLM"] + CLS --> UPSERT["upsertSpeechBundle estendido
tx: speech + speechSegment + searchText"] + UPSERT --> DB[("speech origin=web
sourceKey web:...")] + CLI --> REPORT["relatório stdout + JSON
data/falas-web/reports"] + DB -.->|"origin: camara nas consultas"| CAM["superfícies Câmara
lista · opções · cobertura · Sollinha"] + DB -.-> C216["C216: lista/detalhe/player
buildPrivateMediaResponse"] + DB -.-> C217["C217: cortes
downloadPrivateMediaToFile"] +``` + +**Opções consideradas:** A) estender `speech` com discriminador de origem + collection privada própria de mídia + aquisição yt-dlp/HTTP + pipeline com dependências injetadas | B) collection nova `internetSpeech` (gêmea) | C) reusar `recordingMedia` e/ou servir a mídia no próprio C215. +**Recomendação:** A — porque preserva o contrato prod da Câmara (`sourceKey`, facetas, segmentos, busca) sem criar uma segunda cópia de todo o conhecimento (classificador, ASR, upsert, busca) e sem forçar relação polimórfica no `SpeechCut` que o C217 vai estender. +**Rejeitadas:** + +- **B (collection nova)** porque duplica facets/segmentos/classificador/import/busca e obriga `SpeechCut.speech` (relação fixa a `speech`) a virar polimorfismo, acoplando o C217 sem ganho. +- **C (reusar `recordingMedia` / servir no C215)** porque `recordingMedia` é o artefato da gravação enviada (domínio e access próprios), e o serving pertence ao C216 (detalhe/player/baixar) e ao C217 (corte), que já consomem `privateMediaResponse.ts`; rota nova aqui seria trabalho jogado fora. + +### Decisões de engenharia + +**D1 — Modelo de dados: estender `speech` com discriminador de origem; NÃO criar collection de fala nova.** +`Decisão: A — novos campos nullable em Speech (só origin obrigatório) + sourceKey namespaced web:*; Câmara intacta.` +`Por quê: reusa facetas, segmentos, classificador, upsert, busca trigram e access já existentes; a fala web é a mesma unidade de catálogo ("mesmas facetas, mesmos filtros" — intenção).` +`Rejeitadas: B) collection nova internetSpeech porque é gêmea de facets/segmentos/classificador/import/busca e força relação polimórfica no SpeechCut (C217); C) sem migration — impossível: mídia espelhada + origem/canal exigem schema.` + +- Campos novos: `origin` (select `camara` default | `web`, required, index — backfill dos existentes pelo default da migration); `platform` (select `youtube|instagram|radio|audio`, index, web-only); `externalId`; `sourceUrl`; `title`; `channel`; `mirroredMedia` (upload → `internetSpeechMedia`); `thumbnail` (upload → mesma collection). +- Reusados sem mudança: `speechAt` (data de publicação no caso web; `deriveSpeechYear` continua), `durationSeconds`, `searchText`, facets, `classifiedBy`, `speechSegment`. `legislature`/`type`/`phase`/VOD ficam nulos na origem web (já são nullable). +- `sourceKey` namespaced: `web::` quando houver id externo; senão `web::`. Câmara permanece exatamente como está (o índice unique já existe e o namespace elimina colisão). + +**D2 — Mídia privada: collection de upload nova `internetSpeechMedia`.** +`Decisão: A — collection privada própria; read = canReadSpeech; create/update/delete = payloadAdminOnly (o CLI usa overrideAccess justificado); registro explícito no plugin S3; cascade no beforeDelete do Speech.` +`Por quê: a mídia da fala web tem gate do acervo, não o gate da gravação; e o dono do ciclo de vida é o próprio Speech (a mídia não existe sem a fala).` +`Rejeitadas: reusar recordingMedia (domínio/labels/access da gravação enviada — faria o modelo mentir); usar media pública (read anônimo por contrato — vazaria mídia de terceiro).` + +- Sem registro no plugin S3 (`internetSpeechMedia: true`), produção cai no disco efêmero do container — mesmo modo de falha documentado no C199. +- Cascade: estender o `beforeDelete` do `Speech` (hoje apaga `speechSegment`) para apagar `mirroredMedia`/`thumbnail` quando existirem, com o mesmo bypass justificado (cascade é do dono; roda dentro do delete autorizado do Speech). + +**D3 — Servir o arquivo é do C216/C217, não daqui.** +`Decisão: A — C215 entrega só o artefato armazenado + a collection; C216 usa buildPrivateMediaResponse (detalhe/player/baixar) e C217 usa downloadPrivateMediaToFile (corte).` +`Por quê: o dono único de I/O privado já existe (privateMediaResponse.ts, C193/C199) e a superfície de leitura nasce no C216; rota nova aqui não tem consumidor.` +`Rejeitadas: rota de arquivo no C215 (sem UI/consumidor, morreria no C216); signed URLs do S3 (já rejeitado no C193: endpoint interno inalcançável do browser).` + +**D4 — Aquisição: yt-dlp (YouTube/Instagram) + download HTTP direto (rádio/áudio).** +`Decisão: A — yt-dlp via YTDLP_PATH → PATH, fail-closed com hint de instalação; radio/audio exigem mediaUrl no lote; qualidade suficiente (≤720p, H.264/AAC, merge mp4) e áudio direto preservado; metadados do --dump-json preenchem lacunas; resolver de ffmpeg extraído de reelFfmpeg.mjs para módulo neutro.` +`Por quê: yt-dlp é a ferramenta que cobre as duas plataformas confirmadas no gate; o lote do C218 já traz URL e (quando houver) mídia direta; o probe/resolução de binário já existe e testado nos reels — extrair o dono é melhor que um segundo resolver.` +`Rejeitadas: gallery-dl/TikTok/Facebook/X (fora das plataformas v1); baixar em qualidade máxima/master (disco e tempo sem necessidade); transcode forçado (o ASR extrai o áudio e o player/corte consomem o arquivo como veio); segunda cópia do resolver de ffmpeg (twin).` + +- `YTDLP_PATH` é o seam de teste (padrão do `FFMPEG_PATH`): os testes injetam um fake pelo env. Sem binário e sem caminho → falha fechada com hint (ex.: `pipx install yt-dlp`), nunca "segue sem baixar". +- Achado `radio`/`audio` sem `mediaUrl` → falha honesta no relatório (estágio `validation`/`download`), o lote continua. +- ffmpeg: extrair `runProcess`/`probeFfmpeg`/`resolveFfmpeg` de `scripts/lib/reelFfmpeg.mjs` para `scripts/lib/mediaBinaries.mjs`; `reelFfmpeg.mjs` re-exporta (API dos reels intacta e unit tests dos reels verdes). O script novo resolve o binário e seta `FFMPEG_PATH` resolvido quando o env não veio explícito, para que `src/utilities/media/ffmpeg.ts` (dono de execução) use o mesmo binário. + +**D5 — Donos únicos reusados (proibido gêmeo).** +`Decisão: A — ASR deepInfraTranscribeSegments + chunking/merge puros de recordingTranscription.ts; classificação classifySpeech; upsertSpeechBundle ESTENDIDO com campos web opcionais; privateMediaResponse.ts; withPayloadTransaction.` +`Por quê: são os donos já testados e usados pelo Câmara/C199; a fala web tem o mesmo problema (áudio longo → chunks → segmentos com timestamps) e a mesma curadoria.` +`Rejeitadas: chamar deepInfraTranscribe sem timestamps (não entrega o aceite); portar o normalizador do CLI da Câmara para um módulo novo (já é dono em recordingTranscription.ts); escrever o upsert da web à parte (dois escritores da mesma tabela = facetas divergindo).` + +- `upsertSpeechBundle` ganha os campos web **opcionais** no bundle (origin/platform/externalId/sourceUrl/title/channel/mirroredMedia/thumbnail) e `findSpeechImportState` passa a devolver o estado web (origin, platform, externalId, sourceUrl, mirroredMedia, thumbnail) para as decisões de skip/reprocess. O caminho Câmara (bundle sem os campos web) continua idêntico; o `metadataData` da Câmara não muda. +- Escrita multi-collection: a ingestão cria o upload `internetSpeechMedia` e roda o upsert **na mesma transação** (`withPayloadTransaction`), passando o `req` ao upsert; sem `req`, o upsert abre a própria transação (Câmara segue igual). Objeto S3 órfão em rollback é aceito e registrado (best-effort de limpeza no catch). +- O artefato espelhado é o input do ffmpeg: a ingestão baixa o objeto para temp com `downloadPrivateMediaToFile` (mesmo dono do job de gravação), extrai/dividi com `buildRecordingAudioFfmpegArgs` e mescla com `mergeChunkTranscriptions`/`recordingSearchText`. + +**D6 — Contrato do lote (interface do C218).** +`Decisão: A — JSON { generatedAt, findings: [...] }, schema zod puro em src/lib/webSpeech.ts; finding com platform/url/mediaUrl?/externalId?/title?/channel?/publishedAt (obrigatório)/durationSeconds?/thumbnailUrl?; mediaUrl obrigatória para radio|audio; dedupe por sourceKey dentro do lote.` +`Por quê: o C218 descobre e cura; a ingestão valida — o contrato é a fronteira entre os dois e precisa ser puro (client-safe, unit-testável) e explícito sobre a obrigatoriedade de mediaUrl nas plataformas sem extrator.` +`Rejeitadas: aceitar qualquer JSON sem validação (falha silenciosa vira lixo no catálogo); exigir todos os metadados (a descoberta não garante tudo — o yt-dlp completa as lacunas).` + +- O envelope é validado uma vez; cada finding é validado individualmente (`safeParse`) para que um achado inválido vire falha do achado no relatório, sem derrubar o lote (espírito do aceite do C218: falha de um não derruba o lote). +- Duplicado dentro do lote (mesmo sourceKey) = ignorado, contado no relatório. + +**D7 — Idempotência.** +`Decisão: A — reexecução pula item completo (segmentos + mídia espelhada) atualizando metadados; --reprocess força re-download/re-ASR; curadoria manual nunca sobrescrita; --dry-run planeja sem rede e sem escrita (lê estado do banco); --limit.` +`Por quê: o aceite exige reexecutar sem duplicar; o padrão já é o do import da Câmara (skip por estado) e do C199 (retry reusa o que existe).` +`Rejeitadas: re-baixar/re-transcrever sempre (tempo e custo de ASR sem ganho); apagar-e-recriar o registro (perderia curadoria manual e quebraria a identidade sourceKey).` + +**D8 — Relatório por execução.** +`Decisão: A — stdout + JSON em /reports/.json (out default data/falas-web/, gitignored, padrão data/camara/): achados, novos, atualizados, ignorados (duplicados/já completos), falhas (com estágio e motivo), tempo total, segundos de ASR, custo ASR (constante DeepInfra existente) e custo LLM quando houver.` +`Por quê: falha não é maquiada e o operador precisa decidir o que reprocessar; o formato segue o CLI da Câmara (mesma família de relatórios gitignored).` +`Rejeitadas: só stdout (não auditável); gravar falha como estado do item no catálogo (a intenção cortou: item que falhou fica fora da lista e aparece no relatório).` + +**D9 — Guardas de execução.** +`Decisão: A — assertLocalDatabase + FALAS_WEB_IMPORT_CONFIRM=1 para alvo não-local/override (mesmo padrão de CAMARA_IMPORT_CONFIRM); --dry-run é modo read-only e não exige a flag.` +`Por quê: a ingestão escreve no acervo real e baixa mídia de terceiro; o guard de intenção explícita é o mesmo contrato já auditado no C155/C160.` +`Rejeitadas: confiar só no host (o proxy do homeserver reescreve para 127.0.0.1 e NODE_ENV=production é o sinal honesto — o guard compartilhado já cobre); --dry-run exigindo a flag (travaria o planejamento inofensivo).` + +**D10 — Blindagem das superfícies Câmara (obrigatória).** +`Decisão: A — adicionar origin: camara ao where da lista/opções (speechListFilters.ts/speechPageData.ts), ao speechCoverage.ts (SQL) e ao findSpeechExcerpts.ts; testes pinam.` +`Por quê: a fala web nasce invisível às superfícies existentes; sem isso a ingestão vaza para "Falas da Câmara" com crédito errado e o Sollinha sugeriria trechos com link de player da Câmara que não toca o arquivo. C216 abre a fonte própria e C219 cuida da paridade.` +`Rejeitadas: filtrar no client/view-model (não protege paginação/contagem/cobertura/SQL); abrir a fonte web nessas superfícies agora (é o C216, com contrato de URL próprio).` + +**D11 — Nomes/arquivos.** +`Decisão: A — src/lib/webSpeech.ts (puro: plataformas+labels, schema zod do lote, webSpeechSourceKey, normalização de URL canônica, INTERNET_SPEECH_MEDIA_SLUG); src/utilities/media/ytdlp.ts (dono do binário/execução/metadados); src/utilities/speech/webSpeechIngest.ts (pipeline por achado com deps injetadas — testável); scripts/import-web-speeches.mjs + scripts/lib/webSpeeches.mjs (helpers puros do CLI); src/collections/InternetSpeechMedia.ts; script pnpm falas-web:import.` +`Por quê: cada concern cai no dono existente (lib puro, utilities/media, utilities/speech) sem módulo top-level novo em utilities/ (regra do codebase-map); o CLI mantém o padrão scripts/-.mjs + lib pura.` +`Rejeitadas: um único módulo gigante (não testável por camada); pipeline dentro do CLI (não reaproveitável e sem injeção de deps); nome em português para identificadores (convenção: copy pt-BR, identificadores em inglês).` + +**D12 — Migration.** +`Decisão: A — pnpm migrate:create add_web_speech_origin (campos + collection + índices); nunca editar migrations existentes; HARD-STOP humano antes de criar/aplicar, mesmo no modo --auto.` +`Por quê: schema muda só por migration committed; o build aplica pendentes em produção, então a migration é o ponto de não-retorno e exige confirmação explícita.` +`Rejeitadas: push automático (push: false é invariante); editar a migration inicial/prod (história congelada); SQL manual no banco (não sobrevive a deploy).` + +**D13 — Testes.** +`Decisão: A — unit de src/lib/webSpeech.ts (schema/sourceKey/URL canônica) + unit dos helpers puros do script (plano/relatório); int do upsert estendido (round-trip dos campos web, idempotência, manual preservado, mídia privada) e do pipeline com fakes (YTDLP_PATH fake, transcriber/classificador injetados, FFMPEG_PATH fake do repo); unit dos reels continua verde após a extração do resolver. Sem e2e novo (sem UI); rodar pnpm test:e2e:affected se o classifier selecionar specs do acervo.` +`Por quê: não há superfície nova; as camadas inferiores cobrem o risco real (schema, idempotência, blindagem, download).` +`Rejeitadas: e2e de script (não há harness para scripts — o padrão é unit da lib pura + int do upsert); e2e de UI (não há UI no C215).` + +- Atualizar `scripts/lib/e2e-affected-manifest.mjs` (entry do acervo, ~linha 434) com `src/lib/webSpeech`, `src/collections/Speech.ts` e `src/collections/InternetSpeechMedia.ts` para não cair no smoke fallback. (`src/utilities/speech`, `src/utilities/media` e `src/lib/speech*` já estão mapeados; `src/lib/webSpeech` e as collections não casam prefixo nenhum hoje.) + +### Componentes / mudanças + +- **`Speech`** (`src/collections/Speech.ts`, editar): campos de origem/mídia (D1); `beforeDelete` estendido para apagar `mirroredMedia`/`thumbnail` (D2); `admin.description`/`defaultColumns` passam a citar as duas origens. Nenhum campo da Câmara muda. +- **`InternetSpeechMedia`** (`src/collections/InternetSpeechMedia.ts`, novo): upload privado; `alt` obrigatório (preenchido pelo pipeline); access D2; `admin.group: 'Comunicação'` com descrição de privacidade (forma de `RecordingMedia.ts`). +- **`src/lib/webSpeech.ts`** (novo, puro): `WEB_SPEECH_PLATFORMS` + labels pt-BR ("YouTube", "Instagram", "Rádio", "Áudio"), schema zod do envelope/finding, `webSpeechSourceKey`, `canonicalWebSpeechUrl`, `INTERNET_SPEECH_MEDIA_SLUG`. +- **`src/utilities/media/ytdlp.ts`** (novo): resolução fail-closed do binário (`YTDLP_PATH` → PATH), execução e parse do `--dump-json` (título/canal/data/duração/id externo/thumb), args de qualidade ≤720p H.264/AAC `--merge-output-format mp4`. +- **`src/utilities/speech/webSpeechIngest.ts`** (novo): pipeline por achado com deps injetadas (downloader, transcriber, classifier, runner de ffmpeg) — reusa os donos do D5 e devolve o resultado por achado (novo/atualizado/ignorado/falha com estágio). +- **`src/utilities/speech/speechImport.ts`** (editar): bundle + state com os campos web opcionais; `upsertSpeechBundle` aceita `req` de transação aberta (Câmara sem `req` segue abrindo a própria). +- **`scripts/lib/mediaBinaries.mjs`** (novo): extração de `runProcess`/`probeFfmpeg`/`resolveFfmpeg`; `scripts/lib/reelFfmpeg.mjs` re-exporta sem mudar a API. +- **`scripts/lib/webSpeeches.mjs`** (novo, puro): plano do lote (dedupe/ordem/skip por estado), agregação e linhas do relatório. +- **`scripts/import-web-speeches.mjs`** (novo): CLI (`--findings ` — interface do C218 —, `--out`, `--limit`, `--dry-run`, `--reprocess`), guardas D9, caches de download no padrão `ensureCachedDownload`, relatório D8. +- **`package.json`** (editar): script `falas-web:import` (padrão do `camara:import`). +- **`.gitignore`** (editar): `data/falas-web/`. +- **`payload.config.ts`** (editar): registrar `InternetSpeechMedia` na lista de collections e `internetSpeechMedia: true` no plugin S3. +- **`scripts/lib/e2e-affected-manifest.mjs`** (editar): prefixos do D13. +- **Migration:** `pnpm migrate:create add_web_speech_origin` — enum `origin`/`platform`, colunas em `speech` (`origin` NOT NULL DEFAULT `'camara'`, demais nullable), tabela `internet_speech_media` + FKs + índices (`origin`, `platform`) e as rels de locked documents. Revisar o SQL gerado; **não** criar índice trigram novo (o `speech_search_text_trgm_idx` da C154 cobre as duas origens). **HARD-STOP humano (mesmo em `--auto`), local-only** antes de criar/aplicar; nunca editar migrations antigas. +- **Access / Consent:** `read = canReadSpeech`; `create/update/delete = payloadAdminOnly`; o CLI/pipeline usam `overrideAccess: true` com comentário justificado (ator confiável sem sessão, padrão do import da Câmara). Sem `Consent` novo (sem PII/opt-in; menções continuam texto). +- **UI:** N/A — sem UI (C216 é dono da tela; Impeccable A no plano de intenção). + +### Dados → forma (se aplicável) + +- **N/A por decisão da intenção** (`acervo-falas-web-ingestao.md:35`): aqui nenhum agregado é apresentado; o relatório de execução é insumo operacional de quem roda o CLI, não superfície de usuário — sem dashboard de vaidade nem score. Nada a apresentar; nenhuma forma nova. + +## Fases verificáveis + +1. **Tracer / schema + server** (~1 dia). + - `src/lib/webSpeech.ts`; `InternetSpeechMedia`; campos/`beforeDelete` no `Speech`; registro em `payload.config.ts`; `pnpm generate:types`; `upsertSpeechBundle`/`findSpeechImportState` estendidos. + - Unit: schema do lote (envelope/finding, `mediaUrl` obrigatória em radio|audio), `webSpeechSourceKey`, URL canônica, labels. + - Int: round-trip dos campos web (create/update), idempotência do upsert, `manual` preservado, cascade da mídia no delete do Speech. + - **Migration `add_web_speech_origin` — HARD-STOP humano, local-only** (confirmação explícita antes de criar/aplicar). +2. **Aquisição + pipeline** (~1 dia). + - `scripts/lib/mediaBinaries.mjs` (extração; unit dos reels verde); `src/utilities/media/ytdlp.ts`; `src/utilities/speech/webSpeechIngest.ts`. + - Unit: resolução fail-closed do yt-dlp + parse do metadata (fake via `YTDLP_PATH`); args de qualidade; plano do pipeline. + - Int: pipeline com fakes (`YTDLP_PATH` fake, transcriber/classificador injetados, `FFMPEG_PATH` fake) — happy path com espelho + segmentos + searchText; falha de download/ASR → falha no relatório sem row fantasma; `--reprocess` re-baixa/re-transcreve; segunda execução não re-baixa nem re-transcreve. +3. **CLI + relatório** (~0,75 dia). + - `scripts/lib/webSpeeches.mjs` + `scripts/import-web-speeches.mjs`; `package.json`; `.gitignore`. + - Unit: dedupe por sourceKey, plano (novo/atualizar/ignorar), agregação do relatório, falhas por estágio. + - Int/manual: `--dry-run` sem rede e sem escrita (lê estado do banco); guardas (`FALAS_WEB_IMPORT_CONFIRM`/`assertLocalDatabase`); JSON em `/reports/.json`. +4. **Blindagem Câmara** (~0,25 dia). + - `origin: camara` em `buildSpeechFacetWhere` (cobre `buildSpeechListWhere` e `buildSpeechListWhereIncludingCutOrigins`), `loadSpeechFilterOptions`, `speechCoverage.ts` (SQL) e `findSpeechExcerpts.ts`. + - Unit: where/coverage com o discriminador; int: fala web não aparece na lista, nas opções, na cobertura nem no Sollinha; cobertura da Câmara inalterada. +5. **Testes + gates** (~0,5 dia). + - Manifest e2e; `pnpm gate:fast`; `pnpm test` (unit + int); `pnpm test:e2e:affected` (se o classifier selecionar specs do acervo); `pnpm push`. + +Quota: ~3,5 dias eng + tempo de máquina (download/ASR), dentro do appetite herdado. Se apertar, o corte é o enriquecimento de metadados pelo `--dump-json` (o lote já traz os campos) — nunca as garantias: idempotência, guardas, relatório e blindagem. + +## Rabbit holes / Não escopo (engenharia) + +- Descoberta/crawler/monitoramento e agendamento (C218); varredura de perfis. +- TikTok/Facebook/X, gallery-dl, login/cookies para conteúdo privado. +- Segundo pipeline de ASR/classificação/upsert; segundo serving de mídia (C216/C217); segundo resolver de ffmpeg. +- Publicar mídia/URL pública; reusar `media` pública; reusar `recordingMedia`; signed URLs. +- Transcode/qualidade máxima/masters; geração de thumbnail (só armazena a da origem quando o lote/yt-dlp trouxer; C216 mostra placeholder sem ela); ffprobe. +- UI (C216), cortes (C217), paridade de filtros (C219), diarização, edição de transcrição, publicação. +- Merge multi-fonte/busca global; item de nav novo; tocar o feed do site ou a Central de Conteúdos (C211). +- Dedupe entre plataformas (mesma fala no YouTube e no Instagram) — identidade é por origem; cross-platform é item próprio. +- Limpeza de mídia órfã fora do cascade do Speech; retenção/expurgo. +- Alterar `sourceKey`/dados da Câmara; editar migrations antigas; `push` de schema. + +## Riscos e mitigação + +- **yt-dlp ausente ou quebrado por mudança de plataforma.** Fail-closed com hint de instalação; falha por achado no relatório com estágio; rerodar após atualizar; testes injetam fake por `YTDLP_PATH`. +- **Instagram bloqueia/expira sem login.** Falha honesta no relatório; fluxo de cookies/login fora da v1 (declarado); o lote pode trazer `mediaUrl` quando a descoberta tiver o arquivo direto. +- **Custo/tempo de ASR em horas de vídeo.** `--limit` e `--dry-run`; relatório com segundos e custo; o cap de qualidade evita baixar master (o ASR extrai áudio, não precisa de 4K). +- **Disco efêmero em produção (`internetSpeechMedia: true` esquecido).** Registro no plugin no mesmo PR + checklist de aceite; `resolveS3StorageEnv` falha fechado em config S3 parcial; dev/test em disco local. +- **Vazamento da fala web para as superfícies Câmara (crédito errado).** D10 + pins unit/int; a fala web nasce invisível à lista/opções/cobertura/Sollinha. +- **Duplicata por URL não canônica (tracking params, host, barra final).** `canonicalWebSpeechUrl` unit-pinada + dedupe no lote + `sourceKey` namespaced (índice unique já existente). +- **Migração em prod (coluna NOT NULL).** `origin` nasce com default `'camara'` na migration — linhas existentes cobertas; HARD-STOP humano; nunca editar migrations antigas; o deploy aplica pendentes. +- **Regressão da Câmara pelo upsert estendido.** Campos web opcionais; `metadataData` da Câmara intocado; os int existentes (`speechImport.int.spec.ts`) são o pino. +- **Curadoria manual sobrescrita em `--reprocess`.** Hook `preserveManualFacets` é o pino; int cobre o re-run sobre registro `manual`. +- **PR high-risk (migration) roda só o conjunto curado de e2e**, que não inclui `campaignSpeechAcervo` — a blindagem e o pipeline ficam pinados por unit/int; incluir o spec no curado é mudança deliberada de pin, a decidir se a cobertura e2e for exigida. +- **Objeto S3 órfão em rollback da transação de upload+upsert.** Best-effort de limpeza no catch; órfão é inofensivo (bucket privado) e reexecutar reusa/sobrescreve. + +## Aceite de engenharia + +- [ ] Aceite de produto da intenção ainda coberto: cada fala com data/duração/transcrição com timestamps/facetas/menções/proveniência/origem/canal; mídia espelhada privada e preservada; reexecução não duplica nem sobrescreve curadoria; relatório honesto (achados/novos/atualizados/ignorados/falhas/tempo/custo ASR); sem espelho público, sem segundo cadastro, sem publicação. +- [ ] Invariantes AGENTS/engineering-standards: `overrideAccess: false` + `user` onde há ator (leitura C216/C217 nasce no C216; aqui o CLI é ator confiável com bypass justificado); escrita multi-collection em transação (`withPayloadTransaction` + `req`); sem `Consent`/PII novos; `sourceKey` da Câmara intacto; sem rota/URL pública nova; migração nova sem editar antigas; copy pt-BR / identificadores em inglês; `pnpm gate:fast` e `pnpm push`. +- [ ] Testes de domínio previstos (unit/int) onde access/write paths mudam: schema/sourceKey/URL canônica; upsert estendido (round-trip, idempotência, `manual`, mídia privada); pipeline com fakes; cascade da mídia; blindagem Câmara (lista/opções/cobertura/Sollinha); reels unit verde após a extração do resolver. +- [ ] Migration `add_web_speech_origin` com HARD-STOP humano (mesmo em `--auto`), local-only, revisada antes de aplicar. +- [ ] `internetSpeechMedia: true` no plugin S3 (sem isso, produção cai no disco efêmero). +- [ ] Manifest e2e atualizado (`src/lib/webSpeech`, `src/collections/Speech.ts`, `src/collections/InternetSpeechMedia.ts`) para não cair no smoke fallback. + +## Execução — desvios do plano (2026-09-24) + +- **Thumbnail fora da transação principal (D5 refinado).** O upload da capa roda em best-effort **antes** da transação (media + upsert) e é limpo se a transação falhar: uma imagem quebrada (o sharp do Payload reprocessa imagens) nunca derruba a fala — o teste int cobre o caminho com um JPEG real. No `--reprocess`, se a capa nova falhar, a antiga é preservada (só é apagada quando há substituta). +- **`publishedAt` é obrigatório no schema** (o aceite exige data por fala) e um valor com fuso explícito é convertido para `America/Bahia` antes de virar `speechAt`; data impossível falha o achado em vez de virar mentira no card. +- **Pós-simplify:** o `defaultRun` do `reelFfmpeg` (referência órfã após a extração) foi reconectado; `downloadUrlToFile` ganhou teto de bytes (4 GiB); `databaseTarget`/`assertWriteConfirm` viraram helpers do `scripts/lib/cli.mjs` usados pelos dois CLIs; o predicado de completude foi para `src/lib/webSpeech.ts`; a constante de custo do ASR passou a ter dono único (o CLI da Câmara re-exporta a do app). +- **`ytDlpBinary` não usa o resolver de `mediaBinaries.mjs`.** Não existe binário empacotado de yt-dlp; o dono `src/utilities/media/ytdlp.ts` resolve `YTDLP_PATH` → PATH com probe `--version` fail-closed. O resolver extraído de `reelFfmpeg.mjs` continua sendo usado só para o ffmpeg (o script resolve com fallback empacotado e seta `FFMPEG_PATH`). +- **`upsertSpeechBundle` ganhou `options.req`** e `origin` no bundle (default `'camara'` no create) em vez de um writer novo — o caminho Câmara não mudou. +- **Testes existentes pinados:** `speechListFilters.unit.spec.ts` e `speechExcerptsTool.unit.spec.ts` passam a esperar `origin: camara`; os creates de `speech` em int/e2e ganharam `origin: 'camara'` (o campo é required no tipo gerado). +- **`scripts/lib/mediaBinaries.mjs` e `scripts/lib/webSpeeches.mjs` entraram no `SCRIPTS_SPEC_PINNED`** (invariante OPS119++ de blast radius). +- **Smoke local:** `pnpm falas-web:import --findings … --dry-run` (plano com inválido/duplicado) e a execução real com yt-dlp ausente (falha honesta no relatório + exit 1) rodados contra o banco local do worktree. O run real com mídia é do operador (C218) — a esteira não é executada em produção por este PR. + +## Self-score de decision-quality + +**4,5/5.** + +- **Decisões caras com rejeitadas (1):** as 13 decisões registradas têm opções e rejeições ancoradas em file:line/contrato (`sourceKey` unique da Câmara, relação fixa `SpeechCut.speech`, `preserveManualFacets`, `reelFfmpeg` já testado, `resolveS3StorageEnv` fail-closed, `push: false`, ausência de yt-dlp no repo). +- **Cabe no appetite (2):** schema+server → aquisição/pipeline → CLI/relatório → blindagem → testes/gates ≈3,5 dias eng + tempo de máquina, dentro dos ~3–4 dias herdados; tracer cedo no schema/upsert antes do pipeline. +- **Rabbit holes nomeados (3):** descoberta/crawler, plataformas fora da v1, segundo pipeline/serving, transcode/master, merge multi-fonte, dedupe cross-platform, limpeza de órfãos. +- **Depth check (4):** reusa `upsertSpeechBundle`, `deepInfraTranscribeSegments`, `classifySpeech`, `recordingTranscription`, `privateMediaResponse`, `withPayloadTransaction`, `ensureCachedDownload`, `assertLocalDatabase`; extrai o resolver de binário para um dono neutro em vez de criar um segundo; nada de módulo top-level novo em `src/utilities/`. +- **Outcome preservado (5):** a engenharia não reescreveu o produto — a fala web entra no catálogo com as mesmas facetas, privada, idempotente e com relatório honesto; o que ficou de fora (tela, cortes, descoberta) é exatamente o que a intenção cortou. + +Perde 0,5 porque duas pontas ficam condicionadas a confirmação humana/execução fora do código: o HARD-STOP da migration (deliberado) e a cobertura e2e do PR high-risk (conjunto curado não inclui `campaignSpeechAcervo`; a blindagem fica pinada por unit/int) — o plano declara isso em vez de fingir que está coberto. diff --git a/package.json b/package.json index ef1ab891..73c69482 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "images:fill": "node scripts/fill-image.mjs", "camara:pilot": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" node scripts/pilot-camara-speeches.mjs", "camara:import": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm --import=./scripts/seed-loader.mjs\" node scripts/import-camara-speeches.mjs", + "falas-web:import": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm --import=./scripts/seed-loader.mjs\" node scripts/import-web-speeches.mjs", "typecheck": "tsc --noEmit", "typecheck:tsc6": "tsc6 --noEmit", "prepare": "husky", diff --git a/scripts/import-camara-speeches.mjs b/scripts/import-camara-speeches.mjs index be9b90b2..0bf996a7 100644 --- a/scripts/import-camara-speeches.mjs +++ b/scripts/import-camara-speeches.mjs @@ -78,12 +78,11 @@ import { speechSourceKey, } from './lib/camaraSpeeches.mjs' import { - databaseHostname, + assertWriteConfirm, + databaseTarget, dieWithLabel, ensureCachedDownload, - isTruthyEnv, loadCliEnv, - requiresWriteConfirm, } from './lib/cli.mjs' loadCliEnv() @@ -1149,14 +1148,6 @@ const runRepairLinks = async (payload, options) => { } } -const databaseTarget = () => { - const url = process.env.DATABASE_URL - const host = databaseHostname(url) - if (host === null) return '(DATABASE_URL ausente ou inválida)' - // `databaseHostname` only returns non-null when `new URL` already parsed. - return `${host}${new URL(url).pathname}` -} - const modeLabel = (options) => { if (options.coverage) return 'coverage (read-only)' if (options.verifyLinks !== null) return `verify-links n=${options.verifyLinks} (read-only)` @@ -1168,21 +1159,6 @@ const modeLabel = (options) => { return `legislature ${options.legislature}` } -/** - * C155 write guard: the homeserver env file sets `NODE_ENV=production` and the - * runbook rewrites the DB host to the local proxy, so the host check alone - * cannot see production — the explicit flag is what makes the write deliberate. - */ -const assertWriteAllowed = () => { - if (!requiresWriteConfirm()) return - if (isTruthyEnv(process.env[WRITE_CONFIRM_FLAG])) return - die( - `alvo de escrita não-local/produção detectado (${databaseTarget()}).\n` + - `Confirme a intenção com:\n ${WRITE_CONFIRM_FLAG}=1 pnpm camara:import …\n` + - `Runbook: docs/ops/teqo-1313-deploy.md §C155/§C160.`, - ) -} - async function main() { const options = parseArgs(process.argv.slice(2)) if (options.help) { @@ -1197,7 +1173,12 @@ async function main() { } else { // Confirm first: a non-local host is exactly what the flag is for; the // local-DB guard below still blocks a remote host without ALLOW_REMOTE_DB. - assertWriteAllowed() + assertWriteConfirm({ + label: 'camara:import', + flag: WRITE_CONFIRM_FLAG, + command: 'pnpm camara:import', + hint: 'Runbook: docs/ops/teqo-1313-deploy.md §C155/§C160.', + }) assertLocalDatabase( 'camara:import', 'O import escreve no acervo; produção exige CAMARA_IMPORT_CONFIRM=1 (runbook §C155/§C160).', diff --git a/scripts/import-web-speeches.mjs b/scripts/import-web-speeches.mjs new file mode 100644 index 00000000..081a90db --- /dev/null +++ b/scripts/import-web-speeches.mjs @@ -0,0 +1,240 @@ +/** + * C215 — ingests a batch of web-speech findings (the discovery output of C218) + * into the acervo: validate the batch → acquire the media (yt-dlp for + * YouTube/Instagram, direct HTTP for radio/audio) → transcribe with timestamps + * → classify → mirror the artifact privately → upsert idempotently. Re-running + * never duplicates and never overwrites a manual facet curation; every run + * prints and writes an honest report (found/new/updated/skipped/failures, + * time and ASR cost). + * + * The report lands under `data/falas-web/reports/` (gitignored); the mirrored + * media goes to the private collection (temp files are removed after each + * finding). The local-DB guard refuses a non-local DATABASE_URL, and any write + * run targeting production (or a remote/override DB) additionally requires + * `FALAS_WEB_IMPORT_CONFIRM=1`. + * + * Usage: + * pnpm falas-web:import --findings data/falas-web/lote.json + * pnpm falas-web:import --findings lote.json --dry-run + * pnpm falas-web:import --findings lote.json --limit 3 + * pnpm falas-web:import --findings lote.json --reprocess + * FALAS_WEB_IMPORT_CONFIRM=1 pnpm falas-web:import --findings lote.json + */ +import { mkdir, readFile, writeFile } from 'node:fs/promises' +import { dirname, join } from 'node:path' + +import { getPayload } from 'payload' + +import { assertLocalDatabase } from './assert-local-database.mjs' +import { assertWriteConfirm, databaseTarget, dieWithLabel, loadCliEnv } from './lib/cli.mjs' +import { resolveFfmpeg } from './lib/mediaBinaries.mjs' +import { + formatWebSpeechReport, + planWebSpeechBatch, + summarizePlannedActions, + summarizeWebSpeechResults, + webSpeechPlannedAction, + webSpeechReportStamp, +} from './lib/webSpeeches.mjs' + +loadCliEnv() + +const die = dieWithLabel('falas-web:import') + +const config = (await import('../src/payload.config.ts')).default +const { parseWebSpeechBatch } = await import('../src/lib/webSpeech.ts') +const { findSpeechImportState } = await import('../src/utilities/speech/speechImport.ts') +const { ingestWebSpeech } = await import('../src/utilities/speech/webSpeechIngest.ts') + +const WRITE_CONFIRM_FLAG = 'FALAS_WEB_IMPORT_CONFIRM' +const DEFAULT_OUT_DIR = 'data/falas-web' + +// --------------------------------------------------------------------------- +// CLI +// --------------------------------------------------------------------------- + +const parseArgs = (argv) => { + const options = { + findings: null, + out: DEFAULT_OUT_DIR, + limit: null, + dryRun: false, + reprocess: false, + help: false, + } + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index] + const value = () => { + const next = argv[++index] + if (next === undefined || next.startsWith('--')) die(`faltou valor para ${arg}.`) + return next + } + if (arg === '--help' || arg === '-h') options.help = true + else if (arg === '--findings') options.findings = value() + else if (arg === '--out') options.out = value() + else if (arg === '--limit') options.limit = Number(value()) + else if (arg === '--dry-run') options.dryRun = true + else if (arg === '--reprocess') options.reprocess = true + else die(`argumento desconhecido: ${arg}`) + } + if (options.help) return options + if (!options.findings) die('informe --findings (veja --help).') + if (options.limit !== null && (!Number.isInteger(options.limit) || options.limit < 1)) { + die('--limit deve ser >= 1.') + } + if (options.out.includes('..')) die('--out não pode escapar do diretório do repo (sem "..").') + return options +} + +const HELP = ` +Uso: pnpm falas-web:import --findings [opções] + +O lote é o JSON da descoberta (C218): { generatedAt?, findings: [...] }. +Cada achado: platform (youtube|instagram|radio|audio), url, publishedAt e — +para radio|audio — mediaUrl (arquivo direto). externalId/title/channel/ +durationSeconds/thumbnailUrl são opcionais e o yt-dlp completa lacunas. + +Opções: + --findings lote de achados (obrigatório) + --out diretório dos relatórios (default data/falas-web) + --limit processa no máximo n achados + --dry-run planeja o lote (lê o catálogo) sem baixar nem escrever + --reprocess refaz download/transcrição mesmo com item completo + --help esta ajuda + +Sucesso parcial sai com código 0 (as falhas ficam no relatório); código 1 +quando nenhum achado entrou no catálogo. + +Escrita em alvo não-local ou com NODE_ENV=production exige +${WRITE_CONFIRM_FLAG}=1. O binário do yt-dlp vem de YTDLP_PATH ou do PATH; +instale com \`pipx install yt-dlp\` (o ffmpeg empacotado é usado se não houver +no PATH). +` + +const writeReport = async (options, report) => { + const reportPath = join( + options.out, + 'reports', + `falas-web-${webSpeechReportStamp(report.runAt)}.json`, + ) + await mkdir(dirname(reportPath), { recursive: true }) + await writeFile(reportPath, JSON.stringify(report, null, 2)) + return reportPath +} + +/** The packaged ffmpeg fallback keeps the operator flow working without a system binary. */ +const ensureFfmpeg = async () => { + if (process.env.FFMPEG_PATH?.trim()) return + const resolved = await resolveFfmpeg({ + audio: true, + filters: [], + requirementsLabel: 'libmp3lame para extrair o áudio', + }) + process.env.FFMPEG_PATH = resolved.bin + console.log(`[falas-web:import] ffmpeg: ${resolved.bin} (${resolved.source})`) +} + +async function main() { + const options = parseArgs(process.argv.slice(2)) + if (options.help) { + console.log(HELP) + process.exit(0) + } + + if (options.dryRun) { + if (!process.env.DATABASE_URL) die('DATABASE_URL não definida; recusando continuar.') + } else { + assertWriteConfirm({ + label: 'falas-web:import', + flag: WRITE_CONFIRM_FLAG, + command: 'pnpm falas-web:import', + }) + assertLocalDatabase( + 'falas-web:import', + `A ingestão escreve no acervo; produção exige ${WRITE_CONFIRM_FLAG}=1.`, + ) + } + console.log( + `[falas-web:import] alvo: ${databaseTarget()} | modo: ${options.dryRun ? 'dry-run (read-only)' : options.reprocess ? 'import (--reprocess)' : 'import'}`, + ) + + const envelope = parseWebSpeechBatch(JSON.parse(await readFile(options.findings, 'utf8'))) + if (!envelope.ok) die(envelope.error) + const plan = planWebSpeechBatch(envelope.batch.findings) + const entries = options.limit ? plan.entries.slice(0, options.limit) : plan.entries + if (envelope.batch.findings.length > 0 && entries.length === 0) { + die('nenhum achado válido no lote — corrija o JSON (inválidos/duplicados no relatório).') + } + const runAt = new Date().toISOString() + + const payload = await getPayload({ config }) + const startedAt = Date.now() + + if (options.dryRun) { + const planned = [] + for (const entry of entries) { + const state = await findSpeechImportState(payload, entry.sourceKey) + planned.push({ + sourceKey: entry.sourceKey, + platform: entry.finding.platform, + action: webSpeechPlannedAction(state, options.reprocess), + }) + } + const report = { + runAt, + mode: 'dry-run', + batchGeneratedAt: envelope.batch.generatedAt, + findings: envelope.batch.findings.length, + planned: entries.length, + invalid: plan.invalid, + duplicates: plan.duplicates, + plannedActions: summarizePlannedActions(planned), + plannedEntries: planned, + durationMs: Date.now() - startedAt, + } + const reportPath = await writeReport(options, report) + for (const line of formatWebSpeechReport({ ...report, failures: [] })) console.log(line) + console.log(`\n[falas-web:import] relatório JSON: ${reportPath}`) + process.exit(0) + } + + await ensureFfmpeg() + + const results = [] + for (const entry of entries) { + console.log(`[falas-web:import] ${entry.sourceKey}`) + const result = await ingestWebSpeech(payload, entry.finding, { reprocess: options.reprocess }) + results.push(result) + console.log( + ` → ${result.status}${result.stage ? ` (${result.stage})` : ''}${result.error ? `: ${result.error}` : ''}`, + ) + } + + const summary = summarizeWebSpeechResults(results) + const report = { + runAt, + mode: 'import', + batchGeneratedAt: envelope.batch.generatedAt, + findings: envelope.batch.findings.length, + planned: entries.length, + invalid: plan.invalid, + duplicates: plan.duplicates, + reprocess: options.reprocess, + ...summary, + durationMs: Date.now() - startedAt, + } + const reportPath = await writeReport(options, report) + for (const line of formatWebSpeechReport(report)) console.log(line) + console.log(`\n[falas-web:import] relatório JSON: ${reportPath}`) + + const succeeded = summary.created + summary.updated + summary.skipped + if (summary.failed > 0 && succeeded === 0) { + die('nenhum achado entrou no catálogo — falhas listadas no relatório.') + } + process.exit(0) +} + +main().catch((error) => { + console.error(error) + die(error?.message || String(error)) +}) diff --git a/scripts/lib/camaraFetch.mjs b/scripts/lib/camaraFetch.mjs index df72084b..85a53d1e 100644 --- a/scripts/lib/camaraFetch.mjs +++ b/scripts/lib/camaraFetch.mjs @@ -216,7 +216,12 @@ export async function resolveOfficialPdfUrl(legacyUrl, { timeoutMs = 180_000, at throw lastError } -export const DEEPINFRA_COST_PER_MINUTE_USD = 0.00045 +/** + * Provider price (~US$ per audio minute). The app-side owner is + * `src/utilities/ai/deepInfraTranscribe.ts` (C215) — re-exported here so the + * Câmara scripts and the web ingestion always report the same number. + */ +export { DEEPINFRA_TRANSCRIBE_COST_PER_MINUTE_USD as DEEPINFRA_COST_PER_MINUTE_USD } from '../../src/utilities/ai/deepInfraTranscribe.ts' /** * Sends the excerpt MP4 (video/mp4 is accepted; no ffmpeg) to Deep Infra's diff --git a/scripts/lib/cli.mjs b/scripts/lib/cli.mjs index 18f9532e..939629ba 100644 --- a/scripts/lib/cli.mjs +++ b/scripts/lib/cli.mjs @@ -194,6 +194,33 @@ export const requiresWriteConfirm = ({ allowRemoteDb = isRemoteDbOverrideSet(), } = {}) => nodeEnv === 'production' || allowRemoteDb || !isLocalDatabaseUrl(databaseUrl) +/** Human label of the write target for the guard messages (`host/name`). */ +export const databaseTarget = () => { + const url = process.env.DATABASE_URL + const host = databaseHostname(url) + if (host === null) return '(DATABASE_URL ausente ou inválida)' + return `${host}${new URL(url).pathname}` +} + +/** + * C155/C215 write guard shared by the import CLIs: a non-local/production + * target (or the `ALLOW_REMOTE_DB` escape) refuses the run unless the command's + * own intent flag is set. The homeserver env sets `NODE_ENV=production` and the + * proxy rewrites the DB host, so the explicit flag is what makes the write + * deliberate. + * + * @param {{ label: string, flag: string, command: string, hint?: string }} options + */ +export const assertWriteConfirm = ({ label, flag, command, hint = '' }) => { + if (!requiresWriteConfirm()) return + if (isTruthyEnv(process.env[flag])) return + dieWithLabel(label)( + `alvo de escrita não-local/produção detectado (${databaseTarget()}).\n` + + `Confirme a intenção com:\n ${flag}=1 ${command} …` + + (hint ? `\n${hint}` : ''), + ) +} + /** * Port for `next dev` (OPS40). Next's CLI resolves its port via commander's * `.env('PORT')` BEFORE `@next/env` loads `.env.local`, so a `PORT` written by diff --git a/scripts/lib/e2e-affected-manifest.mjs b/scripts/lib/e2e-affected-manifest.mjs index 6f63a5e3..cb1f3367 100644 --- a/scripts/lib/e2e-affected-manifest.mjs +++ b/scripts/lib/e2e-affected-manifest.mjs @@ -454,6 +454,11 @@ export const E2E_AFFECTED_MANIFEST = [ 'src/utilities/privateMedia', 'src/utilities/media', 'src/lib/speech', + // C215 — the web-speech contract, its collection and the private mirror + // collection drive the same acervo vertical. + 'src/lib/webSpeech', + 'src/collections/Speech.ts', + 'src/collections/InternetSpeechMedia.ts', 'src/lib/reel', 'src/lib/recording', 'src/lib/contentPiece', diff --git a/scripts/lib/mediaBinaries.mjs b/scripts/lib/mediaBinaries.mjs new file mode 100644 index 00000000..664a7d74 --- /dev/null +++ b/scripts/lib/mediaBinaries.mjs @@ -0,0 +1,149 @@ +/** + * Generic external-binary plumbing of the CLI tools: a process runner that + * never throws, a fail-closed capability probe and the ffmpeg resolution + * (`FFMPEG_PATH` → `ffmpeg` on PATH → the registry-packaged + * `@ffmpeg-installer/ffmpeg` binary). Extracted from the reel composer (C196) + * so the web-speech ingestion (C215) reuses the same owner instead of a second + * resolver. + * + * Resolution order (see `docs/plans/reels-tutoriais-impl.md`, Decisão 2): the + * packed fallback keeps the non-technical operator flow working on a + * workstation without a system ffmpeg; the homeserver Docker build installs it + * from npm (never from GitHub releases, which the homeserver cannot reach — see + * the sharp override note in `pnpm-workspace.yaml`). + */ + +import { execFile } from 'node:child_process' +import { promisify } from 'node:util' + +const execFileAsync = promisify(execFile) +const DEFAULT_MAX_BUFFER_BYTES = 16 * 1024 * 1024 + +/** + * @typedef {{ ok: boolean, stdout: string, stderr: string, error?: { code?: string } }} RunResult + * @typedef {(bin: string, args: string[]) => Promise} RunFn + */ + +/** + * Runs a process and returns the exit outcome instead of throwing. + * + * @type {(bin: string, args: string[]) => Promise} + */ +export const runProcess = async (bin, args) => { + try { + const { stdout, stderr } = await execFileAsync(bin, args, { + maxBuffer: DEFAULT_MAX_BUFFER_BYTES, + }) + return { ok: true, stdout, stderr } + } catch (error) { + return { ok: false, stdout: error.stdout ?? '', stderr: error.stderr ?? '', error } + } +} + +const defaultRun = runProcess + +const versionLine = (stdout) => + stdout.split('\n').find((line) => line.startsWith('ffmpeg version')) ?? '' + +/** + * Capability probe: the binary must run and ship every required encoder and + * filter. Missing capabilities are reported, never assumed. + * + * @param {string} bin + * @param {{ run?: RunFn, audio?: boolean, encoders?: string[], filters?: string[] }} [options] + * @returns {Promise<{ ok: true, bin: string, version: string } | { ok: false, bin: string, reason: string }>} + */ +export const probeFfmpeg = async ( + bin, + { run = defaultRun, audio = false, encoders = [], filters = [] } = {}, +) => { + const version = await run(bin, ['-hide_banner', '-version']) + if (!version.ok) { + return { ok: false, bin, reason: `não executou (${version.error?.code ?? 'erro'})` } + } + + const requiredEncoders = [...encoders, ...(audio ? ['libmp3lame'] : [])] + if (requiredEncoders.length > 0) { + const listed = await run(bin, ['-hide_banner', '-encoders']) + const missing = requiredEncoders.filter( + (encoder) => !listed.ok || !new RegExp(`\\b${encoder}\\b`).test(listed.stdout), + ) + if (missing.length > 0) return { ok: false, bin, reason: `sem encoder ${missing.join(', ')}` } + } + + if (filters.length > 0) { + const listed = await run(bin, ['-hide_banner', '-filters']) + if (!listed.ok) return { ok: false, bin, reason: 'não listou filtros' } + const missing = filters.filter((filter) => !new RegExp(`\\b${filter}\\b`).test(listed.stdout)) + if (missing.length > 0) + return { ok: false, bin, reason: `sem filtro(s): ${missing.join(', ')}` } + } + + return { ok: true, bin, version: versionLine(version.stdout) } +} + +const FFMPEG_INSTALL_HINT = + 'Defina FFMPEG_PATH, instale o ffmpeg (ex.: `sudo apt install ffmpeg`) ou rode `pnpm install` para o binário empacotado.' + +/** + * Resolves the first candidate that passes the probe. Explicit `FFMPEG_PATH` is strict. + * + * @param {{ env?: Record, run?: RunFn, installer?: { path?: string } | null, audio?: boolean, encoders?: string[], filters?: string[], requirementsLabel?: string, installHint?: string }} [options] + * @returns {Promise<{ ok: true, bin: string, version: string, source: string }>} + */ +export const resolveFfmpeg = async ({ + env = process.env, + run = defaultRun, + installer, + audio = false, + encoders = [], + filters = [], + requirementsLabel = '', + installHint = FFMPEG_INSTALL_HINT, +} = {}) => { + let packaged = installer + if (packaged === undefined) { + try { + packaged = (await import('@ffmpeg-installer/ffmpeg')).default + } catch { + // The packaged binary throws on import when its platform package is + // missing; the probe below decides, never the import. + packaged = null + } + } + const candidates = [] + const explicit = env.FFMPEG_PATH?.trim() + if (explicit) candidates.push({ bin: explicit, source: 'FFMPEG_PATH', strict: true }) + candidates.push({ bin: 'ffmpeg', source: 'PATH', strict: false }) + if (packaged?.path) candidates.push({ bin: packaged.path, source: 'empacotado', strict: false }) + + const failures = [] + for (const candidate of candidates) { + const probe = await probeFfmpeg(candidate.bin, { run, audio, encoders, filters }) + if (probe.ok) { + return { ok: true, bin: probe.bin, version: probe.version, source: candidate.source } + } + failures.push(`${candidate.source}: ${probe.reason}`) + if (candidate.strict) break + } + + const suffix = requirementsLabel ? ` (${requirementsLabel})` : '' + throw new Error(`Nenhum ffmpeg utilizável${suffix}. ${failures.join('; ')}. ${installHint}`) +} + +/** + * Runs a process and fails with the stderr tail — never swallows the exit + * code. Shared by the CLI tools (ffmpeg and edge-tts). + * + * @param {string} bin + * @param {string[]} args + * @param {{ run?: RunFn, label?: string }} [options] + */ +export const runTool = async (bin, args, { run = defaultRun, label = 'processo' } = {}) => { + const result = await run(bin, args) + if (!result.ok) { + const tail = (result.stderr || result.error?.message || '').trim().slice(-400) + throw new Error(`${label} falhou${tail ? `: ${tail}` : ''}`) + } + return result +} diff --git a/scripts/lib/reelFfmpeg.mjs b/scripts/lib/reelFfmpeg.mjs index d098b125..a75cd1d1 100644 --- a/scripts/lib/reelFfmpeg.mjs +++ b/scripts/lib/reelFfmpeg.mjs @@ -1,29 +1,24 @@ /** - * C196 — ffmpeg plumbing of the reel composer: binary resolution with a - * fail-closed capability probe, the per-scene clip command (zoompan + burned - * caption overlays) and the concat that stitches the scenes into `reel.mp4`. - * - * Resolution order (see `docs/plans/reels-tutoriais-impl.md`, Decisão 2): - * `FFMPEG_PATH` → `ffmpeg` on PATH → the registry-packaged - * `@ffmpeg-installer/ffmpeg` binary. The packed fallback keeps the - * non-technical operator flow working on a workstation without a system - * ffmpeg; the homeserver Docker build installs it from npm (never from GitHub - * releases, which the homeserver cannot reach — see the sharp override note in - * `pnpm-workspace.yaml`). + * C196 — ffmpeg plumbing of the reel composer: the per-scene clip command + * (zoompan + burned caption overlays) and the concat that stitches the scenes + * into `reel.mp4`. The generic binary resolution/probe/runner (C215 moved them + * to `mediaBinaries.mjs`) is re-exported here so the reel tools and their unit + * pins keep the same import surface; the reel requirements (libx264 + the + * composer filters, `atempo` when audio) are injected into the wrappers. */ -import { execFile } from 'node:child_process' import { writeFile } from 'node:fs/promises' -import { promisify } from 'node:util' +import { + probeFfmpeg as probeFfmpegBin, + resolveFfmpeg as resolveFfmpegBin, + runProcess, + runTool, +} from './mediaBinaries.mjs' import { NARRATION_PCM } from './reelScript.mjs' import { seconds } from './reelTimeline.mjs' -const execFileAsync = promisify(execFile) -const MAX_BUFFER_BYTES = 16 * 1024 * 1024 - const REQUIRED_FILTERS = ['fade', 'overlay', 'scale', 'zoompan'] -const AUDIO_REQUIRED_ENCODER = 'libmp3lame' const AUDIO_REQUIRED_FILTERS = ['atempo'] export const REEL_PRESET = 'medium' export const REEL_CRF = 18 @@ -38,25 +33,7 @@ export const PREVIEW_CRF = 14 * @typedef {(bin: string, args: string[]) => Promise<{ ok: boolean, stdout: string, stderr: string, error?: { code?: string } }>} RunFn */ -/** - * Generic process runner shared by the reel tools (ffmpeg and edge-tts): never - * throws, returns the exit outcome. - * - * @type {RunFn} - */ -export const runProcess = async (bin, args) => { - try { - const { stdout, stderr } = await execFileAsync(bin, args, { maxBuffer: MAX_BUFFER_BYTES }) - return { ok: true, stdout, stderr } - } catch (error) { - return { ok: false, stdout: error.stdout ?? '', stderr: error.stderr ?? '', error } - } -} - -const defaultRun = runProcess - -const versionLine = (stdout) => - stdout.split('\n').find((line) => line.startsWith('ffmpeg version')) ?? '' +export { runProcess, runTool } /** * Capability probe: the binary must encode H.264 (libx264) and ship the @@ -66,90 +43,28 @@ const versionLine = (stdout) => * @param {{ run?: RunFn, audio?: boolean }} [options] * @returns {Promise<{ ok: true, bin: string, version: string } | { ok: false, bin: string, reason: string }>} */ -export const probeFfmpeg = async (bin, { run = defaultRun, audio = false } = {}) => { - const version = await run(bin, ['-hide_banner', '-version']) - if (!version.ok) - return { ok: false, bin, reason: `não executou (${version.error?.code ?? 'erro'})` } - const encoders = await run(bin, ['-hide_banner', '-encoders']) - if (!encoders.ok || !/\blibx264\b/.test(encoders.stdout)) { - return { ok: false, bin, reason: 'sem encoder libx264' } - } - if (audio && !new RegExp(`\\b${AUDIO_REQUIRED_ENCODER}\\b`).test(encoders.stdout)) { - return { ok: false, bin, reason: `sem encoder ${AUDIO_REQUIRED_ENCODER}` } - } - const filters = await run(bin, ['-hide_banner', '-filters']) - if (!filters.ok) return { ok: false, bin, reason: 'não listou filtros' } - const requiredFilters = audio - ? [...REQUIRED_FILTERS, ...AUDIO_REQUIRED_FILTERS] - : REQUIRED_FILTERS - const missing = requiredFilters.filter( - (filter) => !new RegExp(`\\b${filter}\\b`).test(filters.stdout), - ) - if (missing.length > 0) return { ok: false, bin, reason: `sem filtro(s): ${missing.join(', ')}` } - return { ok: true, bin, version: versionLine(version.stdout) } -} - -const installHint = - 'Defina FFMPEG_PATH, instale o ffmpeg (ex.: `sudo apt install ffmpeg`) ou rode `pnpm install` para o binário empacotado.' +export const probeFfmpeg = (bin, { audio = false, ...options } = {}) => + probeFfmpegBin(bin, { + ...options, + audio, + encoders: ['libx264'], + filters: [...REQUIRED_FILTERS, ...(audio ? AUDIO_REQUIRED_FILTERS : [])], + }) /** - * Resolves the first candidate that passes the probe. Explicit `FFMPEG_PATH` is strict. + * Resolves the first candidate that passes the reel probe. Explicit + * `FFMPEG_PATH` is strict. * * @param {{ env?: Record, run?: RunFn, installer?: { path?: string } | null, audio?: boolean }} [options] * @returns {Promise<{ ok: true, bin: string, version: string, source: string }>} */ -export const resolveFfmpeg = async ({ - env = process.env, - run = defaultRun, - installer, - audio = false, -} = {}) => { - let packaged = installer - if (packaged === undefined) { - try { - packaged = (await import('@ffmpeg-installer/ffmpeg')).default - } catch { - // The packaged binary throws on import when its platform package is - // missing; the probe below decides, never the import. - packaged = null - } - } - const candidates = [] - const explicit = env.FFMPEG_PATH?.trim() - if (explicit) candidates.push({ bin: explicit, source: 'FFMPEG_PATH', strict: true }) - candidates.push({ bin: 'ffmpeg', source: 'PATH', strict: false }) - if (packaged?.path) candidates.push({ bin: packaged.path, source: 'empacotado', strict: false }) - const failures = [] - for (const candidate of candidates) { - const probe = await probeFfmpeg(candidate.bin, { run, audio }) - if (probe.ok) { - return { ok: true, bin: probe.bin, version: probe.version, source: candidate.source } - } - failures.push(`${candidate.source}: ${probe.reason}`) - if (candidate.strict) break - } - throw new Error( - `Nenhum ffmpeg utilizável (libx264 + filtros do reel${audio ? ' + áudio' : ''}). ${failures.join('; ')}. ${installHint}`, - ) -} - -/** - * Runs a process and fails with the stderr tail — never swallows the exit - * code. Shared by the reel tools (ffmpeg and edge-tts); `runFfmpeg` is the - * ffmpeg-labelled alias kept for the historical call sites. - * - * @param {string} bin - * @param {string[]} args - * @param {{ run?: RunFn, label?: string }} [options] - */ -export const runTool = async (bin, args, { run = defaultRun, label = 'processo' } = {}) => { - const result = await run(bin, args) - if (!result.ok) { - const tail = (result.stderr || result.error?.message || '').trim().slice(-400) - throw new Error(`${label} falhou${tail ? `: ${tail}` : ''}`) - } - return result -} +export const resolveFfmpeg = (options = {}) => + resolveFfmpegBin({ + requirementsLabel: `libx264 + filtros do reel${options.audio ? ' + áudio' : ''}`, + ...options, + encoders: ['libx264'], + filters: [...REQUIRED_FILTERS, ...(options.audio ? AUDIO_REQUIRED_FILTERS : [])], + }) /** @type {typeof runTool} */ export const runFfmpeg = (bin, args, options = {}) => @@ -284,7 +199,7 @@ export const encodeFrames = async ({ preset = PREVIEW_PRESET, crf = PREVIEW_CRF, writeFileImpl = writeFile, - run = defaultRun, + run = runProcess, }) => { await writeFileImpl(listPath, buildFrameListContent({ frames, fps })) await runFfmpeg( diff --git a/scripts/lib/test-affected-core.mjs b/scripts/lib/test-affected-core.mjs index cd957b5f..d4ade2f3 100644 --- a/scripts/lib/test-affected-core.mjs +++ b/scripts/lib/test-affected-core.mjs @@ -82,6 +82,7 @@ export const SCRIPTS_SPEC_PINNED = [ 'scripts/lib/imageResize.mjs', 'scripts/lib/issues-panel.mjs', 'scripts/lib/markdown-ansi.mjs', + 'scripts/lib/mediaBinaries.mjs', 'scripts/lib/plansOnlyClosesGuard.mjs', 'scripts/lib/playwright-e2e-args.mjs', 'scripts/lib/portalTransparenciaEmendas.mjs', @@ -99,6 +100,7 @@ export const SCRIPTS_SPEC_PINNED = [ 'scripts/lib/staging-test-account.mjs', 'scripts/lib/test-affected-core.mjs', 'scripts/lib/testing-audit-metrics-core.mjs', + 'scripts/lib/webSpeeches.mjs', 'scripts/lib/worktree-env.mjs', 'scripts/lib/worktree.mjs', 'scripts/lib/wpArticles.mjs', diff --git a/scripts/lib/webSpeeches.mjs b/scripts/lib/webSpeeches.mjs new file mode 100644 index 00000000..fe222590 --- /dev/null +++ b/scripts/lib/webSpeeches.mjs @@ -0,0 +1,131 @@ +/** + * C215 — pure planning/reporting of the web-speech ingestion CLI: validate the + * batch (one invalid finding fails only itself), dedupe by natural key, decide + * the planned action per entry and aggregate the run report. No I/O: the unit + * specs read this module and `import-web-speeches.mjs` is the impure shell. + */ + +import { formatSpeechSpan } from '../../src/lib/speechClock.ts' +import { + isCompleteWebSpeechState, + parseWebSpeechFinding, + webSpeechSourceKey, +} from '../../src/lib/webSpeech.ts' + +/** @typedef {import('../../src/lib/webSpeech.ts').WebSpeechFinding} WebSpeechFinding */ +/** @typedef {import('../../src/utilities/speech/webSpeechIngest.ts').WebSpeechIngestResult} WebSpeechIngestResult */ + +export { isCompleteWebSpeechState } + +/** + * @param {unknown[]} findings + * @returns {{ entries: Array<{ index: number, sourceKey: string, finding: WebSpeechFinding }>, invalid: Array<{ index: number, error: string }>, duplicates: Array<{ index: number, sourceKey: string }> }} + */ +export const planWebSpeechBatch = (findings) => { + const entries = [] + const invalid = [] + const duplicates = [] + const seen = new Set() + + findings.forEach((raw, index) => { + const parsed = parseWebSpeechFinding(raw) + if (!parsed.ok) { + invalid.push({ index, error: parsed.error }) + return + } + const sourceKey = webSpeechSourceKey(parsed.finding) + if (seen.has(sourceKey)) { + duplicates.push({ index, sourceKey }) + return + } + seen.add(sourceKey) + entries.push({ index, sourceKey, finding: parsed.finding }) + }) + + return { entries, invalid, duplicates } +} + +/** What a re-run would do to an entry, from its stored state. */ +export const webSpeechPlannedAction = (state, reprocess = false) => { + if (state === null) return 'novo' + if (!isCompleteWebSpeechState(state)) return 'atualizado' + return reprocess ? 'reprocessado' : 'ignorado' +} + +/** @param {WebSpeechIngestResult[]} results */ +export const summarizeWebSpeechResults = (results) => { + const count = (status) => results.filter((result) => result.status === status).length + const sum = (pick) => results.reduce((total, result) => total + (Number(pick(result)) || 0), 0) + + return { + created: count('created'), + updated: count('updated'), + skipped: count('skipped'), + failed: count('failed'), + asrSeconds: sum((result) => result.asrSeconds), + asrCostUsd: sum((result) => result.asrCostUsd), + llmCostUsd: sum((result) => result.llmCostUsd), + failures: results + .filter((result) => result.status === 'failed') + .map((result) => ({ + sourceKey: result.sourceKey, + stage: result.stage ?? null, + error: result.error ?? null, + })), + } +} + +/** @param {Array<{ action: string }>} planned */ +export const summarizePlannedActions = (planned) => ({ + novo: planned.filter((entry) => entry.action === 'novo').length, + atualizado: planned.filter((entry) => entry.action === 'atualizado').length, + ignorado: planned.filter((entry) => entry.action === 'ignorado').length, + reprocessado: planned.filter((entry) => entry.action === 'reprocessado').length, +}) + +const secondsLabel = (seconds) => formatSpeechSpan(Math.round(seconds)) + +/** + * Human lines for stdout; the JSON file keeps the full report. Failures are + * never hidden — each one names its stage and reason. + * + * @param {ReturnType & { runAt: string, mode: string, findings: number, planned: number, invalid: Array<{index: number, error: string}>, duplicates: Array<{index: number, sourceKey: string}>, durationMs: number, plannedActions?: Record|null }} report + * @returns {string[]} + */ +export const formatWebSpeechReport = (report) => { + const label = '[falas-web:import]' + const lines = [] + lines.push( + `${label} achados: ${report.findings} · considerados: ${report.planned} · inválidos: ${report.invalid.length} · duplicados: ${report.duplicates.length}`, + ) + + if (report.plannedActions) { + const actions = report.plannedActions + lines.push( + `${label} plano (dry-run): novos: ${actions.novo} · atualizados: ${actions.atualizado} · ignorados: ${actions.ignorado} · reprocessados: ${actions.reprocessado}`, + ) + } else { + const summary = report + lines.push( + `${label} novos: ${summary.created} · atualizados: ${summary.updated} · ignorados: ${summary.skipped} · falhas: ${summary.failed}`, + ) + lines.push( + `${label} tempo: ${(report.durationMs / 1000).toFixed(1)}s · ASR: ${secondsLabel(summary.asrSeconds)} (~US$ ${summary.asrCostUsd.toFixed(4)}) · LLM: US$ ${summary.llmCostUsd.toFixed(4)}`, + ) + } + + for (const failure of report.invalid) { + lines.push(`${label} inválido #${failure.index}: ${failure.error}`) + } + if (report.failures?.length) { + lines.push(`${label} falhas:`) + for (const failure of report.failures) { + lines.push( + ` - ${failure.sourceKey} (${failure.stage ?? 'desconhecido'}): ${failure.error ?? 'erro'}`, + ) + } + } + return lines +} + +export const webSpeechReportStamp = (runAt) => runAt.replace(/[:.]/g, '-') diff --git a/src/collections/InternetSpeechMedia.ts b/src/collections/InternetSpeechMedia.ts new file mode 100644 index 00000000..1730dc89 --- /dev/null +++ b/src/collections/InternetSpeechMedia.ts @@ -0,0 +1,44 @@ +import type { CollectionConfig } from 'payload' + +import { INTERNET_SPEECH_MEDIA_SLUG } from '@/lib/webSpeech' +import { canReadSpeech, payloadAdminOnly } from '@/utilities/campaignAccess' + +/** + * C215 — private upload collection of a web speech's mirrored artifact (the + * media and the optional thumbnail). Like `recordingMedia`, deliberately NOT + * `media`: that collection is anonymous-read by contract (public pages), while + * a third-party speech file may only be read by the communication roles. The + * browser reaches the file through the authenticated acervo route (C216), + * never the public `/api/media/file` proxy. + */ + +export const InternetSpeechMedia: CollectionConfig = { + slug: INTERNET_SPEECH_MEDIA_SLUG, + labels: { + singular: 'Mídia de fala da internet', + plural: 'Mídias de falas da internet', + }, + admin: { + group: 'Comunicação', + description: + 'Arquivos privados das falas encontradas na internet (mídia espelhada e capa). Só abrem com login da campanha.', + }, + access: { + create: payloadAdminOnly, + read: canReadSpeech, + update: payloadAdminOnly, + delete: payloadAdminOnly, + }, + fields: [ + { + name: 'alt', + type: 'text', + label: 'Texto alternativo', + required: true, + admin: { + description: 'Descrição do arquivo para acessibilidade.', + }, + }, + ], + upload: true, +} diff --git a/src/collections/Speech.ts b/src/collections/Speech.ts index 840ab274..d97bf1bb 100644 --- a/src/collections/Speech.ts +++ b/src/collections/Speech.ts @@ -6,15 +6,20 @@ import type { } from 'payload' import { SPEECH_CLASSIFICATION_SOURCES, SPEECH_SCOPES, SPEECH_TOPICS } from '@/lib/speechFacets' +import { INTERNET_SPEECH_MEDIA_SLUG, WEB_SPEECH_PLATFORMS } from '@/lib/webSpeech' import { canReadSpeech, canUpdateSpeech, payloadAdminOnly } from '@/utilities/campaignAccess' /** - * Speech catalog (C153) — one record per Jorge Solla speech in the Câmara, - * with the official transcript (taquigrafia), the ASR segments with timestamps - * (speechSegment), curated facets and extracted mentions. + * Speech catalog (C153) — one record per Jorge Solla speech: the Câmara + * speeches with the official transcript (taquigrafia), ASR segments with + * timestamps (speechSegment), curated facets and extracted mentions, plus the + * web speeches (C215) mirrored from the internet with the same facets and + * segments. * - * Data provenance: Câmara open data + VOD, CC BY 4.0 — keep the credit. - * Identity is the API speech (`sourceKey`), never the regenerable VOD URL. + * Câmara data provenance: open data + VOD, CC BY 4.0 — keep the credit. Web + * rows carry their own origin (platform + URL) and a private mirrored file. + * Identity is the source (`sourceKey`): the Câmara API speech or the web + * namespace (`web::`), never a regenerable URL. */ const FACET_FIELDS = [ @@ -56,8 +61,22 @@ const preserveManualFacets: CollectionBeforeChangeHook = ({ return data } -/** Segments are owned by the speech; Payload relationships do not cascade. */ -const deleteSpeechSegments: CollectionBeforeDeleteHook = async ({ id, req }) => { +/** Speech assets are owned by the speech; Payload relationships do not cascade. */ +const deleteSpeechAssets: CollectionBeforeDeleteHook = async ({ id, req }) => { + const speech = await req.payload.findByID({ + collection: 'speech', + id, + depth: 0, + select: { mirroredMedia: true, thumbnail: true }, + req, + // Intentional bypass: the cascade is owned by this hook; media access is the + // acervo gate and this runs inside the authorized speech delete request. + overrideAccess: true, + }) + const mediaIds = [speech?.mirroredMedia, speech?.thumbnail] + .map((media) => (typeof media === 'object' && media !== null ? media.id : media)) + .filter((mediaId): mediaId is number => typeof mediaId === 'number') + await req.payload.delete({ collection: 'speechSegment', where: { speech: { equals: id } }, @@ -66,6 +85,15 @@ const deleteSpeechSegments: CollectionBeforeDeleteHook = async ({ id, req }) => // admin-only and this runs inside the authorized speech delete request. overrideAccess: true, }) + if (mediaIds.length > 0) { + await req.payload.delete({ + collection: INTERNET_SPEECH_MEDIA_SLUG, + where: { id: { in: mediaIds } }, + req, + // Intentional bypass: same cascade, now over the private mirrored files. + overrideAccess: true, + }) + } } export const Speech: CollectionConfig = { @@ -77,9 +105,9 @@ export const Speech: CollectionConfig = { admin: { group: 'Comunicação', useAsTitle: 'speechAt', - defaultColumns: ['speechAt', 'type', 'phase', 'topics', 'classifiedBy'], + defaultColumns: ['speechAt', 'origin', 'platform', 'type', 'topics', 'classifiedBy'], description: - 'Acervo de falas do deputado na Câmara. Dados e vídeos da Câmara dos Deputados (CC BY 4.0).', + 'Acervo de falas do deputado: discursos na Câmara (CC BY 4.0) e falas publicadas na internet (mídia espelhada privada).', }, access: { create: payloadAdminOnly, @@ -90,7 +118,7 @@ export const Speech: CollectionConfig = { hooks: { beforeValidate: [deriveSpeechYear], beforeChange: [preserveManualFacets], - beforeDelete: [deleteSpeechSegments], + beforeDelete: [deleteSpeechAssets], }, fields: [ { @@ -102,7 +130,24 @@ export const Speech: CollectionConfig = { index: true, admin: { readOnly: true, - description: 'Identidade da fala na API da Câmara (data/hora + tipo + fase).', + description: 'Identidade da fala: API da Câmara ou web::.', + }, + }, + { + // C215 — discriminator of the catalog source. The Câmara surfaces filter + // `camara`; the web source is served by C216 with its own list contract. + name: 'origin', + type: 'select', + label: 'Origem', + required: true, + defaultValue: 'camara', + index: true, + options: [ + { label: 'Câmara', value: 'camara' }, + { label: 'Internet', value: 'web' }, + ], + admin: { + description: 'Discurso da Câmara ou fala publicada na internet.', }, }, { @@ -267,6 +312,71 @@ export const Speech: CollectionConfig = { label: 'VOD (baixar)', admin: { readOnly: true, description: 'Último link conhecido; regerável pelo VOD.' }, }, + // C215 — web speech origin and mirrored assets (null on Câmara rows). + { + name: 'platform', + type: 'select', + label: 'Plataforma', + index: true, + options: WEB_SPEECH_PLATFORMS.map(({ value, label }) => ({ value, label })), + admin: { + description: 'Plataforma de origem da fala da internet.', + }, + }, + { + name: 'externalId', + type: 'text', + label: 'Id externo', + admin: { + readOnly: true, + description: 'Identificador da publicação na plataforma (quando houver).', + }, + }, + { + name: 'sourceUrl', + type: 'text', + label: 'URL de origem', + admin: { + readOnly: true, + description: 'Link canônico da publicação original.', + }, + }, + { + name: 'title', + type: 'text', + label: 'Título', + admin: { + description: 'Título da publicação na plataforma.', + }, + }, + { + name: 'channel', + type: 'text', + label: 'Canal/autor', + admin: { + description: 'Quem publicou (canal, rádio, perfil) — texto de origem, não é contato.', + }, + }, + { + name: 'mirroredMedia', + type: 'upload', + relationTo: INTERNET_SPEECH_MEDIA_SLUG, + label: 'Mídia espelhada', + admin: { + readOnly: true, + description: 'Arquivo privado preservado para player, download e cortes.', + }, + }, + { + name: 'thumbnail', + type: 'upload', + relationTo: INTERNET_SPEECH_MEDIA_SLUG, + label: 'Capa', + admin: { + readOnly: true, + description: 'Capa da origem quando capturada; sem ela a lista mostra placeholder.', + }, + }, { name: 'topics', type: 'select', diff --git a/src/lib/webSpeech.ts b/src/lib/webSpeech.ts new file mode 100644 index 00000000..860f4139 --- /dev/null +++ b/src/lib/webSpeech.ts @@ -0,0 +1,280 @@ +import { z } from 'zod' + +/** + * C215 — pure contract of the web-speech ingestion: the platform taxonomy, the + * finding-batch schema the discovery (C218) hands over, the natural key + * (`web::`) and the canonical URL normalization that + * keeps re-runs idempotent. No I/O: the CLI, the pipeline and the tests share + * this module. + */ + +export const WEB_SPEECH_PLATFORMS = [ + { value: 'youtube', label: 'YouTube' }, + { value: 'instagram', label: 'Instagram' }, + { value: 'radio', label: 'Rádio' }, + { value: 'audio', label: 'Áudio' }, +] as const + +export type WebSpeechPlatform = (typeof WEB_SPEECH_PLATFORMS)[number]['value'] + +const WEB_SPEECH_PLATFORM_VALUES = WEB_SPEECH_PLATFORMS.map((platform) => platform.value) as [ + WebSpeechPlatform, + ...WebSpeechPlatform[], +] + +export const webSpeechPlatformLabel = (value: WebSpeechPlatform): string => + WEB_SPEECH_PLATFORMS.find((platform) => platform.value === value)?.label ?? value + +/** Owner of the private upload collection slug (collection, config and S3). */ +export const INTERNET_SPEECH_MEDIA_SLUG = 'internetSpeechMedia' as const + +/** + * Platforms whose acquisition goes through yt-dlp. The others (`radio`, + * `audio`) are direct files: the finding must carry `mediaUrl` or the item + * fails honestly in the report — the script never guesses a page's media. + */ +const YTDLP_PLATFORMS: readonly WebSpeechPlatform[] = ['youtube', 'instagram'] + +export const requiresDirectMediaUrl = (platform: WebSpeechPlatform): boolean => + !YTDLP_PLATFORMS.includes(platform) + +const TRACKING_PARAMS = new Set(['fbclid', 'gclid', 'igshid', 'igsh', 'si', 'feature']) + +const isTrackingParam = (name: string): boolean => + TRACKING_PARAMS.has(name) || name.toLowerCase().startsWith('utm_') + +const stripTrackingParams = (url: URL): void => { + for (const name of [...url.searchParams.keys()]) { + if (isTrackingParam(name)) url.searchParams.delete(name) + } +} + +const withoutTrailingSlash = (pathname: string): string => + pathname.length > 1 && pathname.endsWith('/') ? pathname.replace(/\/+$/, '') : pathname + +const YOUTUBE_HOSTS = new Set(['youtube.com', 'm.youtube.com', 'music.youtube.com']) +const YOUTU_BE_HOSTS = new Set(['youtu.be']) + +const normalizedHost = (url: URL): string => url.hostname.toLowerCase().replace(/^www\./, '') + +const firstPathSegment = (pathname: string): string | null => + pathname.split('/').find((segment) => segment !== '') ?? null + +/** Video id from any YouTube URL shape (`watch`, `shorts`, `live`, `embed`, `youtu.be`). */ +const youtubeVideoId = (url: URL): string | null => { + const host = normalizedHost(url) + if (YOUTU_BE_HOSTS.has(host)) return firstPathSegment(url.pathname) + if (!YOUTUBE_HOSTS.has(host)) return null + if (url.pathname === '/watch') return url.searchParams.get('v') + const match = /^\/(?:shorts|live|embed)\/([^/?#]+)/.exec(url.pathname) + return match?.[1] ?? null +} + +/** Shortcode + kind (`reel`/`p`/`tv`) from any Instagram post URL. */ +const instagramPost = (url: URL): { kind: string; code: string } | null => { + if (normalizedHost(url) !== 'instagram.com') return null + const match = /^\/(p|reel|reels|tv)\/([^/?#]+)/.exec(url.pathname) + if (!match) return null + return { kind: match[1] === 'reels' ? 'reel' : match[1], code: match[2] } +} + +const parseUrl = (raw: string): URL | null => { + try { + return new URL(raw.trim()) + } catch { + return null + } +} + +/** + * Stable URL for identity and provenance: YouTube/Instagram collapse to the + * canonical post URL and every platform drops tracking params, the fragment + * and a trailing slash. An unparseable URL returns trimmed as-is (the finding + * schema validates the shape before this runs). + */ +export const canonicalWebSpeechUrl = (platform: WebSpeechPlatform, rawUrl: string): string => { + const url = parseUrl(rawUrl) + if (!url) return rawUrl.trim() + + if (platform === 'youtube') { + const videoId = youtubeVideoId(url) + if (videoId) return `https://www.youtube.com/watch?v=${videoId}` + } + if (platform === 'instagram') { + const post = instagramPost(url) + if (post) return `https://www.instagram.com/${post.kind}/${post.code}/` + } + + url.hash = '' + stripTrackingParams(url) + url.hostname = url.hostname.toLowerCase() + url.pathname = withoutTrailingSlash(url.pathname) + return url.toString() +} + +/** + * Natural key of one web speech. `externalId` wins when the discovery already + * knows the platform id; otherwise the canonical URL is the identity — the + * same URL always produces the same key, so re-running never duplicates. + */ +export const webSpeechSourceKey = ({ + platform, + url, + externalId, +}: { + platform: WebSpeechPlatform + url: string + externalId?: string | null +}): string => { + const id = externalId?.trim() + return id ? `web:${platform}:${id}` : `web:${platform}:${canonicalWebSpeechUrl(platform, url)}` +} + +/** A web speech row is complete when it has both the transcript and the mirrored file. */ +export const isCompleteWebSpeechState = ( + state: { segmentCount: number; mirroredMedia: number | null } | null, +): boolean => state !== null && state.segmentCount > 0 && state.mirroredMedia !== null + +const PUBLISHED_AT_PATTERN = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2}))?$/ +const TIMEZONE_SUFFIX = /(?:Z|[+-]\d{2}:?\d{2})$/ + +const isValidCalendarDay = (year: string, month: string, day: string): boolean => { + const date = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day))) + return ( + date.getUTCFullYear() === Number(year) && + date.getUTCMonth() === Number(month) - 1 && + date.getUTCDate() === Number(day) + ) +} + +/** + * `speechAt` (Brasília wall-clock text, same contract as the Câmara rows) from + * the discovery's `publishedAt` — day-only becomes midnight and an explicit + * timezone is converted to America/Bahia. Unrecognized values return null so + * the finding fails validation instead of inventing a date. + */ +export const webSpeechAtFromPublishedAt = (publishedAt: string): string | null => { + const trimmed = publishedAt.trim() + + if (TIMEZONE_SUFFIX.test(trimmed)) { + const instant = new Date(trimmed) + if (Number.isNaN(instant.getTime())) return null + // sv-SE renders the ISO-like `YYYY-MM-DD HH:mm` for the requested zone. + return new Intl.DateTimeFormat('sv-SE', { + timeZone: 'America/Bahia', + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + hour12: false, + }) + .format(instant) + .replace(' ', 'T') + } + + const match = PUBLISHED_AT_PATTERN.exec(trimmed) + if (!match) return null + const [, year, month, day, hour = '00', minute = '00'] = match + if (!isValidCalendarDay(year, month, day)) return null + if (Number(hour) > 23 || Number(minute) > 59) return null + return `${year}-${month}-${day}T${hour}:${minute}` +} + +const MAX_TITLE_LENGTH = 300 +const MAX_CHANNEL_LENGTH = 200 + +const webSpeechFindingSchema = z + .object({ + platform: z.enum(WEB_SPEECH_PLATFORM_VALUES, { error: 'plataforma desconhecida' }), + url: z.string({ error: 'url obrigatória' }).trim().url('url inválida'), + mediaUrl: z.string().trim().url('mediaUrl inválida').optional(), + externalId: z.string().trim().min(1, 'externalId vazio').optional(), + title: z + .string() + .trim() + .min(1, 'título vazio') + .max(MAX_TITLE_LENGTH, `título até ${MAX_TITLE_LENGTH} caracteres`) + .optional(), + channel: z + .string() + .trim() + .min(1, 'canal vazio') + .max(MAX_CHANNEL_LENGTH, `canal até ${MAX_CHANNEL_LENGTH} caracteres`) + .optional(), + publishedAt: z + .string({ error: 'publishedAt obrigatório' }) + .trim() + .min(4, 'publishedAt inválido'), + durationSeconds: z + .number({ error: 'durationSeconds inválido' }) + .finite() + .nonnegative('durationSeconds inválido') + .optional(), + thumbnailUrl: z.string().trim().url('thumbnailUrl inválida').optional(), + }) + .superRefine((finding, context) => { + if (requiresDirectMediaUrl(finding.platform) && !finding.mediaUrl) { + context.addIssue({ + code: 'custom', + path: ['mediaUrl'], + message: `Achado de ${webSpeechPlatformLabel(finding.platform)} exige mediaUrl (arquivo direto).`, + }) + } + if (webSpeechAtFromPublishedAt(finding.publishedAt) === null) { + context.addIssue({ + code: 'custom', + path: ['publishedAt'], + message: 'publishedAt deve ser YYYY-MM-DD ou data/hora ISO.', + }) + } + }) + +export type WebSpeechFinding = z.infer + +export type WebSpeechFindingParseResult = + | { ok: true; finding: WebSpeechFinding } + | { ok: false; error: string } + +export const parseWebSpeechFinding = (value: unknown): WebSpeechFindingParseResult => { + const parsed = webSpeechFindingSchema.safeParse(value) + if (parsed.success) return { ok: true, finding: parsed.data } + const issue = parsed.error.issues[0] + const path = issue?.path.join('.') ?? '' + return { + ok: false, + error: path ? `${path}: ${issue?.message}` : (issue?.message ?? 'achado inválido'), + } +} + +type WebSpeechBatch = { + generatedAt: string | null + findings: unknown[] +} + +export type WebSpeechBatchParseResult = + | { ok: true; batch: WebSpeechBatch } + | { ok: false; error: string } + +/** + * Envelope only: the findings stay raw so one invalid entry becomes a failure + * of that entry in the report instead of killing the whole batch. + */ +export const parseWebSpeechBatch = (value: unknown): WebSpeechBatchParseResult => { + const envelope = z + .object({ + generatedAt: z.string().trim().min(1).optional(), + findings: z.array(z.unknown()), + }) + .safeParse(value) + if (!envelope.success) { + return { ok: false, error: 'lote inválido: esperado { generatedAt?, findings: [...] }.' } + } + return { + ok: true, + batch: { + generatedAt: envelope.data.generatedAt ?? null, + findings: envelope.data.findings, + }, + } +} diff --git a/src/migrations/20260924_042028_add_web_speech_origin.json b/src/migrations/20260924_042028_add_web_speech_origin.json new file mode 100644 index 00000000..dc8391d8 --- /dev/null +++ b/src/migrations/20260924_042028_add_web_speech_origin.json @@ -0,0 +1,18613 @@ +{ + "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 + }, + "origin": { + "name": "origin", + "type": "enum_speech_origin", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'camara'" + }, + "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 + }, + "platform": { + "name": "platform", + "type": "enum_speech_platform", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "source_url": { + "name": "source_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "channel": { + "name": "channel", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "mirrored_media_id": { + "name": "mirrored_media_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "thumbnail_id": { + "name": "thumbnail_id", + "type": "integer", + "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_origin_idx": { + "name": "speech_origin_idx", + "columns": [ + { + "expression": "origin", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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_platform_idx": { + "name": "speech_platform_idx", + "columns": [ + { + "expression": "platform", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "speech_mirrored_media_idx": { + "name": "speech_mirrored_media_idx", + "columns": [ + { + "expression": "mirrored_media_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "speech_thumbnail_idx": { + "name": "speech_thumbnail_idx", + "columns": [ + { + "expression": "thumbnail_id", + "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": { + "speech_mirrored_media_id_internet_speech_media_id_fk": { + "name": "speech_mirrored_media_id_internet_speech_media_id_fk", + "tableFrom": "speech", + "tableTo": "internet_speech_media", + "columnsFrom": [ + "mirrored_media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "speech_thumbnail_id_internet_speech_media_id_fk": { + "name": "speech_thumbnail_id_internet_speech_media_id_fk", + "tableFrom": "speech", + "tableTo": "internet_speech_media", + "columnsFrom": [ + "thumbnail_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "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.internet_speech_media": { + "name": "internet_speech_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": { + "internet_speech_media_updated_at_idx": { + "name": "internet_speech_media_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "internet_speech_media_created_at_idx": { + "name": "internet_speech_media_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "internet_speech_media_filename_idx": { + "name": "internet_speech_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.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.content_piece_topics": { + "name": "content_piece_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_content_piece_topics", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + } + }, + "indexes": { + "content_piece_topics_order_idx": { + "name": "content_piece_topics_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "content_piece_topics_parent_idx": { + "name": "content_piece_topics_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "content_piece_topics_value_idx": { + "name": "content_piece_topics_value_idx", + "columns": [ + { + "expression": "value", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "content_piece_topics_parent_fk": { + "name": "content_piece_topics_parent_fk", + "tableFrom": "content_piece_topics", + "tableTo": "content_piece", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.content_piece_curated_fields": { + "name": "content_piece_curated_fields", + "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_content_piece_curated_fields", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + } + }, + "indexes": { + "content_piece_curated_fields_order_idx": { + "name": "content_piece_curated_fields_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "content_piece_curated_fields_parent_idx": { + "name": "content_piece_curated_fields_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "content_piece_curated_fields_parent_fk": { + "name": "content_piece_curated_fields_parent_fk", + "tableFrom": "content_piece_curated_fields", + "tableTo": "content_piece", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.content_piece": { + "name": "content_piece", + "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": false + }, + "type": { + "name": "type", + "type": "enum_content_piece_type", + "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": false + }, + "city_label": { + "name": "city_label", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "region": { + "name": "region", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "institution": { + "name": "institution", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "piece_date": { + "name": "piece_date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "duration_seconds": { + "name": "duration_seconds", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "transcript": { + "name": "transcript", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "media_id": { + "name": "media_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "source_url": { + "name": "source_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "origin": { + "name": "origin", + "type": "enum_content_piece_origin", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'arquivo'" + }, + "status": { + "name": "status", + "type": "enum_content_piece_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'rascunho'" + }, + "published_at": { + "name": "published_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "processing_status": { + "name": "processing_status", + "type": "enum_content_piece_processing_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pronto'" + }, + "step": { + "name": "step", + "type": "enum_content_piece_step", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "search_text": { + "name": "search_text", + "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": { + "content_piece_slug_idx": { + "name": "content_piece_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "content_piece_type_idx": { + "name": "content_piece_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "content_piece_municipality_idx": { + "name": "content_piece_municipality_idx", + "columns": [ + { + "expression": "municipality_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "content_piece_piece_date_idx": { + "name": "content_piece_piece_date_idx", + "columns": [ + { + "expression": "piece_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "content_piece_media_idx": { + "name": "content_piece_media_idx", + "columns": [ + { + "expression": "media_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "content_piece_source_url_idx": { + "name": "content_piece_source_url_idx", + "columns": [ + { + "expression": "source_url", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "content_piece_status_idx": { + "name": "content_piece_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "content_piece_processing_status_idx": { + "name": "content_piece_processing_status_idx", + "columns": [ + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "content_piece_created_by_idx": { + "name": "content_piece_created_by_idx", + "columns": [ + { + "expression": "created_by_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "content_piece_updated_at_idx": { + "name": "content_piece_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "content_piece_created_at_idx": { + "name": "content_piece_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "content_piece_municipality_id_municipality_id_fk": { + "name": "content_piece_municipality_id_municipality_id_fk", + "tableFrom": "content_piece", + "tableTo": "municipality", + "columnsFrom": [ + "municipality_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "content_piece_media_id_content_media_id_fk": { + "name": "content_piece_media_id_content_media_id_fk", + "tableFrom": "content_piece", + "tableTo": "content_media", + "columnsFrom": [ + "media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "content_piece_created_by_id_campaign_user_id_fk": { + "name": "content_piece_created_by_id_campaign_user_id_fk", + "tableFrom": "content_piece", + "tableTo": "campaign_user", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.content_media": { + "name": "content_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": { + "content_media_updated_at_idx": { + "name": "content_media_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "content_media_created_at_idx": { + "name": "content_media_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "content_media_filename_idx": { + "name": "content_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.content_event": { + "name": "content_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "enum_content_event_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "subject_type": { + "name": "subject_type", + "type": "enum_content_event_subject_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "subject_id": { + "name": "subject_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "variant": { + "name": "variant", + "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": { + "content_event_updated_at_idx": { + "name": "content_event_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "content_event_created_at_idx": { + "name": "content_event_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "subjectType_subjectId_type_idx": { + "name": "subjectType_subjectId_type_idx", + "columns": [ + { + "expression": "subject_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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.share_link_destinations": { + "name": "share_link_destinations", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "live": { + "name": "live", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": { + "share_link_destinations_order_idx": { + "name": "share_link_destinations_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "share_link_destinations_parent_id_idx": { + "name": "share_link_destinations_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "share_link_destinations_parent_id_fk": { + "name": "share_link_destinations_parent_id_fk", + "tableFrom": "share_link_destinations", + "tableTo": "share_link", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.share_link": { + "name": "share_link", + "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 + }, + "mode": { + "name": "mode", + "type": "enum_share_link_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'direct'" + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "image_id": { + "name": "image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "starts_at": { + "name": "starts_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "ends_at": { + "name": "ends_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "published": { + "name": "published", + "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": { + "share_link_slug_idx": { + "name": "share_link_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "share_link_image_idx": { + "name": "share_link_image_idx", + "columns": [ + { + "expression": "image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "share_link_updated_at_idx": { + "name": "share_link_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "share_link_created_at_idx": { + "name": "share_link_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "share_link_image_id_media_id_fk": { + "name": "share_link_image_id_media_id_fk", + "tableFrom": "share_link", + "tableTo": "media", + "columnsFrom": [ + "image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.jingle": { + "name": "jingle", + "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": false + }, + "cover_image_id": { + "name": "cover_image_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "audio_id": { + "name": "audio_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "published": { + "name": "published", + "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": { + "jingle_slug_idx": { + "name": "jingle_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "jingle_cover_image_idx": { + "name": "jingle_cover_image_idx", + "columns": [ + { + "expression": "cover_image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "jingle_audio_idx": { + "name": "jingle_audio_idx", + "columns": [ + { + "expression": "audio_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "jingle_updated_at_idx": { + "name": "jingle_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "jingle_created_at_idx": { + "name": "jingle_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "jingle_cover_image_id_media_id_fk": { + "name": "jingle_cover_image_id_media_id_fk", + "tableFrom": "jingle", + "tableTo": "media", + "columnsFrom": [ + "cover_image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "jingle_audio_id_media_id_fk": { + "name": "jingle_audio_id_media_id_fk", + "tableFrom": "jingle", + "tableTo": "media", + "columnsFrom": [ + "audio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "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 + }, + "internet_speech_media_id": { + "name": "internet_speech_media_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 + }, + "content_piece_id": { + "name": "content_piece_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "content_media_id": { + "name": "content_media_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "content_event_id": { + "name": "content_event_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 + }, + "share_link_id": { + "name": "share_link_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "jingle_id": { + "name": "jingle_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_internet_speech_media_id_idx": { + "name": "payload_locked_documents_rels_internet_speech_media_id_idx", + "columns": [ + { + "expression": "internet_speech_media_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_content_piece_id_idx": { + "name": "payload_locked_documents_rels_content_piece_id_idx", + "columns": [ + { + "expression": "content_piece_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_content_media_id_idx": { + "name": "payload_locked_documents_rels_content_media_id_idx", + "columns": [ + { + "expression": "content_media_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_content_event_id_idx": { + "name": "payload_locked_documents_rels_content_event_id_idx", + "columns": [ + { + "expression": "content_event_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": {} + }, + "payload_locked_documents_rels_share_link_id_idx": { + "name": "payload_locked_documents_rels_share_link_id_idx", + "columns": [ + { + "expression": "share_link_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_jingle_id_idx": { + "name": "payload_locked_documents_rels_jingle_id_idx", + "columns": [ + { + "expression": "jingle_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_internet_speech_media_fk": { + "name": "payload_locked_documents_rels_internet_speech_media_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "internet_speech_media", + "columnsFrom": [ + "internet_speech_media_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_content_piece_fk": { + "name": "payload_locked_documents_rels_content_piece_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "content_piece", + "columnsFrom": [ + "content_piece_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_content_media_fk": { + "name": "payload_locked_documents_rels_content_media_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "content_media", + "columnsFrom": [ + "content_media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_content_event_fk": { + "name": "payload_locked_documents_rels_content_event_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "content_event", + "columnsFrom": [ + "content_event_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" + }, + "payload_locked_documents_rels_share_link_fk": { + "name": "payload_locked_documents_rels_share_link_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "share_link", + "columnsFrom": [ + "share_link_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_jingle_fk": { + "name": "payload_locked_documents_rels_jingle_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "jingle", + "columnsFrom": [ + "jingle_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_origin": { + "name": "enum_speech_origin", + "schema": "public", + "values": [ + "camara", + "web" + ] + }, + "public.enum_speech_legislature": { + "name": "enum_speech_legislature", + "schema": "public", + "values": [ + "54", + "55", + "56", + "57" + ] + }, + "public.enum_speech_platform": { + "name": "enum_speech_platform", + "schema": "public", + "values": [ + "youtube", + "instagram", + "radio", + "audio" + ] + }, + "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_content_piece_topics": { + "name": "enum_content_piece_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_content_piece_curated_fields": { + "name": "enum_content_piece_curated_fields", + "schema": "public", + "values": [ + "title", + "description", + "topics", + "municipality", + "institution", + "pieceDate", + "transcript", + "type" + ] + }, + "public.enum_content_piece_type": { + "name": "enum_content_piece_type", + "schema": "public", + "values": [ + "video", + "foto", + "texto", + "audio", + "card" + ] + }, + "public.enum_content_piece_origin": { + "name": "enum_content_piece_origin", + "schema": "public", + "values": [ + "arquivo", + "instagram", + "youtube" + ] + }, + "public.enum_content_piece_status": { + "name": "enum_content_piece_status", + "schema": "public", + "values": [ + "rascunho", + "publicado" + ] + }, + "public.enum_content_piece_processing_status": { + "name": "enum_content_piece_processing_status", + "schema": "public", + "values": [ + "processando", + "pronto", + "falhou" + ] + }, + "public.enum_content_piece_step": { + "name": "enum_content_piece_step", + "schema": "public", + "values": [ + "extraindo", + "transcrevendo", + "catalogando", + "salvando" + ] + }, + "public.enum_content_event_type": { + "name": "enum_content_event_type", + "schema": "public", + "values": [ + "abertura", + "download", + "compartilhar_whatsapp", + "compartilhar_link" + ] + }, + "public.enum_content_event_subject_type": { + "name": "enum_content_event_subject_type", + "schema": "public", + "values": [ + "peca", + "card" + ] + }, + "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_share_link_mode": { + "name": "enum_share_link_mode", + "schema": "public", + "values": [ + "direct", + "announcement" + ] + }, + "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": "f6975ae2-80fa-4920-96b3-ebab824f42d7", + "prevId": "00000000-0000-0000-0000-000000000000" +} \ No newline at end of file diff --git a/src/migrations/20260924_042028_add_web_speech_origin.ts b/src/migrations/20260924_042028_add_web_speech_origin.ts new file mode 100644 index 00000000..054b651a --- /dev/null +++ b/src/migrations/20260924_042028_add_web_speech_origin.ts @@ -0,0 +1,71 @@ +import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres' + +export async function up({ db, payload, req }: MigrateUpArgs): Promise { + await db.execute(sql` + CREATE TYPE "public"."enum_speech_origin" AS ENUM('camara', 'web'); + CREATE TYPE "public"."enum_speech_platform" AS ENUM('youtube', 'instagram', 'radio', 'audio'); + CREATE TABLE "internet_speech_media" ( + "id" serial PRIMARY KEY NOT NULL, + "alt" varchar NOT NULL, + "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "url" varchar, + "thumbnail_u_r_l" varchar, + "filename" varchar, + "mime_type" varchar, + "filesize" numeric, + "width" numeric, + "height" numeric, + "focal_x" numeric, + "focal_y" numeric + ); + + ALTER TABLE "speech" ADD COLUMN "origin" "enum_speech_origin" DEFAULT 'camara' NOT NULL; + ALTER TABLE "speech" ADD COLUMN "platform" "enum_speech_platform"; + ALTER TABLE "speech" ADD COLUMN "external_id" varchar; + ALTER TABLE "speech" ADD COLUMN "source_url" varchar; + ALTER TABLE "speech" ADD COLUMN "title" varchar; + ALTER TABLE "speech" ADD COLUMN "channel" varchar; + ALTER TABLE "speech" ADD COLUMN "mirrored_media_id" integer; + ALTER TABLE "speech" ADD COLUMN "thumbnail_id" integer; + ALTER TABLE "payload_locked_documents_rels" ADD COLUMN "internet_speech_media_id" integer; + CREATE INDEX "internet_speech_media_updated_at_idx" ON "internet_speech_media" USING btree ("updated_at"); + CREATE INDEX "internet_speech_media_created_at_idx" ON "internet_speech_media" USING btree ("created_at"); + CREATE UNIQUE INDEX "internet_speech_media_filename_idx" ON "internet_speech_media" USING btree ("filename"); + ALTER TABLE "speech" ADD CONSTRAINT "speech_mirrored_media_id_internet_speech_media_id_fk" FOREIGN KEY ("mirrored_media_id") REFERENCES "public"."internet_speech_media"("id") ON DELETE set null ON UPDATE no action; + ALTER TABLE "speech" ADD CONSTRAINT "speech_thumbnail_id_internet_speech_media_id_fk" FOREIGN KEY ("thumbnail_id") REFERENCES "public"."internet_speech_media"("id") ON DELETE set null ON UPDATE no action; + ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_internet_speech_media_fk" FOREIGN KEY ("internet_speech_media_id") REFERENCES "public"."internet_speech_media"("id") ON DELETE cascade ON UPDATE no action; + CREATE INDEX "speech_origin_idx" ON "speech" USING btree ("origin"); + CREATE INDEX "speech_platform_idx" ON "speech" USING btree ("platform"); + CREATE INDEX "speech_mirrored_media_idx" ON "speech" USING btree ("mirrored_media_id"); + CREATE INDEX "speech_thumbnail_idx" ON "speech" USING btree ("thumbnail_id"); + CREATE INDEX "payload_locked_documents_rels_internet_speech_media_id_idx" ON "payload_locked_documents_rels" USING btree ("internet_speech_media_id");`) +} + +export async function down({ db, payload, req }: MigrateDownArgs): Promise { + await db.execute(sql` + ALTER TABLE "internet_speech_media" DISABLE ROW LEVEL SECURITY; + DROP TABLE "internet_speech_media" CASCADE; + ALTER TABLE "speech" DROP CONSTRAINT "speech_mirrored_media_id_internet_speech_media_id_fk"; + + ALTER TABLE "speech" DROP CONSTRAINT "speech_thumbnail_id_internet_speech_media_id_fk"; + + ALTER TABLE "payload_locked_documents_rels" DROP CONSTRAINT "payload_locked_documents_rels_internet_speech_media_fk"; + + DROP INDEX "speech_origin_idx"; + DROP INDEX "speech_platform_idx"; + DROP INDEX "speech_mirrored_media_idx"; + DROP INDEX "speech_thumbnail_idx"; + DROP INDEX "payload_locked_documents_rels_internet_speech_media_id_idx"; + ALTER TABLE "speech" DROP COLUMN "origin"; + ALTER TABLE "speech" DROP COLUMN "platform"; + ALTER TABLE "speech" DROP COLUMN "external_id"; + ALTER TABLE "speech" DROP COLUMN "source_url"; + ALTER TABLE "speech" DROP COLUMN "title"; + ALTER TABLE "speech" DROP COLUMN "channel"; + ALTER TABLE "speech" DROP COLUMN "mirrored_media_id"; + ALTER TABLE "speech" DROP COLUMN "thumbnail_id"; + ALTER TABLE "payload_locked_documents_rels" DROP COLUMN "internet_speech_media_id"; + DROP TYPE "public"."enum_speech_origin"; + DROP TYPE "public"."enum_speech_platform";`) +} diff --git a/src/migrations/index.ts b/src/migrations/index.ts index 73966f2b..6dbdec7b 100644 --- a/src/migrations/index.ts +++ b/src/migrations/index.ts @@ -82,6 +82,7 @@ import * as migration_20260923_032714_add_content_piece from './20260923_032714_ import * as migration_20260923_054145_add_share_link_announcement from './20260923_054145_add_share_link_announcement'; import * as migration_20260923_172437_add_content_events from './20260923_172437_add_content_events'; import * as migration_20260923_190414_add_content_event_variant from './20260923_190414_add_content_event_variant'; +import * as migration_20260924_042028_add_web_speech_origin from './20260924_042028_add_web_speech_origin'; export const migrations = [ { @@ -502,6 +503,11 @@ export const migrations = [ { up: migration_20260923_190414_add_content_event_variant.up, down: migration_20260923_190414_add_content_event_variant.down, - name: '20260923_190414_add_content_event_variant' + name: '20260923_190414_add_content_event_variant', + }, + { + up: migration_20260924_042028_add_web_speech_origin.up, + down: migration_20260924_042028_add_web_speech_origin.down, + name: '20260924_042028_add_web_speech_origin' }, ]; diff --git a/src/payload-types.ts b/src/payload-types.ts index f98004fd..19d53dfa 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -89,6 +89,7 @@ export interface Config { speech: Speech; speechSegment: SpeechSegment; speechCut: SpeechCut; + internetSpeechMedia: InternetSpeechMedia; reel: Reel; reelMedia: ReelMedia; contentPiece: ContentPiece; @@ -143,6 +144,7 @@ export interface Config { speech: SpeechSelect | SpeechSelect; speechSegment: SpeechSegmentSelect | SpeechSegmentSelect; speechCut: SpeechCutSelect | SpeechCutSelect; + internetSpeechMedia: InternetSpeechMediaSelect | InternetSpeechMediaSelect; reel: ReelSelect | ReelSelect; reelMedia: ReelMediaSelect | ReelMediaSelect; contentPiece: ContentPieceSelect | ContentPieceSelect; @@ -938,7 +940,7 @@ export interface MunicipalityUpdate { createdAt: string; } /** - * Acervo de falas do deputado na Câmara. Dados e vídeos da Câmara dos Deputados (CC BY 4.0). + * Acervo de falas do deputado: discursos na Câmara (CC BY 4.0) e falas publicadas na internet (mídia espelhada privada). * * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "speech". @@ -946,9 +948,13 @@ export interface MunicipalityUpdate { export interface Speech { id: number; /** - * Identidade da fala na API da Câmara (data/hora + tipo + fase). + * Identidade da fala: API da Câmara ou web::. */ sourceKey: string; + /** + * Discurso da Câmara ou fala publicada na internet. + */ + origin: 'camara' | 'web'; /** * Horário local de Brasília, como publicado pela Câmara. */ @@ -988,6 +994,34 @@ export interface Speech { * Último link conhecido; regerável pelo VOD. */ vodDownloadUrl?: string | null; + /** + * Plataforma de origem da fala da internet. + */ + platform?: ('youtube' | 'instagram' | 'radio' | 'audio') | null; + /** + * Identificador da publicação na plataforma (quando houver). + */ + externalId?: string | null; + /** + * Link canônico da publicação original. + */ + sourceUrl?: string | null; + /** + * Título da publicação na plataforma. + */ + title?: string | null; + /** + * Quem publicou (canal, rádio, perfil) — texto de origem, não é contato. + */ + channel?: string | null; + /** + * Arquivo privado preservado para player, download e cortes. + */ + mirroredMedia?: (number | null) | InternetSpeechMedia; + /** + * Capa da origem quando capturada; sem ela a lista mostra placeholder. + */ + thumbnail?: (number | null) | InternetSpeechMedia; topics?: | ( | 'saude' @@ -1022,6 +1056,30 @@ export interface Speech { updatedAt: string; createdAt: string; } +/** + * Arquivos privados das falas encontradas na internet (mídia espelhada e capa). Só abrem com login da campanha. + * + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "internetSpeechMedia". + */ +export interface InternetSpeechMedia { + id: number; + /** + * Descrição do arquivo para acessibilidade. + */ + alt: string; + updatedAt: string; + createdAt: string; + url?: string | null; + thumbnailURL?: string | null; + filename?: string | null; + mimeType?: string | null; + filesize?: number | null; + width?: number | null; + height?: number | null; + focalX?: number | null; + focalY?: number | null; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "speechSegment". @@ -2052,6 +2110,10 @@ export interface PayloadLockedDocument { relationTo: 'speechCut'; value: number | SpeechCut; } | null) + | ({ + relationTo: 'internetSpeechMedia'; + value: number | InternetSpeechMedia; + } | null) | ({ relationTo: 'reel'; value: number | Reel; @@ -2608,6 +2670,7 @@ export interface ActivitySelect { */ export interface SpeechSelect { sourceKey?: T; + origin?: T; speechAt?: T; year?: T; legislature?: T; @@ -2629,6 +2692,13 @@ export interface SpeechSelect { excerptTMs?: T; vodPlaybackUrl?: T; vodDownloadUrl?: T; + platform?: T; + externalId?: T; + sourceUrl?: T; + title?: T; + channel?: T; + mirroredMedia?: T; + thumbnail?: T; topics?: T; scopes?: T; classifiedBy?: T; @@ -2673,6 +2743,24 @@ export interface SpeechCutSelect { updatedAt?: T; createdAt?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "internetSpeechMedia_select". + */ +export interface InternetSpeechMediaSelect { + alt?: T; + updatedAt?: T; + createdAt?: T; + url?: T; + thumbnailURL?: T; + filename?: T; + mimeType?: T; + filesize?: T; + width?: T; + height?: T; + focalX?: T; + focalY?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "reel_select". @@ -3603,6 +3691,7 @@ export interface TaskCreateCollectionExport { | 'speech' | 'speechSegment' | 'speechCut' + | 'internetSpeechMedia' | 'reel' | 'reelMedia' | 'contentPiece' diff --git a/src/payload.config.ts b/src/payload.config.ts index e227f381..042c2895 100644 --- a/src/payload.config.ts +++ b/src/payload.config.ts @@ -26,6 +26,7 @@ import { ElectionCandidate } from './collections/ElectionCandidate' import { ElectionCandidateVote } from './collections/ElectionCandidateVote' import { ElectionTally } from './collections/ElectionTally' import { GoogleCalendarSync } from './collections/GoogleCalendarSync' +import { InternetSpeechMedia } from './collections/InternetSpeechMedia' import { Jingle } from './collections/Jingle' import { Leadership } from './collections/Leadership' import { Media } from './collections/Media' @@ -127,6 +128,7 @@ export default buildConfig({ Speech, SpeechSegment, SpeechCut, + InternetSpeechMedia, Reel, ReelMedia, ContentPiece, @@ -189,6 +191,8 @@ export default buildConfig({ recordingMedia: true, // C211 — private content-piece files; same failure mode if omitted. contentMedia: true, + // C215 — private mirrored web-speech files; same failure mode if omitted. + internetSpeechMedia: true, }, bucket: mediaStorage.bucket, config: { diff --git a/src/utilities/ai/deepInfraTranscribe.ts b/src/utilities/ai/deepInfraTranscribe.ts index ab4bdd85..13e69a8f 100644 --- a/src/utilities/ai/deepInfraTranscribe.ts +++ b/src/utilities/ai/deepInfraTranscribe.ts @@ -25,6 +25,13 @@ export const DEEPINFRA_TRANSCRIBE_URL = 'https://api.deepinfra.com/v1/openai/aud const DEEPINFRA_WHISPER_MODEL = 'openai/whisper-large-v3' const DEEPINFRA_LANGUAGE = 'pt' +/** + * C215 — provider price (~US$ per audio minute, Whisper large-v3 at Deep + * Infra) used by the ingestion report to state the ASR cost honestly. The + * Câmara CLI keeps its own copy; this is the app-side owner. + */ +export const DEEPINFRA_TRANSCRIBE_COST_PER_MINUTE_USD = 0.00045 + export type TranscribeAudioResult = | { ok: true; text: string } | { ok: false; error: string; status: number } diff --git a/src/utilities/ai/tools/findSpeechExcerpts.ts b/src/utilities/ai/tools/findSpeechExcerpts.ts index 2f5e5d09..20c8e59e 100644 --- a/src/utilities/ai/tools/findSpeechExcerpts.ts +++ b/src/utilities/ai/tools/findSpeechExcerpts.ts @@ -95,7 +95,14 @@ export const findSpeechExcerpts = (ctx: AIToolContext) => const speeches = await ctx.payload.find({ collection: 'speech', - where: { and: terms.map((term) => ({ searchText: { like: term } })) }, + // C215 — the assistant keeps suggesting Câmara excerpts; the web + // speeches (C216) are a different source with their own screen. + where: { + and: [ + { origin: { equals: 'camara' } }, + ...terms.map((term) => ({ searchText: { like: term } })), + ], + }, depth: 0, limit: SPEECH_CANDIDATE_LIMIT, page: 1, diff --git a/src/utilities/media/downloadToFile.ts b/src/utilities/media/downloadToFile.ts new file mode 100644 index 00000000..0d3f6fbd --- /dev/null +++ b/src/utilities/media/downloadToFile.ts @@ -0,0 +1,63 @@ +import 'server-only' + +import { createWriteStream } from 'node:fs' +import { Readable, Transform } from 'node:stream' +import { pipeline } from 'node:stream/promises' +import type { ReadableStream as NodeReadableStream } from 'node:stream/web' + +/** + * C215 — streams one public URL to a local file: the direct acquisition path + * of radio/audio findings (yt-dlp owns the platform extractors). A non-2xx + * answer fails before anything is written, the body is never buffered in + * memory and both a timeout and a byte ceiling bound a stalled or oversized + * server (a partial file is removed by the caller's temp dir). + */ + +/** Ceiling: hours of audio/video are the real case, 4 GiB is the guard. */ +const DOWNLOAD_MAX_BYTES = 4 * 1024 * 1024 * 1024 +const DOWNLOAD_TIMEOUT_MS = 60 * 60_000 +const TOO_LARGE_MESSAGE = 'Download excede o tamanho máximo permitido.' + +export const downloadUrlToFile = async ({ + url, + destinationPath, + timeoutMs = DOWNLOAD_TIMEOUT_MS, + maxBytes = DOWNLOAD_MAX_BYTES, + fetchImpl = fetch, +}: { + url: string + destinationPath: string + timeoutMs?: number + maxBytes?: number + fetchImpl?: typeof fetch +}): Promise => { + const response = await fetchImpl(url, { + redirect: 'follow', + signal: AbortSignal.timeout(timeoutMs), + }) + if (!response.ok) throw new Error(`Download falhou (HTTP ${response.status}).`) + if (!response.body) throw new Error('Download sem corpo.') + + const declaredLength = Number(response.headers.get('content-length')) + if (Number.isFinite(declaredLength) && declaredLength > maxBytes) { + throw new Error(TOO_LARGE_MESSAGE) + } + + let received = 0 + const ceiling = new Transform({ + transform(chunk, _encoding, callback) { + received += chunk.length + if (received > maxBytes) { + callback(new Error(TOO_LARGE_MESSAGE)) + return + } + callback(null, chunk) + }, + }) + + await pipeline( + Readable.fromWeb(response.body as unknown as NodeReadableStream), + ceiling, + createWriteStream(destinationPath), + ) +} diff --git a/src/utilities/media/ffmpeg.ts b/src/utilities/media/ffmpeg.ts index 39727df1..80e43aa4 100644 --- a/src/utilities/media/ffmpeg.ts +++ b/src/utilities/media/ffmpeg.ts @@ -16,7 +16,12 @@ const FFMPEG_MIN_TIMEOUT_MS = 30_000 const FFMPEG_MAX_TIMEOUT_MS = 300_000 const FFMPEG_MAX_BUFFER_BYTES = 8 * 1024 * 1024 -const ffmpegBinary = (): string => process.env.FFMPEG_PATH?.trim() || 'ffmpeg' +/** + * The resolved binary (`FFMPEG_PATH` override or `ffmpeg` on PATH). Exported + * so callers that must tell another tool where ffmpeg lives (yt-dlp's + * `--ffmpeg-location`, C215) read the same resolution instead of the env. + */ +export const ffmpegBinary = (): string => process.env.FFMPEG_PATH?.trim() || 'ffmpeg' /** Last-resort copy when an error carries no message; each job names its own. */ export const messageOf = (error: unknown, fallback: string): string => diff --git a/src/utilities/media/ytdlp.ts b/src/utilities/media/ytdlp.ts new file mode 100644 index 00000000..1d8ea951 --- /dev/null +++ b/src/utilities/media/ytdlp.ts @@ -0,0 +1,170 @@ +import 'server-only' + +import { execFile } from 'node:child_process' + +/** + * C215 — the single owner of yt-dlp execution for the web-speech ingestion: + * binary resolution (`YTDLP_PATH` → `yt-dlp` on PATH, fail-closed), the + * metadata probe (`--dump-json`) and the download of one URL to a local file. + * YouTube and Instagram only; direct radio/audio files are fetched by + * `downloadToFile`. + * + * There is no packaged yt-dlp: the operator installs it and the tests inject a + * fake through `YTDLP_PATH`. The format cap keeps the mirrored artifact enough + * for transcription, player and cut — never the master. + */ + +export const YTDLP_INSTALL_HINT = + 'Instale o yt-dlp (ex.: `pipx install yt-dlp`) ou defina YTDLP_PATH.' + +/** H.264/AAC up to 720p when available, then the best single file. */ +export const YTDLP_FORMAT = 'bv*[height<=720][vcodec^=avc1]+ba[acodec^=mp4a]/b[height<=720]/b' + +const VERSION_TIMEOUT_MS = 30_000 +const METADATA_TIMEOUT_MS = 120_000 +const DOWNLOAD_TIMEOUT_MS = 60 * 60_000 +const MAX_BUFFER_BYTES = 16 * 1024 * 1024 + +export type YtDlpResult = + | { ok: true; stdout: string; stderr: string } + | { + ok: false + stdout: string + stderr: string + error?: { code?: string | number | null; message?: string } + } + +export type YtDlpRunner = ( + bin: string, + args: string[], + options?: { timeoutMs?: number }, +) => Promise + +const defaultRunner: YtDlpRunner = (bin, args, { timeoutMs } = {}) => + new Promise((resolve) => { + execFile( + bin, + args, + { timeout: timeoutMs, maxBuffer: MAX_BUFFER_BYTES }, + (error, stdout, stderr) => { + if (!error) { + resolve({ ok: true, stdout: stdout ?? '', stderr: stderr ?? '' }) + return + } + resolve({ ok: false, stdout: stdout ?? '', stderr: stderr ?? '', error }) + }, + ) + }) + +const ytDlpBinary = (env: Record = process.env): string => + env.YTDLP_PATH?.trim() || 'yt-dlp' + +export type YtDlpResolution = { ok: true; bin: string } | { ok: false; reason: string } + +/** Probe of the resolved binary; a missing/unusable yt-dlp never degrades silently. */ +export const resolveYtDlp = async ({ + env = process.env, + run = defaultRunner, +}: { + env?: Record + run?: YtDlpRunner +} = {}): Promise => { + const bin = ytDlpBinary(env) + const probe = await run(bin, ['--version'], { timeoutMs: VERSION_TIMEOUT_MS }) + if (!probe.ok) { + const code = probe.error?.code + return { + ok: false, + reason: code === 'ENOENT' ? `yt-dlp não encontrado (${bin})` : `yt-dlp não executou (${bin})`, + } + } + return { ok: true, bin } +} + +export type YtDlpMetadata = { + externalId: string | null + title: string | null + channel: string | null + durationSeconds: number | null + thumbnailUrl: string | null +} + +const stringOrNull = (value: unknown): string | null => + typeof value === 'string' && value.trim() !== '' ? value.trim() : null + +const positiveNumberOrNull = (value: unknown): number | null => { + const number = Number(value) + return Number.isFinite(number) && number > 0 ? number : null +} + +/** One `--dump-json` document; absent fields stay null instead of guessing. */ +export const parseYtDlpMetadata = (value: unknown): YtDlpMetadata => { + const data = value && typeof value === 'object' ? (value as Record) : {} + return { + externalId: stringOrNull(data.id), + title: stringOrNull(data.title), + channel: stringOrNull(data.channel) ?? stringOrNull(data.uploader), + durationSeconds: positiveNumberOrNull(data.duration), + thumbnailUrl: stringOrNull(data.thumbnail), + } +} + +const stderrTail = (result: Extract): string => { + const tail = (result.stderr || result.error?.message || '').trim().slice(-400) + return tail ? `: ${tail}` : '' +} + +export const readYtDlpMetadata = async ({ + bin, + url, + run = defaultRunner, +}: { + bin: string + url: string + run?: YtDlpRunner +}): Promise => { + const result = await run( + bin, + ['--dump-json', '--no-playlist', '--no-warnings', '--skip-download', url], + { timeoutMs: METADATA_TIMEOUT_MS }, + ) + if (!result.ok) throw new Error(`yt-dlp não leu os metadados${stderrTail(result)}`) + try { + return parseYtDlpMetadata(JSON.parse(result.stdout)) + } catch { + throw new Error('yt-dlp devolveu metadados fora do formato.') + } +} + +export const downloadWithYtDlp = async ({ + bin, + url, + outputTemplate, + ffmpegLocation = null, + run = defaultRunner, +}: { + bin: string + url: string + outputTemplate: string + ffmpegLocation?: string | null + run?: YtDlpRunner +}): Promise => { + const result = await run( + bin, + [ + '--no-playlist', + '--no-warnings', + '--no-progress', + '-f', + YTDLP_FORMAT, + '--merge-output-format', + 'mp4', + ...(ffmpegLocation ? ['--ffmpeg-location', ffmpegLocation] : []), + '-o', + outputTemplate, + url, + ], + { timeoutMs: DOWNLOAD_TIMEOUT_MS }, + ) + if (!result.ok) throw new Error(`yt-dlp falhou no download${stderrTail(result)}`) +} diff --git a/src/utilities/speech/speechCoverage.ts b/src/utilities/speech/speechCoverage.ts index 4c9109c7..d7b673e9 100644 --- a/src/utilities/speech/speechCoverage.ts +++ b/src/utilities/speech/speechCoverage.ts @@ -45,6 +45,7 @@ const COVERAGE_QUERY = sql` )) AS "with_segments", COUNT(*) FILTER (WHERE s."audio_id" IS NULL AND s."youtube_url" IS NOT NULL) AS "fallback_youtube" FROM "speech" s + WHERE s."origin" = 'camara' GROUP BY s."legislature" ORDER BY s."legislature" NULLS LAST ` diff --git a/src/utilities/speech/speechImport.ts b/src/utilities/speech/speechImport.ts index c5a00aa2..9faf3da3 100644 --- a/src/utilities/speech/speechImport.ts +++ b/src/utilities/speech/speechImport.ts @@ -5,13 +5,22 @@ import type { Payload, RequiredDataFromCollectionSlug } from 'payload' import type { MergedSpeechFacets } from '@/lib/speechFacets' import { normalizeForSearch } from '@/lib/speechSearch' import type { Speech } from '@/payload-types' -import { withPayloadTransaction } from '@/utilities/payloadTransaction' +import { + withPayloadTransaction, + type PayloadTransactionRequest, +} from '@/utilities/payloadTransaction' /** * Idempotent persistence of one imported speech (C153). Identity is * `sourceKey`; metadata is refreshed on every run, segments are replaced only * when the caller supplies them, and a `manual` facet curation is never * overwritten. Everything runs in one transaction on the Payload session. + * + * C215 — the same owner persists the web speeches: the optional origin fields + * (`origin: 'web'`, platform, canonical URL, title, channel and the mirrored + * media) travel in the bundle. The web pipeline already holds a transaction + * (media upload + upsert) and passes it via `options.req`; the Câmara path + * passes nothing and keeps opening its own. */ type SpeechSegmentInput = { @@ -42,6 +51,15 @@ export type SpeechImportBundle = { excerptTMs: number | null vodPlaybackUrl: string | null vodDownloadUrl: string | null + /** C215 — web origin metadata (undefined on the Câmara path). */ + origin?: Speech['origin'] + platform?: Speech['platform'] + externalId?: string | null + sourceUrl?: string | null + title?: string | null + channel?: string | null + mirroredMedia?: number | null + thumbnail?: number | null /** Undefined preserves the stored facets (e.g. a run that skipped classification). */ facets?: MergedSpeechFacets /** Undefined preserves the stored segments (e.g. `--skip-transcribe`). */ @@ -60,6 +78,12 @@ export type SpeechImportState = { summary: string | null officialTranscript: string | null officialTextUrl: string | null + origin: Speech['origin'] + platform: Speech['platform'] | null + externalId: string | null + sourceUrl: string | null + mirroredMedia: number | null + thumbnail: number | null } export type SpeechUpsertCounts = { @@ -69,6 +93,10 @@ export type SpeechUpsertCounts = { manualFacetsPreserved: boolean } +/** Depth-0 upload relations arrive as the id; normalize for the state contract. */ +const mediaIdOf = (media: Speech['mirroredMedia']): number | null => + typeof media === 'object' && media !== null ? media.id : (media ?? null) + /** Current stored state of one speech, for the importer's skip decisions. */ export const findSpeechImportState = async ( payload: Payload, @@ -89,6 +117,12 @@ export const findSpeechImportState = async ( summary: true, officialTranscript: true, officialTextUrl: true, + origin: true, + platform: true, + externalId: true, + sourceUrl: true, + mirroredMedia: true, + thumbnail: true, }, // Intentional bypass: the import CLI is a trusted actor with no session. overrideAccess: true, @@ -115,152 +149,189 @@ export const findSpeechImportState = async ( summary: speech.summary ?? null, officialTranscript: speech.officialTranscript ?? null, officialTextUrl: speech.officialTextUrl ?? null, + origin: speech.origin, + platform: speech.platform ?? null, + externalId: speech.externalId ?? null, + sourceUrl: speech.sourceUrl ?? null, + mirroredMedia: mediaIdOf(speech.mirroredMedia), + thumbnail: mediaIdOf(speech.thumbnail), } } type SpeechWriteData = RequiredDataFromCollectionSlug<'speech'> -const metadataData = ( - bundle: SpeechImportBundle, -): Omit => ({ - speechAt: bundle.speechAt, - year: bundle.year, - legislature: bundle.legislature, - type: bundle.type, - phase: bundle.phase, - durationSeconds: bundle.durationSeconds, - summary: bundle.summary, - officialTranscript: bundle.officialTranscript, - officialTextUrl: bundle.officialTextUrl, - keywords: bundle.keywords, - eventId: bundle.eventId, - eventType: bundle.eventType, - eventStartAt: bundle.eventStartAt, - eventEndAt: bundle.eventEndAt, - youtubeUrl: bundle.youtubeUrl, - presidingOfficer: bundle.presidingOfficer, - audioId: bundle.audioId, - excerptTMs: bundle.excerptTMs, - vodPlaybackUrl: bundle.vodPlaybackUrl, - vodDownloadUrl: bundle.vodDownloadUrl, -}) +type SpeechMetadataData = Omit -export const upsertSpeechBundle = async ( +const metadataData = (bundle: SpeechImportBundle): SpeechMetadataData => { + const data: SpeechMetadataData = { + origin: bundle.origin ?? 'camara', + speechAt: bundle.speechAt, + year: bundle.year, + legislature: bundle.legislature, + type: bundle.type, + phase: bundle.phase, + durationSeconds: bundle.durationSeconds, + summary: bundle.summary, + officialTranscript: bundle.officialTranscript, + officialTextUrl: bundle.officialTextUrl, + keywords: bundle.keywords, + eventId: bundle.eventId, + eventType: bundle.eventType, + eventStartAt: bundle.eventStartAt, + eventEndAt: bundle.eventEndAt, + youtubeUrl: bundle.youtubeUrl, + presidingOfficer: bundle.presidingOfficer, + audioId: bundle.audioId, + excerptTMs: bundle.excerptTMs, + vodPlaybackUrl: bundle.vodPlaybackUrl, + vodDownloadUrl: bundle.vodDownloadUrl, + } + + // Web-origin fields travel only when the caller supplied them: the Câmara + // bundle omits them and Payload leaves the stored values untouched. + if (bundle.platform !== undefined) data.platform = bundle.platform + if (bundle.externalId !== undefined) data.externalId = bundle.externalId + if (bundle.sourceUrl !== undefined) data.sourceUrl = bundle.sourceUrl + if (bundle.title !== undefined) data.title = bundle.title + if (bundle.channel !== undefined) data.channel = bundle.channel + if (bundle.mirroredMedia !== undefined) data.mirroredMedia = bundle.mirroredMedia + if (bundle.thumbnail !== undefined) data.thumbnail = bundle.thumbnail + + return data +} + +export type SpeechUpsertOptions = { + /** Existing transaction to ride (C215 media upload + upsert); absent opens one. */ + req?: PayloadTransactionRequest +} + +const upsertSpeechBundleInTransaction = async ( payload: Payload, bundle: SpeechImportBundle, -): Promise => - withPayloadTransaction(payload, async ({ req }) => { - const found = await payload.find({ + req: PayloadTransactionRequest, +): Promise => { + const found = await payload.find({ + collection: 'speech', + where: { sourceKey: { equals: bundle.sourceKey } }, + limit: 1, + depth: 0, + select: { id: true, classifiedBy: true }, + req, + // Intentional bypass: the import CLI is a trusted actor with no session. + overrideAccess: true, + }) + const current = found.docs[0] + const manualFacetsPreserved = current?.classifiedBy === 'manual' + + // C154 — the speech-level search text mirrors the segments and is owned by + // this bundle: `segments: undefined` (skip/ASR failure) preserves whatever + // is stored, exactly like the segments themselves. + const segmentSearchText = + bundle.segments === undefined + ? undefined + : normalizeForSearch(bundle.segments.map((segment) => segment.text).join(' ')) + + const metadata = metadataData(bundle) + const facets: Partial = {} + if (bundle.facets && !manualFacetsPreserved) { + const municipalitySlugs = bundle.facets.municipalities.map((entry) => entry.slug) + const municipalities = + municipalitySlugs.length === 0 + ? [] + : ( + await payload.find({ + collection: 'municipality', + where: { slug: { in: municipalitySlugs } }, + limit: municipalitySlugs.length, + depth: 0, + select: { slug: true }, + req, + // Intentional bypass: resolving gazetteer slugs inside the import transaction. + overrideAccess: true, + }) + ).docs + facets.topics = bundle.facets.topics + facets.scopes = bundle.facets.scopes + facets.classifiedBy = bundle.facets.classifiedBy + facets.mentionedMunicipalities = municipalities.map((doc) => doc.id) + facets.mentionedPeople = bundle.facets.people + facets.mentionedPrograms = bundle.facets.programs + facets.mentionedProjects = bundle.facets.projects + } + const data = { ...metadata, ...facets } + + let speechId: number + let created = false + if (current) { + await payload.update({ collection: 'speech', - where: { sourceKey: { equals: bundle.sourceKey } }, - limit: 1, + id: current.id, + data: segmentSearchText === undefined ? data : { ...data, searchText: segmentSearchText }, depth: 0, - select: { id: true, classifiedBy: true }, req, // Intentional bypass: the import CLI is a trusted actor with no session. overrideAccess: true, }) - const current = found.docs[0] - const manualFacetsPreserved = current?.classifiedBy === 'manual' + speechId = current.id + } else { + const speech = await payload.create({ + collection: 'speech', + data: { + sourceKey: bundle.sourceKey, + classifiedBy: bundle.facets?.classifiedBy ?? 'gazetteer', + searchText: segmentSearchText ?? '', + ...data, + }, + depth: 0, + req, + // Intentional bypass: the import CLI is a trusted actor with no session. + overrideAccess: true, + }) + speechId = speech.id + created = true + } - // C154 — the speech-level search text mirrors the segments and is owned by - // this bundle: `segments: undefined` (skip/ASR failure) preserves whatever - // is stored, exactly like the segments themselves. - const segmentSearchText = - bundle.segments === undefined - ? undefined - : normalizeForSearch(bundle.segments.map((segment) => segment.text).join(' ')) + let segmentsInserted = 0 + let segmentsDeleted = 0 + if (bundle.segments !== undefined) { + const removed = await payload.delete({ + collection: 'speechSegment', + where: { speech: { equals: speechId } }, + req, + // Intentional bypass: replacing segments owned by the speech being imported. + overrideAccess: true, + }) + segmentsDeleted = removed.docs.length - const metadata = metadataData(bundle) - const facets: Partial = {} - if (bundle.facets && !manualFacetsPreserved) { - const municipalitySlugs = bundle.facets.municipalities.map((entry) => entry.slug) - const municipalities = - municipalitySlugs.length === 0 - ? [] - : ( - await payload.find({ - collection: 'municipality', - where: { slug: { in: municipalitySlugs } }, - limit: municipalitySlugs.length, - depth: 0, - select: { slug: true }, - req, - // Intentional bypass: resolving gazetteer slugs inside the import transaction. - overrideAccess: true, - }) - ).docs - facets.topics = bundle.facets.topics - facets.scopes = bundle.facets.scopes - facets.classifiedBy = bundle.facets.classifiedBy - facets.mentionedMunicipalities = municipalities.map((doc) => doc.id) - facets.mentionedPeople = bundle.facets.people - facets.mentionedPrograms = bundle.facets.programs - facets.mentionedProjects = bundle.facets.projects - } - const data = { ...metadata, ...facets } - - let speechId: number - let created = false - if (current) { - await payload.update({ - collection: 'speech', - id: current.id, - data: segmentSearchText === undefined ? data : { ...data, searchText: segmentSearchText }, - depth: 0, - req, - // Intentional bypass: the import CLI is a trusted actor with no session. - overrideAccess: true, - }) - speechId = current.id - } else { - const speech = await payload.create({ - collection: 'speech', - data: { - sourceKey: bundle.sourceKey, - classifiedBy: bundle.facets?.classifiedBy ?? 'gazetteer', - searchText: segmentSearchText ?? '', - ...data, - }, - depth: 0, - req, - // Intentional bypass: the import CLI is a trusted actor with no session. - overrideAccess: true, - }) - speechId = speech.id - created = true - } - - let segmentsInserted = 0 - let segmentsDeleted = 0 - if (bundle.segments !== undefined) { - const removed = await payload.delete({ + for (const [index, segment] of bundle.segments.entries()) { + await payload.create({ collection: 'speechSegment', - where: { speech: { equals: speechId } }, + data: { + speech: speechId, + order: index + 1, + startSeconds: segment.startSeconds, + endSeconds: segment.endSeconds, + text: segment.text, + searchText: normalizeForSearch(segment.text), + }, req, - // Intentional bypass: replacing segments owned by the speech being imported. + // Intentional bypass: the import CLI is a trusted actor with no session. overrideAccess: true, }) - segmentsDeleted = removed.docs.length - - for (const [index, segment] of bundle.segments.entries()) { - await payload.create({ - collection: 'speechSegment', - data: { - speech: speechId, - order: index + 1, - startSeconds: segment.startSeconds, - endSeconds: segment.endSeconds, - text: segment.text, - searchText: normalizeForSearch(segment.text), - }, - req, - // Intentional bypass: the import CLI is a trusted actor with no session. - overrideAccess: true, - }) - segmentsInserted += 1 - } + segmentsInserted += 1 } + } - return { created, segmentsInserted, segmentsDeleted, manualFacetsPreserved } - }) + return { created, segmentsInserted, segmentsDeleted, manualFacetsPreserved } +} + +export const upsertSpeechBundle = async ( + payload: Payload, + bundle: SpeechImportBundle, + options: SpeechUpsertOptions = {}, +): Promise => { + if (options.req) return upsertSpeechBundleInTransaction(payload, bundle, options.req) + return withPayloadTransaction(payload, ({ req }) => + upsertSpeechBundleInTransaction(payload, bundle, req), + ) +} diff --git a/src/utilities/speech/speechListFilters.ts b/src/utilities/speech/speechListFilters.ts index da455242..6a5c0ff2 100644 --- a/src/utilities/speech/speechListFilters.ts +++ b/src/utilities/speech/speechListFilters.ts @@ -32,7 +32,9 @@ const durationBucketWhere = (bucket: SpeechDurationBucket): Where => { } const buildSpeechFacetWhere = (state: SpeechListState): Where[] => { - const filters: Where[] = [] + // C215 — this contract is the Câmara list: the web speeches share the table + // but are served by their own source (C216), never mixed here. + const filters: Where[] = [{ origin: { equals: 'camara' } }] if (state.years?.length) filters.push({ year: { in: state.years } }) if (state.topics?.length) filters.push({ topics: { in: state.topics } }) diff --git a/src/utilities/speech/speechPageData.ts b/src/utilities/speech/speechPageData.ts index 972b9d2e..e7870375 100644 --- a/src/utilities/speech/speechPageData.ts +++ b/src/utilities/speech/speechPageData.ts @@ -91,6 +91,9 @@ const loadSpeechFilterOptions = async ( depth: 0, limit: 0, pagination: false, + // C215 — options are the Câmara catalog's; the web source (C216) builds + // its own options from its rows. + where: { origin: { equals: 'camara' } }, select: { year: true, phase: true, mentionedMunicipalities: true }, user, overrideAccess: false, diff --git a/src/utilities/speech/webSpeechIngest.ts b/src/utilities/speech/webSpeechIngest.ts new file mode 100644 index 00000000..362c4b6e --- /dev/null +++ b/src/utilities/speech/webSpeechIngest.ts @@ -0,0 +1,514 @@ +import 'server-only' + +import { mkdtemp, readdir, readFile, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import type { Payload } from 'payload' + +import { + buildRecordingAudioFfmpegArgs, + mergeChunkTranscriptions, + RECORDING_AUDIO_CHUNK_SECONDS, + type RecordingChunkTranscription, + type RecordingTranscriptSegment, +} from '@/lib/recordingTranscription' +import type { SpeechFacetInput } from '@/lib/speechGazetteer' +import { + canonicalWebSpeechUrl, + INTERNET_SPEECH_MEDIA_SLUG, + isCompleteWebSpeechState, + requiresDirectMediaUrl, + webSpeechAtFromPublishedAt, + webSpeechPlatformLabel, + webSpeechSourceKey, + type WebSpeechFinding, +} from '@/lib/webSpeech' +import { + DEEPINFRA_TRANSCRIBE_COST_PER_MINUTE_USD, + deepInfraTranscribeSegments, + type TranscribeSegmentsResult, +} from '@/utilities/ai/deepInfraTranscribe' +import { downloadUrlToFile } from '@/utilities/media/downloadToFile' +import { ffmpegBinary, messageOf, runFfmpeg } from '@/utilities/media/ffmpeg' +import { + downloadWithYtDlp, + readYtDlpMetadata, + resolveYtDlp, + YTDLP_INSTALL_HINT, + type YtDlpMetadata, +} from '@/utilities/media/ytdlp' +import { withPayloadTransaction } from '@/utilities/payloadTransaction' +import { + classifySpeech, + type SpeechClassificationResult, +} from '@/utilities/speech/speechClassifier' +import { + findSpeechImportState, + upsertSpeechBundle, + type SpeechImportBundle, +} from '@/utilities/speech/speechImport' + +/** + * C215 — one web speech end to end: acquire the media (yt-dlp or direct HTTP), + * transcribe with timestamps (the C199 chunk owner), classify (the C153 owner), + * mirror the artifact in the private collection and upsert idempotently (the + * Câmara import owner). Every dependency is injectable so the int specs run + * offline; the CLI is the only caller. + */ + +/** Audio extraction has no duration to scale the timeout from; 30 min is ample. */ +const FFMPEG_TIMEOUT_MS = 30 * 60_000 +const ACQUISITION_FAILURE = 'Não foi possível baixar a mídia da fala.' +const TRANSCRIPTION_FAILURE = 'Não foi possível transcrever a fala.' +const MEDIA_FAILURE = 'Não foi possível guardar a mídia da fala.' +const CLASSIFICATION_FAILURE = 'Não foi possível classificar a fala.' + +type WebSpeechIngestStatus = 'created' | 'updated' | 'skipped' | 'failed' +type WebSpeechIngestStage = + | 'validation' + | 'acquisition' + | 'transcription' + | 'classification' + | 'media' + | 'upsert' + +export type WebSpeechIngestResult = { + sourceKey: string + status: WebSpeechIngestStatus + stage: WebSpeechIngestStage | null + error: string | null + asrSeconds: number + asrCostUsd: number + llmCostUsd: number | null +} + +type WebSpeechAcquisition = { + /** Produced file inside the temp dir, extension included. */ + fileName: string + metadata: YtDlpMetadata | null +} + +export type WebSpeechAcquirer = ( + finding: WebSpeechFinding, + tempDir: string, +) => Promise + +export type WebSpeechTranscriber = ( + file: Blob, + options?: { filename?: string }, +) => Promise + +export type WebSpeechClassifier = (input: SpeechFacetInput) => Promise + +export type WebSpeechIngestDeps = { + acquire?: WebSpeechAcquirer + transcribe?: WebSpeechTranscriber + classify?: WebSpeechClassifier + runFfmpeg?: typeof runFfmpeg + downloadThumbnail?: (url: string, destinationPath: string) => Promise +} + +class WebSpeechIngestError extends Error { + readonly stage: WebSpeechIngestStage + + constructor(stage: WebSpeechIngestStage, message: string) { + super(message) + this.stage = stage + } +} + +const extensionOf = (url: string, fallback: string): string => { + try { + const name = new URL(url).pathname.split('/').pop() ?? '' + const index = name.lastIndexOf('.') + const extension = index >= 0 ? name.slice(index).toLowerCase() : '' + return /^\.[a-z0-9]{1,5}$/.test(extension) ? extension : fallback + } catch { + return fallback + } +} + +/** yt-dlp for YouTube/Instagram; direct mediaUrl for radio/audio. */ +const defaultAcquire: WebSpeechAcquirer = async (finding, tempDir) => { + if (requiresDirectMediaUrl(finding.platform)) { + if (!finding.mediaUrl) throw new Error('Achado sem mediaUrl.') + const fileName = `source${extensionOf(finding.mediaUrl, '.mp3')}` + await downloadUrlToFile({ url: finding.mediaUrl, destinationPath: join(tempDir, fileName) }) + return { fileName, metadata: null } + } + + const resolved = await resolveYtDlp() + if (!resolved.ok) throw new Error(`${resolved.reason}. ${YTDLP_INSTALL_HINT}`) + + const metadata = await readYtDlpMetadata({ bin: resolved.bin, url: finding.url }) + await downloadWithYtDlp({ + bin: resolved.bin, + url: finding.url, + outputTemplate: join(tempDir, 'source.%(ext)s'), + ffmpegLocation: ffmpegBinary(), + }) + const fileName = (await readdir(tempDir)).find((name) => name.startsWith('source.')) + if (!fileName) throw new Error('yt-dlp não produziu arquivo.') + return { fileName, metadata } +} + +type BundleMetadata = { + title?: string | null + channel?: string | null + externalId?: string | null + durationSeconds?: number | null +} + +/** + * The web bundle: Câmara-only fields stay null (no legislature, no VOD, no + * official transcript) and the web metadata travels only when known — + * `undefined` preserves the stored value on a metadata refresh. + */ +const baseBundle = ( + finding: WebSpeechFinding, + sourceKey: string, + speechAt: string, + metadata: BundleMetadata, +): SpeechImportBundle => ({ + sourceKey, + origin: 'web', + platform: finding.platform, + externalId: metadata.externalId, + sourceUrl: canonicalWebSpeechUrl(finding.platform, finding.url), + title: metadata.title, + channel: metadata.channel, + speechAt, + year: Number(speechAt.slice(0, 4)), + legislature: null, + type: null, + phase: null, + durationSeconds: metadata.durationSeconds ?? null, + summary: null, + officialTranscript: null, + officialTextUrl: null, + keywords: [], + eventId: null, + eventType: null, + eventStartAt: null, + eventEndAt: null, + youtubeUrl: null, + presidingOfficer: null, + audioId: null, + excerptTMs: null, + vodPlaybackUrl: null, + vodDownloadUrl: null, +}) + +const emptyResult = ( + sourceKey: string, + status: WebSpeechIngestStatus, + stage: WebSpeechIngestStage | null, + error: string | null, +): WebSpeechIngestResult => ({ + sourceKey, + status, + stage, + error, + asrSeconds: 0, + asrCostUsd: 0, + llmCostUsd: null, +}) + +type ExtractedTranscript = { + segments: RecordingTranscriptSegment[] + durationSeconds: number | null + asrSeconds: number +} + +/** Extract → chunk → transcribe → merge (the C199 owners, relative offsets). */ +const extractTranscript = async ({ + ffmpeg, + transcribe, + inputPath, + tempDir, + declaredDuration, +}: { + ffmpeg: typeof runFfmpeg + transcribe: WebSpeechTranscriber + inputPath: string + tempDir: string + declaredDuration: number | null +}): Promise => { + const outputPattern = join(tempDir, 'chunk-%03d.mp3') + await ffmpeg( + buildRecordingAudioFfmpegArgs({ inputPath, outputPattern }), + declaredDuration ?? 0, + TRANSCRIPTION_FAILURE, + FFMPEG_TIMEOUT_MS, + ) + const chunkFiles = (await readdir(tempDir)).filter((name) => /^chunk-\d+\.mp3$/.test(name)).sort() + if (chunkFiles.length === 0) throw new Error(TRANSCRIPTION_FAILURE) + + const chunks: RecordingChunkTranscription[] = [] + let asrSeconds = 0 + let measuredDuration = 0 + for (const [index, chunkFile] of chunkFiles.entries()) { + const bytes = await readFile(join(tempDir, chunkFile)) + const result = await transcribe(new Blob([bytes], { type: 'audio/mpeg' }), { + filename: chunkFile, + }) + if (!result.ok) throw new Error(result.error) + chunks.push({ + offsetSeconds: index * RECORDING_AUDIO_CHUNK_SECONDS, + segments: result.segments, + }) + asrSeconds += result.durationSeconds ?? RECORDING_AUDIO_CHUNK_SECONDS + if (result.durationSeconds !== null) measuredDuration += result.durationSeconds + } + + const segments = mergeChunkTranscriptions(chunks) + if (segments.length === 0) throw new Error(TRANSCRIPTION_FAILURE) + + // Only measured data (or the declared duration / the last segment end) — + // never the chunk count as a duration. + const lastSegmentEnd = segments.at(-1)?.endSeconds + const durationSeconds = + measuredDuration > 0 + ? Math.round(measuredDuration) + : (declaredDuration ?? (lastSegmentEnd !== undefined ? Math.round(lastSegmentEnd) : null)) + + return { segments, durationSeconds, asrSeconds } +} + +/** + * The artifact and the speech in ONE transaction (`req` threaded through the + * upload and the upsert). Throws `WebSpeechIngestError('media')`; the caller + * owns the best-effort cleanup of a thumbnail created before the failure. + */ +const persistIngestedSpeech = async ({ + payload, + finding, + sourceKey, + speechAt, + metadata, + facets, + segments, + durationSeconds, + inputPath, + thumbnail, +}: { + payload: Payload + finding: WebSpeechFinding + sourceKey: string + speechAt: string + metadata: { title: string | null; channel: string | null; externalId: string | null } + facets: SpeechClassificationResult['facets'] + segments: RecordingTranscriptSegment[] + durationSeconds: number | null + inputPath: string + thumbnail: number | null +}): Promise => { + const alt = `${webSpeechPlatformLabel(finding.platform)} — ${metadata.title ?? speechAt}` + + try { + await withPayloadTransaction(payload, async ({ req }) => { + const media = await payload.create({ + collection: INTERNET_SPEECH_MEDIA_SLUG, + data: { alt }, + filePath: inputPath, + req, + // Intentional bypass: the ingestion CLI is a trusted actor with no session. + overrideAccess: true, + }) + + await upsertSpeechBundle( + payload, + { + ...baseBundle(finding, sourceKey, speechAt, { ...metadata, durationSeconds }), + facets, + segments: segments.map((segment) => ({ + startSeconds: segment.startSeconds, + endSeconds: segment.endSeconds, + text: segment.text, + })), + mirroredMedia: media.id, + thumbnail, + }, + { req }, + ) + }) + } catch (error) { + throw new WebSpeechIngestError('media', messageOf(error, MEDIA_FAILURE)) + } +} + +export const ingestWebSpeech = async ( + payload: Payload, + finding: WebSpeechFinding, + { reprocess = false, deps = {} }: { reprocess?: boolean; deps?: WebSpeechIngestDeps } = {}, +): Promise => { + const sourceKey = webSpeechSourceKey(finding) + const speechAt = webSpeechAtFromPublishedAt(finding.publishedAt) + if (!speechAt) + return emptyResult(sourceKey, 'failed', 'validation', 'Data de publicação inválida.') + + const acquire = deps.acquire ?? defaultAcquire + const transcribe = deps.transcribe ?? deepInfraTranscribeSegments + const classify = deps.classify ?? classifySpeech + const ffmpeg = deps.runFfmpeg ?? runFfmpeg + const downloadThumbnail = + deps.downloadThumbnail ?? + ((url, destinationPath) => downloadUrlToFile({ url, destinationPath })) + + let asrSeconds = 0 + let tempDir: string | null = null + + try { + const state = await findSpeechImportState(payload, sourceKey) + + if (isCompleteWebSpeechState(state) && !reprocess) { + // Complete row: refresh only the metadata the finding brought (undefined + // preserves the stored values); never re-download or re-transcribe. + await upsertSpeechBundle( + payload, + baseBundle(finding, sourceKey, speechAt, { + title: finding.title, + channel: finding.channel, + externalId: finding.externalId, + durationSeconds: finding.durationSeconds ?? state?.durationSeconds ?? undefined, + }), + ) + return emptyResult(sourceKey, 'skipped', null, null) + } + + tempDir = await mkdtemp(join(tmpdir(), 'web-speech-')) + let acquisition: WebSpeechAcquisition + try { + acquisition = await acquire(finding, tempDir) + } catch (error) { + throw new WebSpeechIngestError('acquisition', messageOf(error, ACQUISITION_FAILURE)) + } + + const inputPath = join(tempDir, acquisition.fileName) + const metadata = acquisition.metadata + const title = finding.title ?? metadata?.title ?? null + const channel = finding.channel ?? metadata?.channel ?? null + const externalId = finding.externalId ?? metadata?.externalId ?? null + const declaredDuration = finding.durationSeconds ?? metadata?.durationSeconds ?? null + const thumbnailUrl = finding.thumbnailUrl ?? metadata?.thumbnailUrl ?? null + + let extracted: ExtractedTranscript + try { + extracted = await extractTranscript({ + ffmpeg, + transcribe, + inputPath, + tempDir, + declaredDuration, + }) + } catch (error) { + throw new WebSpeechIngestError('transcription', messageOf(error, TRANSCRIPTION_FAILURE)) + } + asrSeconds = extracted.asrSeconds + + let classification: SpeechClassificationResult + try { + classification = await classify({ + transcript: extracted.segments.map((segment) => segment.text).join(' '), + summary: title, + keywords: [], + }) + } catch (error) { + throw new WebSpeechIngestError('classification', messageOf(error, CLASSIFICATION_FAILURE)) + } + + // Thumbnail is best effort and lives OUTSIDE the main transaction: a broken + // image (or a failed upload) must never lose the speech. + let thumbnailId: number | null = null + if (thumbnailUrl) { + try { + const destination = join(tempDir, `thumbnail${extensionOf(thumbnailUrl, '.jpg')}`) + await downloadThumbnail(thumbnailUrl, destination) + const thumbnail = await payload.create({ + collection: INTERNET_SPEECH_MEDIA_SLUG, + data: { alt: `Capa — ${title ?? speechAt}` }, + filePath: destination, + // Intentional bypass: the ingestion CLI is a trusted actor with no session. + overrideAccess: true, + }) + thumbnailId = thumbnail.id + } catch { + thumbnailId = null + } + } + + const previousThumbnail = state?.thumbnail ?? null + const previousMediaIds = [ + ...(typeof state?.mirroredMedia === 'number' ? [state.mirroredMedia] : []), + // Only a replaced thumbnail is deleted; keeping it is the fallback when + // the new download failed. + ...(thumbnailId !== null && typeof previousThumbnail === 'number' ? [previousThumbnail] : []), + ] + + try { + await persistIngestedSpeech({ + payload, + finding, + sourceKey, + speechAt, + metadata: { title, channel, externalId }, + facets: classification.facets, + segments: extracted.segments, + durationSeconds: extracted.durationSeconds, + inputPath, + thumbnail: thumbnailId ?? previousThumbnail, + }) + } catch (error) { + if (thumbnailId !== null) { + await payload + .delete({ + collection: INTERNET_SPEECH_MEDIA_SLUG, + id: thumbnailId, + // Intentional bypass: the pipeline cleans up the file it just created. + overrideAccess: true, + }) + .catch(() => undefined) + } + throw error + } + + // Replaced artifacts are removed best effort: an orphan object in the + // private bucket is harmless, a leaked row is not. + if (previousMediaIds.length > 0) { + await payload + .delete({ + collection: INTERNET_SPEECH_MEDIA_SLUG, + where: { id: { in: previousMediaIds } }, + // Intentional bypass: the pipeline replaces the files it owns. + overrideAccess: true, + }) + .catch(() => undefined) + } + + return { + sourceKey, + status: state ? 'updated' : 'created', + stage: null, + error: null, + asrSeconds, + asrCostUsd: (asrSeconds * DEEPINFRA_TRANSCRIBE_COST_PER_MINUTE_USD) / 60, + llmCostUsd: classification.llm.estimatedCostUsd, + } + } catch (error) { + const asrCostUsd = (asrSeconds * DEEPINFRA_TRANSCRIBE_COST_PER_MINUTE_USD) / 60 + if (error instanceof WebSpeechIngestError) { + return { + ...emptyResult(sourceKey, 'failed', error.stage, error.message), + asrSeconds, + asrCostUsd, + } + } + return { + ...emptyResult(sourceKey, 'failed', null, messageOf(error, 'Falha na ingestão da fala.')), + asrSeconds, + asrCostUsd, + } + } finally { + if (tempDir) await rm(tempDir, { recursive: true, force: true }).catch(() => undefined) + } +} diff --git a/tests/e2e/campaignSpeechAcervo.e2e.spec.ts b/tests/e2e/campaignSpeechAcervo.e2e.spec.ts index 3a524dec..2eb0f5e1 100644 --- a/tests/e2e/campaignSpeechAcervo.e2e.spec.ts +++ b/tests/e2e/campaignSpeechAcervo.e2e.spec.ts @@ -51,6 +51,7 @@ const createSpeech = async ( collection: 'speech', data: { sourceKey: campaign.fixtures.value('speech'), + origin: 'camara', speechAt: input.speechAt ?? '2026-03-10T18:00', type: 'BREVES COMUNICAÇÕES', phase: 'Breves Comunicações', diff --git a/tests/e2e/campaignSpeechCut.e2e.spec.ts b/tests/e2e/campaignSpeechCut.e2e.spec.ts index 9c2fa47f..a5a4f3de 100644 --- a/tests/e2e/campaignSpeechCut.e2e.spec.ts +++ b/tests/e2e/campaignSpeechCut.e2e.spec.ts @@ -35,6 +35,7 @@ const createSpeech = async ( collection: 'speech', data: { sourceKey: campaign.fixtures.value('speech'), + origin: 'camara', speechAt: '2026-08-11T18:48', type: 'BREVES COMUNICAÇÕES', phase: 'Breves Comunicações', diff --git a/tests/int/institutionSnapshot.int.spec.ts b/tests/int/institutionSnapshot.int.spec.ts index 3614ad9a..1a2c526e 100644 --- a/tests/int/institutionSnapshot.int.spec.ts +++ b/tests/int/institutionSnapshot.int.spec.ts @@ -65,6 +65,7 @@ describe('institution snapshot (C187)', () => { collection: 'speech', data: { sourceKey: `test-inst-${randomUUID()}`, + origin: 'camara', speechAt: '2024-05-10T17:28', type: 'DISCURSO', phase: 'Discurso', diff --git a/tests/int/speechCatalog.int.spec.ts b/tests/int/speechCatalog.int.spec.ts index 54881bf6..208516bd 100644 --- a/tests/int/speechCatalog.int.spec.ts +++ b/tests/int/speechCatalog.int.spec.ts @@ -27,6 +27,7 @@ const createSpeech = async ( collection: 'speech', data: { sourceKey: `test-${randomUUID()}`, + origin: 'camara', speechAt: '2023-02-07T17:28', type: 'BREVES COMUNICAÇÕES', phase: 'Breves Comunicações', @@ -199,6 +200,7 @@ describe('speech catalog (C153)', () => { collection: 'speech', data: { sourceKey: `test-${randomUUID()}`, + origin: 'camara', speechAt: '2023-02-08T10:00', classifiedBy: 'gazetteer', }, @@ -223,6 +225,7 @@ describe('speech catalog (C153)', () => { collection: 'speech', data: { sourceKey: `test-${randomUUID()}`, + origin: 'camara', speechAt: '2023-02-08T10:00', classifiedBy: 'gazetteer', }, diff --git a/tests/int/speechCoverage.int.spec.ts b/tests/int/speechCoverage.int.spec.ts index 4b982814..3c35cc1e 100644 --- a/tests/int/speechCoverage.int.spec.ts +++ b/tests/int/speechCoverage.int.spec.ts @@ -20,11 +20,12 @@ type SpeechCreateData = RequiredDataFromCollectionSlug<'speech'> const createSpeech = ( transactionID: number | string, - data: SpeechCreateData, + data: Omit, ): Promise<{ id: number }> => payload.create({ collection: 'speech', - data, + // C215 — this spec measures the Câmara coverage. + data: { origin: 'camara', ...data }, depth: 0, req: { transactionID }, // Intentional bypass: the spec seeds its own rows. diff --git a/tests/int/themeSnapshot.int.spec.ts b/tests/int/themeSnapshot.int.spec.ts index 28931b4e..533ebc37 100644 --- a/tests/int/themeSnapshot.int.spec.ts +++ b/tests/int/themeSnapshot.int.spec.ts @@ -59,6 +59,7 @@ describe('theme snapshot (C190)', () => { collection: 'speech', data: { sourceKey: `test-theme-${randomUUID()}`, + origin: 'camara', speechAt: '2024-05-10T17:28', type: 'DISCURSO', phase: 'Discurso', diff --git a/tests/int/webSpeechImport.int.spec.ts b/tests/int/webSpeechImport.int.spec.ts new file mode 100644 index 00000000..7ee82721 --- /dev/null +++ b/tests/int/webSpeechImport.int.spec.ts @@ -0,0 +1,205 @@ +// @vitest-environment node + +import { randomUUID } from 'node:crypto' +import type { Payload } from 'payload' +import { getPayload } from 'payload' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' + +import { matchMunicipalityMentions } from '@/lib/speechGazetteer' +import { INTERNET_SPEECH_MEDIA_SLUG } from '@/lib/webSpeech' +import type { InternetSpeechMedia } from '@/payload-types' +import config from '@/payload.config' +import { + findSpeechImportState, + upsertSpeechBundle, + type SpeechImportBundle, +} from '@/utilities/speech/speechImport' + +// C215 — the web-speech fields of the shared upsert: round-trip, idempotency +// (metadata refreshes, segments/facets preserved when omitted), manual +// curation wins and the speech delete cascades over the mirrored media. + +let payload: Payload +const createdSourceKeys = new Set() + +const MEDIA_BYTES = Buffer.from('web-speech-media') + +const createMedia = async (name: string): Promise => + payload.create({ + collection: INTERNET_SPEECH_MEDIA_SLUG, + data: { alt: `Arquivo ${name}` }, + file: { data: MEDIA_BYTES, mimetype: 'video/mp4', name, size: MEDIA_BYTES.length }, + overrideAccess: true, + }) + +const webBundle = (overrides: Partial = {}): SpeechImportBundle => ({ + sourceKey: `web:test-${randomUUID()}`, + origin: 'web', + platform: 'youtube', + externalId: 'abc123', + sourceUrl: 'https://www.youtube.com/watch?v=abc123', + title: 'Entrevista na rádio', + channel: 'Canal do Solla', + speechAt: '2026-09-20T00:00', + year: 2026, + legislature: null, + type: null, + phase: null, + durationSeconds: 180, + summary: null, + officialTranscript: null, + officialTextUrl: null, + keywords: [], + eventId: null, + eventType: null, + eventStartAt: null, + eventEndAt: null, + youtubeUrl: null, + presidingOfficer: null, + audioId: null, + excerptTMs: null, + vodPlaybackUrl: null, + vodDownloadUrl: null, + facets: { + topics: ['saude'], + scopes: ['bahia'], + municipalities: matchMunicipalityMentions('Feira de Santana'), + people: ['Lula'], + programs: [], + projects: [], + classifiedBy: 'gazetteer', + }, + segments: [{ startSeconds: 0, endSeconds: 2.5, text: 'A saúde pública baiana' }], + ...overrides, +}) + +const findBySourceKey = async (sourceKey: string) => { + const found = await payload.find({ + collection: 'speech', + where: { sourceKey: { equals: sourceKey } }, + depth: 1, + overrideAccess: true, + }) + return found.docs[0] ?? null +} + +describe('upsertSpeechBundle web origin (C215)', () => { + beforeAll(async () => { + payload = await getPayload({ config: await config }) + }) + + afterAll(async () => { + for (const sourceKey of createdSourceKeys) { + const speech = await findBySourceKey(sourceKey) + if (!speech) continue + await payload.delete({ collection: 'speech', id: speech.id, depth: 0, overrideAccess: true }) + } + }) + + it('persists the origin fields and the mirrored media', async () => { + const media = await createMedia('fala.mp4') + const bundle = webBundle({ mirroredMedia: media.id }) + createdSourceKeys.add(bundle.sourceKey) + + const counts = await upsertSpeechBundle(payload, bundle) + expect(counts.created).toBe(true) + + const speech = await findBySourceKey(bundle.sourceKey) + expect(speech).toMatchObject({ + origin: 'web', + platform: 'youtube', + externalId: 'abc123', + sourceUrl: 'https://www.youtube.com/watch?v=abc123', + title: 'Entrevista na rádio', + channel: 'Canal do Solla', + speechAt: '2026-09-20T00:00', + year: 2026, + durationSeconds: 180, + classifiedBy: 'gazetteer', + topics: ['saude'], + }) + expect(speech?.mirroredMedia).toMatchObject({ id: media.id }) + expect(speech?.mentionedMunicipalities?.[0]).toMatchObject({ slug: 'feira-de-santana' }) + + const state = await findSpeechImportState(payload, bundle.sourceKey) + expect(state).toMatchObject({ + origin: 'web', + platform: 'youtube', + externalId: 'abc123', + sourceUrl: 'https://www.youtube.com/watch?v=abc123', + mirroredMedia: media.id, + thumbnail: null, + segmentCount: 1, + }) + }) + + it('refreshes metadata on re-run and preserves omitted segments/facets', async () => { + const bundle = webBundle() + createdSourceKeys.add(bundle.sourceKey) + await upsertSpeechBundle(payload, bundle) + + const counts = await upsertSpeechBundle( + payload, + webBundle({ + sourceKey: bundle.sourceKey, + title: 'Título atualizado', + segments: undefined, + facets: undefined, + }), + ) + expect(counts.created).toBe(false) + expect(counts.segmentsInserted).toBe(0) + + const speech = await findBySourceKey(bundle.sourceKey) + expect(speech?.title).toBe('Título atualizado') + expect(speech?.searchText).toBe('a saude publica baiana') + expect(speech?.topics).toEqual(['saude']) + const segments = await payload.count({ + collection: 'speechSegment', + where: { speech: { equals: speech?.id } }, + overrideAccess: true, + }) + expect(segments.totalDocs).toBe(1) + }) + + it('never overwrites a manual facet curation with the web metadata', async () => { + const bundle = webBundle() + createdSourceKeys.add(bundle.sourceKey) + await upsertSpeechBundle(payload, bundle) + const speech = await findBySourceKey(bundle.sourceKey) + + await payload.update({ + collection: 'speech', + id: speech?.id, + data: { classifiedBy: 'manual', topics: ['cultura'], title: 'Curado à mão' }, + depth: 0, + overrideAccess: true, + }) + + await upsertSpeechBundle(payload, webBundle({ sourceKey: bundle.sourceKey })) + const again = await findBySourceKey(bundle.sourceKey) + expect(again?.topics).toEqual(['cultura']) + expect(again?.classifiedBy).toBe('manual') + // Manual curation is facets only: origin metadata still refreshes. + expect(again?.title).toBe('Entrevista na rádio') + }) + + it('deletes the mirrored media when the speech is deleted', async () => { + const media = await createMedia('cascata.mp4') + const bundle = webBundle({ mirroredMedia: media.id }) + createdSourceKeys.add(bundle.sourceKey) + await upsertSpeechBundle(payload, bundle) + const speech = await findBySourceKey(bundle.sourceKey) + + await payload.delete({ collection: 'speech', id: speech?.id, depth: 0, overrideAccess: true }) + + const mediaAfter = await payload + .findByID({ + collection: INTERNET_SPEECH_MEDIA_SLUG, + id: media.id, + overrideAccess: true, + }) + .catch(() => null) + expect(mediaAfter).toBeNull() + }) +}) diff --git a/tests/int/webSpeechIngest.int.spec.ts b/tests/int/webSpeechIngest.int.spec.ts new file mode 100644 index 00000000..28753cc2 --- /dev/null +++ b/tests/int/webSpeechIngest.int.spec.ts @@ -0,0 +1,355 @@ +// @vitest-environment node + +import { writeFile } from 'node:fs/promises' +import { join } from 'node:path' +import type { Payload } from 'payload' +import { getPayload } from 'payload' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' + +import { matchMunicipalityMentions } from '@/lib/speechGazetteer' +import { INTERNET_SPEECH_MEDIA_SLUG, type WebSpeechFinding } from '@/lib/webSpeech' +import type { Speech } from '@/payload-types' +import config from '@/payload.config' +import { + ingestWebSpeech, + type WebSpeechAcquirer, + type WebSpeechClassifier, + type WebSpeechIngestDeps, + type WebSpeechTranscriber, +} from '@/utilities/speech/webSpeechIngest' + +// C215 — the ingestion pipeline with injected seams: acquire → transcribe → +// classify → mirror → upsert. The fakes keep it offline; the assertions pin +// idempotency (second run only refreshes metadata), --reprocess, the mirrored +// private file and the honest failure stages. + +let payload: Payload +const acquireCalls: string[] = [] + +// Real magic bytes so Payload's own detection derives the mimeType (the fake +// pipeline writes files, but the upload must behave like production). +const MP4_BYTES = Buffer.from('00000018667479706d703432000000006d70343269736f6d', 'hex') +const MP3_BYTES = Buffer.from('fffb900000000000000000000000000000000000', 'hex') +// Real 2x2 JPEG: Payload runs sharp over image uploads (focal point), so the +// fake thumbnail must decode like a real one. +const JPEG_BYTES = Buffer.from( + '/9j/2wBDAA0JCgsKCA0LCgsODg0PEyAVExISEyccHhcgLikxMC4pLSwzOko+MzZGNywtQFdBRkxOUlNSMj5aYVpQYEpRUk//2wBDAQ4ODhMREyYVFSZPNS01T09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0//wAARCAACAAIDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAX/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAABAb/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCGACsH/9k=', + 'base64', +) + +// Every key these specs may touch, in both the external-id and the URL form: +// a failed attempt must never leak a row into the retry. +const TEST_SOURCE_KEYS = [ + 'web:youtube:abc123', + 'web:youtube:https://www.youtube.com/watch?v=abc123', + 'web:youtube:reproc', + 'web:youtube:https://www.youtube.com/watch?v=reproc', + 'web:youtube:thumb', + 'web:youtube:https://www.youtube.com/watch?v=thumb', + 'web:youtube:thumb-keep', + 'web:youtube:https://www.youtube.com/watch?v=thumb-keep', + 'web:youtube:broken', + 'web:youtube:https://www.youtube.com/watch?v=broken', + 'web:youtube:semvoz', + 'web:youtube:https://www.youtube.com/watch?v=semvoz', + 'web:radio:https://radio.example/entrevista', +] + +const cleanup = async (): Promise => { + await payload.delete({ + collection: 'speech', + where: { sourceKey: { in: TEST_SOURCE_KEYS } }, + overrideAccess: true, + }) +} + +const finding = (overrides: Partial = {}): WebSpeechFinding => ({ + platform: 'youtube', + url: 'https://www.youtube.com/watch?v=abc123', + publishedAt: '2026-09-20', + ...overrides, +}) + +const acquire: WebSpeechAcquirer = async (current, tempDir) => { + acquireCalls.push(current.url) + const fileName = 'source.mp4' + await writeFile(join(tempDir, fileName), MP4_BYTES) + return { + fileName, + metadata: { + externalId: 'abc123', + title: 'Título do yt-dlp', + channel: 'Canal do yt-dlp', + durationSeconds: 12, + thumbnailUrl: null, + }, + } +} + +const transcribe: WebSpeechTranscriber = async () => ({ + ok: true, + segments: [{ start: 0, end: 2.5, text: 'A saúde pública em Feira de Santana' }], + durationSeconds: 2.5, +}) + +const classify: WebSpeechClassifier = async () => ({ + facets: { + topics: ['saude'], + scopes: ['bahia'], + municipalities: matchMunicipalityMentions('Feira de Santana'), + people: [], + programs: [], + projects: [], + classifiedBy: 'gazetteer', + }, + llm: { used: false, totalTokens: null, estimatedCostUsd: null, error: null }, +}) + +const fakeFfmpeg: WebSpeechIngestDeps['runFfmpeg'] = async (args) => { + const outputPattern = args.at(-1) ?? '' + if (!outputPattern.includes('%03d')) throw new Error('fake ffmpeg sem outputPattern') + await writeFile(outputPattern.replace('%03d', '000'), MP3_BYTES) +} + +const deps = (overrides: WebSpeechIngestDeps = {}): WebSpeechIngestDeps => ({ + acquire, + transcribe, + classify, + runFfmpeg: fakeFfmpeg, + ...overrides, +}) + +const findBySourceKey = async (sourceKey: string): Promise => { + const found = await payload.find({ + collection: 'speech', + where: { sourceKey: { equals: sourceKey } }, + depth: 1, + overrideAccess: true, + }) + return found.docs[0] ?? null +} + +const countSegments = async (speechId: number): Promise => + ( + await payload.count({ + collection: 'speechSegment', + where: { speech: { equals: speechId } }, + overrideAccess: true, + }) + ).totalDocs + +describe('ingestWebSpeech (C215)', () => { + beforeAll(async () => { + payload = await getPayload({ config: await config }) + await cleanup() + }) + + afterAll(async () => { + await cleanup() + }) + + it('ingests one finding end to end and re-running only refreshes metadata', async () => { + acquireCalls.length = 0 + const current = finding({ externalId: 'abc123', title: 'Título do lote' }) + const sourceKey = 'web:youtube:abc123' + + const first = await ingestWebSpeech(payload, current, { deps: deps() }) + expect(first).toMatchObject({ + sourceKey, + status: 'created', + stage: null, + error: null, + }) + expect(first.asrSeconds).toBe(2.5) + expect(first.asrCostUsd).toBeGreaterThan(0) + + const speech = await findBySourceKey(sourceKey) + expect(speech).toMatchObject({ + origin: 'web', + platform: 'youtube', + externalId: 'abc123', + sourceUrl: 'https://www.youtube.com/watch?v=abc123', + title: 'Título do lote', + channel: 'Canal do yt-dlp', + speechAt: '2026-09-20T00:00', + year: 2026, + durationSeconds: 3, + classifiedBy: 'gazetteer', + topics: ['saude'], + }) + expect(speech?.searchText).toBe('a saude publica em feira de santana') + expect(speech?.mentionedMunicipalities?.[0]).toMatchObject({ slug: 'feira-de-santana' }) + const media = typeof speech?.mirroredMedia === 'object' ? speech.mirroredMedia : null + expect(media?.mimeType).toBe('video/mp4') + expect(await countSegments(speech?.id ?? 0)).toBe(1) + expect(acquireCalls).toHaveLength(1) + + const second = await ingestWebSpeech(payload, current, { deps: deps() }) + expect(second.status).toBe('skipped') + expect(acquireCalls).toHaveLength(1) + const again = await findBySourceKey(sourceKey) + expect(again?.id).toBe(speech?.id) + expect(again?.title).toBe('Título do lote') + // The metadata-only refresh must never wipe the stored duration/media. + expect(again?.durationSeconds).toBe(3) + expect(await countSegments(again?.id ?? 0)).toBe(1) + }) + + it('reprocesses on demand and removes the replaced artifact', async () => { + acquireCalls.length = 0 + const current = finding({ + externalId: 'reproc', + url: 'https://www.youtube.com/watch?v=reproc', + }) + const sourceKey = 'web:youtube:reproc' + + await ingestWebSpeech(payload, current, { deps: deps() }) + const before = await findBySourceKey(sourceKey) + const previousMediaId = + typeof before?.mirroredMedia === 'object' ? before.mirroredMedia?.id : null + expect(previousMediaId).not.toBeNull() + + const result = await ingestWebSpeech(payload, current, { deps: deps(), reprocess: true }) + expect(result.status).toBe('updated') + expect(acquireCalls).toHaveLength(2) + + const after = await findBySourceKey(sourceKey) + const nextMediaId = typeof after?.mirroredMedia === 'object' ? after.mirroredMedia?.id : null + expect(nextMediaId).not.toBe(previousMediaId) + + const previous = await payload + .findByID({ + collection: INTERNET_SPEECH_MEDIA_SLUG, + id: previousMediaId ?? 0, + overrideAccess: true, + }) + .catch(() => null) + expect(previous).toBeNull() + }) + + it('mirrors the origin thumbnail when the finding brings one', async () => { + const current = finding({ + externalId: 'thumb', + url: 'https://www.youtube.com/watch?v=thumb', + thumbnailUrl: 'https://i.ytimg.com/vi/thumb/hq.jpg', + }) + const sourceKey = 'web:youtube:thumb' + + const result = await ingestWebSpeech(payload, current, { + deps: deps({ + downloadThumbnail: async (_url, destinationPath) => { + await writeFile(destinationPath, JPEG_BYTES) + }, + }), + }) + expect(result.status).toBe('created') + + const speech = await findBySourceKey(sourceKey) + const thumbnail = typeof speech?.thumbnail === 'object' ? speech.thumbnail : null + expect(thumbnail?.mimeType).toBe('image/jpeg') + }) + + it('keeps the stored thumbnail when a reprocess cannot fetch a new one', async () => { + const current = finding({ + externalId: 'thumb-keep', + url: 'https://www.youtube.com/watch?v=thumb-keep', + thumbnailUrl: 'https://i.ytimg.com/vi/thumb-keep/hq.jpg', + }) + const sourceKey = 'web:youtube:thumb-keep' + + await ingestWebSpeech(payload, current, { + deps: deps({ + downloadThumbnail: async (_url, destinationPath) => { + await writeFile(destinationPath, JPEG_BYTES) + }, + }), + }) + const before = await findBySourceKey(sourceKey) + const beforeThumbnail = typeof before?.thumbnail === 'object' ? before.thumbnail?.id : null + expect(beforeThumbnail).not.toBeNull() + + const result = await ingestWebSpeech(payload, current, { + deps: deps({ + downloadThumbnail: async () => { + throw new Error('capa indisponível') + }, + }), + reprocess: true, + }) + expect(result.status).toBe('updated') + + const after = await findBySourceKey(sourceKey) + const afterThumbnail = typeof after?.thumbnail === 'object' ? after.thumbnail?.id : null + expect(afterThumbnail).toBe(beforeThumbnail) + }) + + it('fails honestly when the acquisition fails, without a ghost row', async () => { + const current = finding({ + externalId: 'broken', + url: 'https://www.youtube.com/watch?v=broken', + }) + const sourceKey = 'web:youtube:broken' + + const result = await ingestWebSpeech(payload, current, { + deps: deps({ + acquire: async () => { + throw new Error('yt-dlp falhou no download') + }, + }), + }) + expect(result).toMatchObject({ status: 'failed', stage: 'acquisition' }) + expect(result.error).toContain('yt-dlp falhou no download') + expect(await findBySourceKey(sourceKey)).toBeNull() + }) + + it('fails honestly when the transcription fails', async () => { + const current = finding({ + externalId: 'semvoz', + url: 'https://www.youtube.com/watch?v=semvoz', + }) + const sourceKey = 'web:youtube:semvoz' + + const result = await ingestWebSpeech(payload, current, { + deps: deps({ + transcribe: async () => ({ ok: false, error: 'Transcrição vazia.', status: 502 }), + }), + }) + expect(result).toMatchObject({ status: 'failed', stage: 'transcription' }) + expect(result.error).toContain('Transcrição vazia') + expect(await findBySourceKey(sourceKey)).toBeNull() + }) + + it('accepts a radio finding with a direct mediaUrl', async () => { + const current = finding({ + platform: 'radio', + url: 'https://radio.example/entrevista', + mediaUrl: 'https://radio.example/entrevista.mp3', + publishedAt: '2026-09-21', + }) + const sourceKey = 'web:radio:https://radio.example/entrevista' + + const downloaded: string[] = [] + const result = await ingestWebSpeech(payload, current, { + deps: deps({ + acquire: async (currentFinding, tempDir) => { + downloaded.push(currentFinding.mediaUrl ?? '') + const fileName = 'source.mp3' + await writeFile(join(tempDir, fileName), MP3_BYTES) + return { fileName, metadata: null } + }, + }), + }) + expect(result.status).toBe('created') + expect(downloaded).toEqual(['https://radio.example/entrevista.mp3']) + + const speech = await findBySourceKey(sourceKey) + expect(speech).toMatchObject({ + origin: 'web', + platform: 'radio', + sourceUrl: 'https://radio.example/entrevista', + channel: null, + }) + const media = typeof speech?.mirroredMedia === 'object' ? speech.mirroredMedia : null + expect(media?.mimeType).toBe('audio/mpeg') + }) +}) diff --git a/tests/unit/downloadToFile.unit.spec.ts b/tests/unit/downloadToFile.unit.spec.ts new file mode 100644 index 00000000..b3a32386 --- /dev/null +++ b/tests/unit/downloadToFile.unit.spec.ts @@ -0,0 +1,104 @@ +// @vitest-environment node + +import { mkdtemp, readFile, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' + +import { downloadUrlToFile } from '@/utilities/media/downloadToFile' + +// C215 — the direct-acquisition streamer: non-2xx fails before writing, the +// byte ceiling guards the disk and the happy path writes the exact bytes. + +let tempDir: string + +const response = ({ + body, + status = 200, + headers = {}, +}: { + body: ReadableStream | null + status?: number + headers?: Record +}) => + ({ + ok: status >= 200 && status < 300, + status, + body, + headers: new Headers(headers), + }) as Response + +const streamOf = (chunks: string[]): ReadableStream => + new ReadableStream({ + start(controller) { + for (const chunk of chunks) controller.enqueue(new TextEncoder().encode(chunk)) + controller.close() + }, + }) + +const fetchOf = (value: Response) => (async () => value) as unknown as typeof fetch + +describe('downloadUrlToFile', () => { + beforeAll(async () => { + tempDir = await mkdtemp(join(tmpdir(), 'download-to-file-')) + }) + + afterAll(async () => { + await rm(tempDir, { recursive: true, force: true }) + }) + + it('writes the streamed body to the destination', async () => { + const destinationPath = join(tempDir, 'ok.mp3') + await downloadUrlToFile({ + url: 'https://radio.example/a.mp3', + destinationPath, + fetchImpl: fetchOf(response({ body: streamOf(['fala', ' da ', 'radio']) })), + }) + expect(await readFile(destinationPath, 'utf8')).toBe('fala da radio') + }) + + it('fails on a non-2xx answer without writing the file', async () => { + const destinationPath = join(tempDir, 'missing.mp3') + await expect( + downloadUrlToFile({ + url: 'https://radio.example/missing.mp3', + destinationPath, + fetchImpl: fetchOf(response({ body: null, status: 404 })), + }), + ).rejects.toThrow(/HTTP 404/) + }) + + it('fails when the response has no body', async () => { + await expect( + downloadUrlToFile({ + url: 'https://radio.example/empty.mp3', + destinationPath: join(tempDir, 'empty.mp3'), + fetchImpl: fetchOf(response({ body: null })), + }), + ).rejects.toThrow(/sem corpo/) + }) + + it('refuses a declared size above the ceiling', async () => { + await expect( + downloadUrlToFile({ + url: 'https://radio.example/big.mp3', + destinationPath: join(tempDir, 'big.mp3'), + fetchImpl: fetchOf( + response({ body: streamOf(['x']), headers: { 'content-length': '99' } }), + ), + maxBytes: 10, + }), + ).rejects.toThrow(/tamanho máximo/) + }) + + it('aborts mid-stream when the body crosses the ceiling', async () => { + await expect( + downloadUrlToFile({ + url: 'https://radio.example/stream.mp3', + destinationPath: join(tempDir, 'stream.mp3'), + fetchImpl: fetchOf(response({ body: streamOf(['12345', '67890', 'x']) })), + maxBytes: 10, + }), + ).rejects.toThrow(/tamanho máximo/) + }) +}) diff --git a/tests/unit/speechExcerptsTool.unit.spec.ts b/tests/unit/speechExcerptsTool.unit.spec.ts index 5f3e00d7..d5ed4fb7 100644 --- a/tests/unit/speechExcerptsTool.unit.spec.ts +++ b/tests/unit/speechExcerptsTool.unit.spec.ts @@ -48,7 +48,12 @@ const execute = (findSpeechExcerpts(ctxFor(actor, payload)) as unknown as ExecutableTool).execute(args) const SPEECH_WHERE = { - and: [{ searchText: { like: 'hospital' } }, { searchText: { like: 'suburbio' } }], + and: [ + // C215 — the assistant keeps suggesting Câmara excerpts only. + { origin: { equals: 'camara' } }, + { searchText: { like: 'hospital' } }, + { searchText: { like: 'suburbio' } }, + ], } const reranked = (index: number, reason = 'Serve à intenção') => ({ index, reason }) diff --git a/tests/unit/speechListFilters.unit.spec.ts b/tests/unit/speechListFilters.unit.spec.ts index d9673779..7fa49673 100644 --- a/tests/unit/speechListFilters.unit.spec.ts +++ b/tests/unit/speechListFilters.unit.spec.ts @@ -8,9 +8,12 @@ import { } from '@/utilities/speech/speechListFilters' import { parseSpeechListParams } from '@/utilities/speech/speechListUrl' +// C215 — the Câmara list contract always carries the source discriminator. +const camara = { origin: { equals: 'camara' } } + describe('buildSpeechListWhere', () => { - it('is empty without filters', () => { - expect(buildSpeechListWhere(parseSpeechListParams({}))).toEqual({}) + it('keeps only the Câmara discriminator without filters', () => { + expect(buildSpeechListWhere(parseSpeechListParams({}))).toEqual({ and: [camara] }) }) it('assembles the facet filters', () => { @@ -26,6 +29,7 @@ describe('buildSpeechListWhere', () => { expect(where).toEqual({ and: [ + camara, { year: { in: [2026] } }, { topics: { in: ['saude'] } }, { scopes: { in: ['bahia'] } }, @@ -39,6 +43,7 @@ describe('buildSpeechListWhere', () => { const where = buildSpeechListWhere(parseSpeechListParams({ q: 'Farmácia Popular' })) expect(where).toEqual({ and: [ + camara, { or: [ { searchText: { like: 'farmacia popular' } }, @@ -51,24 +56,26 @@ describe('buildSpeechListWhere', () => { it('maps each duration bucket to its range and ORs them', () => { expect(buildSpeechListWhere(parseSpeechListParams({ duration: 'curta' }))).toEqual({ - and: [{ durationSeconds: { less_than: 120 } }], + and: [camara, { durationSeconds: { less_than: 120 } }], }) expect(buildSpeechListWhere(parseSpeechListParams({ duration: ['media'] }))).toEqual({ and: [ + camara, { durationSeconds: { greater_than_equal: 120, less_than: 300 }, }, ], }) expect(buildSpeechListWhere(parseSpeechListParams({ duration: ['longa'] }))).toEqual({ - and: [{ durationSeconds: { greater_than_equal: 300 } }], + and: [camara, { durationSeconds: { greater_than_equal: 300 } }], }) expect(buildSpeechListWhere(parseSpeechListParams({ duration: ['sem_duracao'] }))).toEqual({ - and: [{ durationSeconds: { exists: false } }], + and: [camara, { durationSeconds: { exists: false } }], }) expect(buildSpeechListWhere(parseSpeechListParams({ duration: ['curta', 'longa'] }))).toEqual({ and: [ + camara, { or: [ { durationSeconds: { less_than: 120 } }, @@ -89,6 +96,7 @@ describe('buildSpeechListWhereIncludingCutOrigins (C174)', () => { expect(where).toEqual({ and: [ + camara, { or: [ { searchText: { like: 'reforma' } }, @@ -107,6 +115,7 @@ describe('buildSpeechListWhereIncludingCutOrigins (C174)', () => { ) expect(withFacet).toEqual({ and: [ + camara, { topics: { in: ['saude'] } }, { or: [ @@ -132,6 +141,7 @@ describe('buildSpeechListWhere theme expansion (C192)', () => { expect(where).toEqual({ and: [ + camara, { or: [ { searchText: { like: 'defesa do sus' } }, @@ -148,7 +158,7 @@ describe('buildSpeechListWhere theme expansion (C192)', () => { it('dedupes a term that repeats the query', () => { expect(buildSpeechListWhere(parseSpeechListParams({ q: 'SUS' }), ['SUS'])).toEqual({ - and: [{ or: [{ searchText: { like: 'sus' } }, { keywords: { contains: 'SUS' } }] }], + and: [camara, { or: [{ searchText: { like: 'sus' } }, { keywords: { contains: 'SUS' } }] }], }) }) @@ -161,6 +171,7 @@ describe('buildSpeechListWhere theme expansion (C192)', () => { ), ).toEqual({ and: [ + camara, { topics: { in: ['saude'] } }, { or: [ diff --git a/tests/unit/webSpeech.unit.spec.ts b/tests/unit/webSpeech.unit.spec.ts new file mode 100644 index 00000000..e18e1b5b --- /dev/null +++ b/tests/unit/webSpeech.unit.spec.ts @@ -0,0 +1,147 @@ +import { describe, expect, it } from 'vitest' + +import { + canonicalWebSpeechUrl, + parseWebSpeechBatch, + parseWebSpeechFinding, + webSpeechAtFromPublishedAt, + webSpeechPlatformLabel, + webSpeechSourceKey, +} from '@/lib/webSpeech' + +// C215 — the pure web-speech contract: identity (sourceKey), canonical URL and +// the finding-batch validation that fails one entry without killing the batch. + +describe('canonicalWebSpeechUrl', () => { + it('collapses every YouTube shape to the watch URL', () => { + expect(canonicalWebSpeechUrl('youtube', 'https://youtu.be/dQw4w9WgXcQ?si=track')).toBe( + 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', + ) + expect( + canonicalWebSpeechUrl('youtube', 'https://www.youtube.com/shorts/dQw4w9WgXcQ?feature=share'), + ).toBe('https://www.youtube.com/watch?v=dQw4w9WgXcQ') + expect( + canonicalWebSpeechUrl('youtube', 'https://m.youtube.com/watch?v=dQw4w9WgXcQ&t=30s'), + ).toBe('https://www.youtube.com/watch?v=dQw4w9WgXcQ') + expect(canonicalWebSpeechUrl('youtube', 'https://www.youtube.com/live/dQw4w9WgXcQ')).toBe( + 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', + ) + }) + + it('collapses Instagram post shapes and normalizes reels', () => { + expect( + canonicalWebSpeechUrl('instagram', 'https://www.instagram.com/reels/ABC123/?igsh=xyz'), + ).toBe('https://www.instagram.com/reel/ABC123/') + expect(canonicalWebSpeechUrl('instagram', 'https://instagram.com/p/ABC123')).toBe( + 'https://www.instagram.com/p/ABC123/', + ) + }) + + it('drops tracking params, the fragment and the trailing slash for direct audio', () => { + expect( + canonicalWebSpeechUrl( + 'radio', + 'https://RadioBahia.com.br/audio/entrevista.mp3/?utm_source=share&id=7#t=1', + ), + ).toBe('https://radiobahia.com.br/audio/entrevista.mp3?id=7') + }) + + it('keeps an unparseable URL trimmed instead of inventing one', () => { + expect(canonicalWebSpeechUrl('audio', ' nao-e-url ')).toBe('nao-e-url') + }) +}) + +describe('webSpeechSourceKey', () => { + it('prefers the external id and falls back to the canonical URL', () => { + expect( + webSpeechSourceKey({ + platform: 'youtube', + url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', + externalId: 'dQw4w9WgXcQ', + }), + ).toBe('web:youtube:dQw4w9WgXcQ') + expect( + webSpeechSourceKey({ platform: 'audio', url: 'https://radio.example/a.mp3?utm_source=x' }), + ).toBe('web:audio:https://radio.example/a.mp3') + }) + + it('is stable across YouTube URL shapes (same identity, no duplicate row)', () => { + const short = webSpeechSourceKey({ platform: 'youtube', url: 'https://youtu.be/dQw4w9WgXcQ' }) + const long = webSpeechSourceKey({ + platform: 'youtube', + url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=90', + }) + expect(short).toBe(long) + expect(short).toBe('web:youtube:https://www.youtube.com/watch?v=dQw4w9WgXcQ') + }) +}) + +describe('webSpeechAtFromPublishedAt', () => { + it('normalizes date-only and converts an explicit timezone to Brasília', () => { + expect(webSpeechAtFromPublishedAt('2026-09-20')).toBe('2026-09-20T00:00') + // 14:35Z is 11:35 in America/Bahia (UTC-3, no DST). + expect(webSpeechAtFromPublishedAt('2026-09-20T14:35:00Z')).toBe('2026-09-20T11:35') + expect(webSpeechAtFromPublishedAt('2026-09-20T14:35:00-03:00')).toBe('2026-09-20T14:35') + expect(webSpeechAtFromPublishedAt('2026-09-20 14:35')).toBe('2026-09-20T14:35') + }) + + it('returns null for an unrecognized or impossible date', () => { + expect(webSpeechAtFromPublishedAt('20/09/2026')).toBeNull() + expect(webSpeechAtFromPublishedAt('2026-09-20lixo')).toBeNull() + expect(webSpeechAtFromPublishedAt('2026-99-99')).toBeNull() + expect(webSpeechAtFromPublishedAt('2026-09-20T25:00')).toBeNull() + }) +}) + +describe('parseWebSpeechFinding', () => { + const validYoutube = { + platform: 'youtube', + url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', + publishedAt: '2026-09-20', + } + + it('accepts a YouTube finding without mediaUrl', () => { + const parsed = parseWebSpeechFinding(validYoutube) + expect(parsed.ok).toBe(true) + }) + + it('requires mediaUrl for radio/audio and publishedAt everywhere', () => { + const radio = parseWebSpeechFinding({ ...validYoutube, platform: 'radio' }) + expect(radio.ok).toBe(false) + if (!radio.ok) expect(radio.error).toContain('mediaUrl') + + const noDate = parseWebSpeechFinding({ ...validYoutube, publishedAt: undefined }) + expect(noDate.ok).toBe(false) + + const badDate = parseWebSpeechFinding({ ...validYoutube, publishedAt: 'ontem' }) + expect(badDate.ok).toBe(false) + }) + + it('rejects an unknown platform and a non-URL', () => { + expect(parseWebSpeechFinding({ ...validYoutube, platform: 'tiktok' }).ok).toBe(false) + expect(parseWebSpeechFinding({ ...validYoutube, url: 'nada' }).ok).toBe(false) + }) +}) + +describe('parseWebSpeechBatch', () => { + it('validates only the envelope and keeps the findings raw', () => { + const parsed = parseWebSpeechBatch({ generatedAt: '2026-09-24T10:00:00Z', findings: [{}] }) + expect(parsed.ok).toBe(true) + if (parsed.ok) { + expect(parsed.batch.generatedAt).toBe('2026-09-24T10:00:00Z') + expect(parsed.batch.findings).toEqual([{}]) + } + }) + + it('fails closed on a malformed envelope', () => { + expect(parseWebSpeechBatch({}).ok).toBe(false) + expect(parseWebSpeechBatch({ findings: 'nope' }).ok).toBe(false) + }) +}) + +describe('webSpeechPlatformLabel', () => { + it('owns the pt-BR labels the acervo shows', () => { + expect(webSpeechPlatformLabel('youtube')).toBe('YouTube') + expect(webSpeechPlatformLabel('radio')).toBe('Rádio') + }) +}) diff --git a/tests/unit/webSpeeches.unit.spec.ts b/tests/unit/webSpeeches.unit.spec.ts new file mode 100644 index 00000000..fb436214 --- /dev/null +++ b/tests/unit/webSpeeches.unit.spec.ts @@ -0,0 +1,147 @@ +import { describe, expect, it } from 'vitest' + +import { + formatWebSpeechReport, + isCompleteWebSpeechState, + planWebSpeechBatch, + summarizePlannedActions, + summarizeWebSpeechResults, + webSpeechPlannedAction, +} from '../../scripts/lib/webSpeeches.mjs' + +type WebSpeechIngestResult = + import('../../src/utilities/speech/webSpeechIngest.ts').WebSpeechIngestResult + +// C215 — the CLI planning/reporting rules: one invalid finding fails only +// itself, duplicates are dropped by natural key and the report never hides a +// failure. + +const youtube = (overrides = {}) => ({ + platform: 'youtube', + url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', + publishedAt: '2026-09-20', + ...overrides, +}) + +describe('planWebSpeechBatch', () => { + it('keeps the valid entries and reports invalid/duplicates separately', () => { + const plan = planWebSpeechBatch([ + youtube(), + youtube({ url: 'https://youtu.be/dQw4w9WgXcQ' }), + youtube({ platform: 'radio' }), + youtube({ externalId: 'outro', url: 'https://www.youtube.com/watch?v=zzz' }), + { platform: 'youtube', url: 'nao-e-url', publishedAt: '2026-09-20' }, + ]) + + expect(plan.entries.map((entry) => entry.sourceKey)).toEqual([ + 'web:youtube:https://www.youtube.com/watch?v=dQw4w9WgXcQ', + 'web:youtube:outro', + ]) + expect(plan.duplicates).toEqual([ + { index: 1, sourceKey: 'web:youtube:https://www.youtube.com/watch?v=dQw4w9WgXcQ' }, + ]) + expect(plan.invalid.map((entry) => entry.index)).toEqual([2, 4]) + }) +}) + +describe('webSpeechPlannedAction', () => { + it('reads the stored state: new, incomplete, complete', () => { + expect(webSpeechPlannedAction(null)).toBe('novo') + expect(webSpeechPlannedAction({ segmentCount: 0, mirroredMedia: null })).toBe('atualizado') + expect(webSpeechPlannedAction({ segmentCount: 3, mirroredMedia: null })).toBe('atualizado') + expect(webSpeechPlannedAction({ segmentCount: 3, mirroredMedia: 9 })).toBe('ignorado') + expect(webSpeechPlannedAction({ segmentCount: 3, mirroredMedia: 9 }, true)).toBe('reprocessado') + }) + + it('isCompleteWebSpeechState requires transcript and mirrored file', () => { + expect(isCompleteWebSpeechState(null)).toBe(false) + expect(isCompleteWebSpeechState({ segmentCount: 0, mirroredMedia: 1 })).toBe(false) + expect(isCompleteWebSpeechState({ segmentCount: 1, mirroredMedia: null })).toBe(false) + expect(isCompleteWebSpeechState({ segmentCount: 1, mirroredMedia: 1 })).toBe(true) + }) +}) + +describe('summarizeWebSpeechResults', () => { + it('counts statuses, sums costs and lists every failure', () => { + const result = (overrides: Partial): WebSpeechIngestResult => ({ + sourceKey: 'web:youtube:x', + status: 'skipped', + stage: null, + error: null, + asrSeconds: 0, + asrCostUsd: 0, + llmCostUsd: null, + ...overrides, + }) + const summary = summarizeWebSpeechResults([ + result({ status: 'created', asrSeconds: 60, asrCostUsd: 0.00045, llmCostUsd: 0.0001 }), + result({ status: 'updated', asrSeconds: 120, asrCostUsd: 0.0009 }), + result({}), + result({ + status: 'failed', + sourceKey: 'web:audio:https://radio.example/a.mp3', + stage: 'acquisition', + error: 'HTTP 404', + }), + ]) + + expect(summary).toMatchObject({ created: 1, updated: 1, skipped: 1, failed: 1 }) + expect(summary.asrSeconds).toBe(180) + expect(summary.asrCostUsd).toBeCloseTo(0.00135, 6) + expect(summary.llmCostUsd).toBeCloseTo(0.0001, 6) + expect(summary.failures).toEqual([ + { + sourceKey: 'web:audio:https://radio.example/a.mp3', + stage: 'acquisition', + error: 'HTTP 404', + }, + ]) + }) +}) + +describe('formatWebSpeechReport', () => { + const base = { + runAt: '2026-09-24T10:00:00.000Z', + mode: 'import', + findings: 4, + planned: 3, + invalid: [{ index: 3, error: 'url: inválida' }], + duplicates: [{ index: 1, sourceKey: 'web:youtube:x' }], + durationMs: 12_500, + created: 1, + updated: 1, + skipped: 1, + failed: 1, + asrSeconds: 90, + asrCostUsd: 0.000675, + llmCostUsd: 0.0001, + failures: [ + { + sourceKey: 'web:audio:https://radio.example/a.mp3', + stage: 'acquisition' as const, + error: 'HTTP 404', + }, + ], + } + + it('prints counts, costs, the invalid entries and every failure', () => { + const lines = formatWebSpeechReport(base).join('\n') + expect(lines).toContain('novos: 1 · atualizados: 1 · ignorados: 1 · falhas: 1') + expect(lines).toContain('ASR: 1min30s (~US$ 0.0007)') + expect(lines).toContain('inválido #3') + expect(lines).toContain('web:audio:https://radio.example/a.mp3 (acquisition): HTTP 404') + }) + + it('prints the dry-run plan instead of the execution counts', () => { + const lines = formatWebSpeechReport({ + ...base, + mode: 'dry-run', + plannedActions: summarizePlannedActions([ + { action: 'novo' }, + { action: 'ignorado' }, + { action: 'reprocessado' }, + ]), + }).join('\n') + expect(lines).toContain('plano (dry-run): novos: 1 · atualizados: 0 · ignorados: 1') + }) +}) diff --git a/tests/unit/ytdlp.unit.spec.ts b/tests/unit/ytdlp.unit.spec.ts new file mode 100644 index 00000000..ef92162d --- /dev/null +++ b/tests/unit/ytdlp.unit.spec.ts @@ -0,0 +1,145 @@ +import { describe, expect, it } from 'vitest' + +import { + downloadWithYtDlp, + parseYtDlpMetadata, + readYtDlpMetadata, + resolveYtDlp, + YTDLP_FORMAT, + type YtDlpResult, +} from '@/utilities/media/ytdlp' + +// C215 — the yt-dlp owner: fail-closed resolution, metadata parsing and the +// deterministic download command (format cap, merge, ffmpeg location). + +const okRun = + (stdout = '') => + async (): Promise => ({ ok: true, stdout, stderr: '' }) + +describe('resolveYtDlp', () => { + it('accepts the explicit YTDLP_PATH and the PATH fallback', async () => { + await expect( + resolveYtDlp({ env: { YTDLP_PATH: '/custom/yt-dlp' }, run: okRun('2026.09.20') }), + ).resolves.toEqual({ ok: true, bin: '/custom/yt-dlp' }) + await expect(resolveYtDlp({ env: {}, run: okRun('2026.09.20') })).resolves.toEqual({ + ok: true, + bin: 'yt-dlp', + }) + }) + + it('fails closed with the reason when the binary is missing', async () => { + const missing = async (): Promise => ({ + ok: false, + stdout: '', + stderr: '', + error: { code: 'ENOENT' }, + }) + const resolved = await resolveYtDlp({ env: {}, run: missing }) + expect(resolved.ok).toBe(false) + if (!resolved.ok) expect(resolved.reason).toContain('não encontrado') + }) +}) + +describe('parseYtDlpMetadata', () => { + it('maps the dump-json document and falls back to uploader', () => { + expect( + parseYtDlpMetadata({ + id: 'dQw4w9WgXcQ', + title: ' Entrevista ', + uploader: 'Canal do Solla', + duration: 123.4, + thumbnail: 'https://i.ytimg.com/vi/dQw4w9WgXcQ/hq.jpg', + }), + ).toEqual({ + externalId: 'dQw4w9WgXcQ', + title: 'Entrevista', + channel: 'Canal do Solla', + durationSeconds: 123.4, + thumbnailUrl: 'https://i.ytimg.com/vi/dQw4w9WgXcQ/hq.jpg', + }) + }) + + it('keeps absent/invalid fields null instead of guessing', () => { + expect(parseYtDlpMetadata({ duration: -5, title: '' })).toEqual({ + externalId: null, + title: null, + channel: null, + durationSeconds: null, + thumbnailUrl: null, + }) + }) +}) + +describe('readYtDlpMetadata', () => { + it('parses the JSON stdout', async () => { + const run = okRun(JSON.stringify({ id: 'x', title: 'Fala', duration: 60 })) + await expect( + readYtDlpMetadata({ bin: 'yt-dlp', url: 'https://x', run }), + ).resolves.toMatchObject({ externalId: 'x', title: 'Fala', durationSeconds: 60 }) + }) + + it('throws with the stderr tail on failure and on malformed JSON', async () => { + const failing = async (): Promise => ({ + ok: false, + stdout: '', + stderr: 'ERROR: Video unavailable', + }) + await expect( + readYtDlpMetadata({ bin: 'yt-dlp', url: 'https://x', run: failing }), + ).rejects.toThrow(/Video unavailable/) + + await expect( + readYtDlpMetadata({ bin: 'yt-dlp', url: 'https://x', run: okRun('not json') }), + ).rejects.toThrow(/fora do formato/) + }) +}) + +describe('downloadWithYtDlp', () => { + it('caps quality, merges to mp4 and forwards the ffmpeg location', async () => { + const calls: string[][] = [] + const run = async (_bin: string, args: string[]): Promise => { + calls.push(args) + return { ok: true, stdout: '', stderr: '' } + } + + await downloadWithYtDlp({ + bin: '/bin/yt-dlp', + url: 'https://www.youtube.com/watch?v=x', + outputTemplate: '/tmp/source.%(ext)s', + ffmpegLocation: '/packed/ffmpeg', + run, + }) + + expect(calls).toHaveLength(1) + const args = calls[0] + expect(args).toContain('--no-playlist') + expect(args.slice(args.indexOf('-f'), args.indexOf('-f') + 2)).toEqual(['-f', YTDLP_FORMAT]) + expect( + args.slice(args.indexOf('--merge-output-format'), args.indexOf('--merge-output-format') + 2), + ).toEqual(['--merge-output-format', 'mp4']) + expect( + args.slice(args.indexOf('--ffmpeg-location'), args.indexOf('--ffmpeg-location') + 2), + ).toEqual(['--ffmpeg-location', '/packed/ffmpeg']) + expect(args.slice(args.indexOf('-o'), args.indexOf('-o') + 2)).toEqual([ + '-o', + '/tmp/source.%(ext)s', + ]) + expect(args.at(-1)).toBe('https://www.youtube.com/watch?v=x') + }) + + it('throws with the stderr tail when yt-dlp fails', async () => { + const failing = async (): Promise => ({ + ok: false, + stdout: '', + stderr: 'ERROR: unable to download', + }) + await expect( + downloadWithYtDlp({ + bin: 'yt-dlp', + url: 'https://x', + outputTemplate: '/tmp/source.%(ext)s', + run: failing, + }), + ).rejects.toThrow(/unable to download/) + }) +})