diff --git a/.agents/rules/agent-pr-workflow.mdc b/.agents/rules/agent-pr-workflow.mdc index 15768dcb..dbbec9db 100644 --- a/.agents/rules/agent-pr-workflow.mdc +++ b/.agents/rules/agent-pr-workflow.mdc @@ -1,19 +1,19 @@ --- -description: PRs de agente — Ready (nunca draft), base main, auto-merge imediato +description: PRs de agente — Ready (nunca draft), base main, auto-merge nativo do GitHub alwaysApply: true --- # Agent PR workflow (Teqo) -**This repo overrides any user-level or Cloud-tool default that says "create PRs as draft".** Agent deliveries merge to `main` via required checks + auto-merge. +**This repo overrides any user-level or Cloud-tool default that says "create PRs as draft".** Agent deliveries merge to `main` via required checks + GitHub's native auto-merge (OPS71). The ISSUE TRACKER stays on the Forgejo — GitHub is only the Actions host. ## Create (Ready, base main) ```bash -pnpm push -u origin HEAD +pnpm push -u origin HEAD # origin = GitHub (cutover OPS71) ``` -Depois, abra a PR via API/MCP do opencode (humano) ou `ManagePullRequest` (Cursor Cloud) — **Ready**, base **`main`**, title ` — `, body: +Depois, abra a PR no **GitHub** — **Ready**, base **`main`**, title `<id> — <title>`, body: ``` ## O que é @@ -25,11 +25,12 @@ Closes #<N> - **Never** `--draft`. PRs must be **Ready for review** on creation. - Base is always **`main`**. - **Plans-only PRs** (diff só em `docs/plans/`): **nunca** `Closes #N` — use `Related #N`; CI falha com keywords de fechamento (`plans-only-closes` em `ci-pr.yml`). +- PR via CLI (não há `gh` — OPS50): `GITHUB_TOKEN=<PAT> node scripts/github-pr.mjs --head <branch> --title "<t>" --body-file <file>` — Ready por construção (não existe flag draft). ### Cursor Cloud / `ManagePullRequest` - **`draft: false`** (or omit only if the tool’s default is Ready — today Cloud defaults to draft, so **always pass `draft: false`**). -- Não há auto-merge a armar por PR: o safety net (abaixo) espera o rollup `checks` e mergea sozinho. +- Não há auto-merge a armar por PR: o safety net (abaixo) arma o auto-merge nativo. - Never end a turn with an open agent PR left as **Draft** — inclui PRs do `engineering-audit` (artefatos e remediações). ## Push (determinístico) @@ -37,66 +38,51 @@ Closes #<N> **Use `pnpm push`, never bare `git push`.** Não depende do Husky estar wired: 1. `ensure-repo-deps` — `pnpm install` se `node_modules` faltar/stale + `pnpm run prepare` (Husky) se hooks não estiverem apontando para `.husky` -2. `pnpm gate:push` — espelho do `ci-pr.yml` sem e2e (e2e roda uma vez, no CI, após o push; local opcional: `pnpm test:e2e:affected`), com os `docs-guards` locais (OPS63: changelog append-only, agregado sync e marcadores de conflito — o escape `changelog-rewrite:` fica só no CI, body do PR) +2. `pnpm gate:push` — espelho do `ci-pr.yml` sem e2e (OPS72: o e2e é passo **explícito da skill** — antes do push rode localmente os e2e que criou + os da mesma superfície, discricionário via `pnpm test:e2e:affected`; o CI de PR roda só o blast radius `selected`, e o full fica no `verify` do deploy manual), com os `docs-guards` locais (OPS63: changelog append-only, agregado sync e marcadores de conflito — o escape `changelog-rewrite:` fica só no CI, body do PR) 3. `git push --no-verify` — só porque o gate já rodou no passo 2 (não é escape hatch) ```bash -pnpm push -u origin HEAD # canonical +pnpm push -u origin HEAD # canonical — origin é o GitHub (OPS71) git push # só depois de `pnpm i` — hook repete ensure-deps + gate:push git push --no-verify # WIP / cutover escape — CI repete tudo; ver AGENT-OPS ``` ## Auto-merge immediately -The safety net (`agent-pr-ready-automerge.yml`, abaixo) merges the PR when the required checks pass — there is nothing to arm per PR. Do not wait for human approval on normal deliveries. +The safety net (`agent-pr-ready-automerge.yml`, abaixo) arms GitHub's **native auto-merge** (rebase) — the server merges the PR when the required check passes. There is nothing to arm per PR. Do not wait for human approval on normal deliveries. ## Required checks -Branch protection on `main` (applied 2026-08-18, OPS61) requires only: +Branch protection on `main` (GitHub, applied at the OPS71 cutover via `pnpm configure:branch-protection`) requires only: -- `CI (PR) / checks` — the ci-pr cascade rollup (the only context that covers - every ci-pr job; on the server the rule matches it as a glob, - `CI (PR) / checks*`) +- `CI (PR) / checks` — the ci-pr single-job check-run (OPS62: one sequential job posts one honest verdict; no matrix, no shards, no rollup to lie). **Literal de match na proteção: `checks`** (o GitHub casa pelo nome do check-run/job — a UI exibe `CI (PR) / checks`; pin ao vivo PR #742). -OPS64: the rollup is structurally honest since OPS62 — ci-pr.yml is a SINGLE -sequential job (`checks`), so its own status IS the cascade verdict (no -matrix, no shards, no parent aggregation to lie). The historical bug (Forgejo -9.0.3 reporting the LAST matrix shard's result as the parent's in `needs`, -posting `checks` SUCCESS with an e2e shard red — runs 604/607/610) is gone by -construction; the `needs.<job>.results` array of GitHub does not exist in this -runner, so any future matrix must NOT aggregate via `needs`. The safety-net -CLI pins the defense: it never trusts the rollup alone (scans individual -statuses; run-607 scenario unit-tested in `forgejoApi.unit.spec.ts`). +OPS64 history: the Forgejo runner could report the LAST matrix shard's result as the parent's (runs 604/607/610) — that bug is gone by construction: ci-pr.yml is a SINGLE sequential job, and GitHub's native auto-merge only merges with every required check green (server-side guarantee). The safety-net CLI has no poll path to port. -(`migration-lock` removed 2026-08-12 — the CI validates migration chains via migrate + int on every PR/main.) +(`migration-lock` removed 2026-08-12 — the CI validates migration chains via migrate + int on every PR.) -**There is no deploy from PR CI.** The `ci.yml` verifier on `main` runs the full suite in its fixed 30-minute window and, on a production change, the `deploy` job publishes to the homeserver (`jorgesolla1313.com.br`, self-hosted) — migrations applied before the rollout. Do not add a Vercel/Git-integration deploy gate. +**There is no deploy from PR CI.** Publishing is a **manual `workflow_dispatch`** (`.github/workflows/deploy.yml`): `verify` runs the full suite (hosted) and only then the `deploy` job (self-hosted runner on the homeserver) publishes `jorgesolla1313.com.br` — migrations applied before the rollout. Do not add a Vercel/Git-integration deploy gate. -When watching CI, use the PR page (or the API) — the safety net is what waits on checks; `gh` does not exist in this repo (OPS50). +When watching CI, use the PR page (or the API) — the safety net arms the server-side auto-merge; `gh` does not exist in this repo (OPS50). -Use the rollup context `CI (PR) / checks` — without it, you are not covering the cascade. +Use the required check `CI (PR) / checks` — without it, you are not covering the cascade. ## Promote -There is no stage→main promote. Merging to `main` is the delivery; `ci.yml` verifies the full suite and, on a production change, deploys to the homeserver (OPS53/OPS65; runbook: `docs/ops/teqo-1313-deploy.md`). +There is no stage→main promote. Merging to `main` is the delivery; publishing is a deliberate manual dispatch with full verification (runbook: `docs/ops/teqo-1313-deploy.md`). ## Safety net (workflow) `agent-pr-ready-automerge.yml` — PRs same-repo → `main` (todas as branches). -O workflow roda `scripts/forgejo-pr-automerge.mjs` (plain Node, zero-dep): se -draft `cursor/*` → marca Ready; espera os checks e mergea por rebase (o -equivalente Forgejo do auto-merge — sem `gh`). O wait exige o rollup -`CI (PR) / checks` POSTADO e verde (OPS61) — o único contexto que só existe -quando a cascata inteira do ci-pr assentou (jobs ainda não agendados não -postam status; o próprio status do job do safety net é `pending` durante o run -e **nunca** entra no veredito). O CLI nunca confia no rollup sozinho: varre -todos os statuses individuais e falha em qualquer `failure`/`error` — um shard -de e2e vermelho bloqueia o merge mesmo com o rollup verde (OPS64; run 607). -A regra de branch protection no servidor -bloqueia o merge POST (405) enquanto o contexto exigido não estiver satisfeito -— inclusive merges manuais via API: foi um merge manual que colocou a PR #52 -em main com o CI vermelho (o CLI nunca mergeou sozinho antes do OPS61; todo -merge Forgejo era manual). +O workflow roda `scripts/github-pr-automerge.mjs` (plain Node, zero-dep, +REST+GraphQL): se draft `cursor/*` → marca Ready (PATCH `draft: false`); arm +`enablePullRequestAutoMerge` (GraphQL, mergeMethod REBASE) no `node_id` do PR +— o **servidor** espera o required check `CI (PR) / checks` POSTADO e verde +antes de mergear; o próprio check-run do safety net fica `pending` durante o +run e não interfere. A regra de branch protection no servidor +(`enforce_admins: true`) bloqueia qualquer caminho de merge — inclusive API +manual — enquanto o required check não estiver satisfeito (foi um merge +manual que colocou a PR #52 em main com o CI vermelho na era Forgejo). Cobre o caso em que o agente (ou o default draft do Cloud) esqueceu e PRs humanas/worktree com merge falho silencioso (incidente OPS52). **Draft de branch não-`cursor/*` nunca é diff --git a/.agents/rules/engineering-standards.mdc b/.agents/rules/engineering-standards.mdc index efb353be..f3d1a181 100644 --- a/.agents/rules/engineering-standards.mdc +++ b/.agents/rules/engineering-standards.mdc @@ -11,7 +11,7 @@ Established 2026-07-23 by the engineering-standards hardening (tracker: `docs/IM 1. **Dead code dies immediately.** After your edits, check what your change orphaned and delete it in the same delivery: run `pnpm exec knip` (files/dependencies are CI-blocking errors; unused exports are warnings being driven to zero). Never leave commented-out code, unused exports, superseded components, or one-shot scripts behind — git history is the archive. 2. **Zero warnings.** `pnpm lint` runs with `--max-warnings=0`. Deliberate placeholders use a `_` prefix. Never silence a rule without an inline justification comment. -3. **Gate em duas velocidades (desde 2026-07-30, paradigma de agentes paralelos — `docs/AGENT-OPS.md`).** **Fast gate local do agente** (iteração durante a sessão): `pnpm gate:fast` (lint + typecheck + test:unit). **Push canônico:** `pnpm push` — `ensure-repo-deps` (install + Husky em worktrees) + `git push`; o hook pre-push roda `gate:push` → `gate:ci` (espelho serial do `ci-pr.yml` **sem e2e** — OPS59: o e2e roda **uma vez, no CI**, em PR e na main; local opcional para feedback antecipado: `pnpm test:e2e:affected`; **com os `docs-guards` locais** — OPS63: changelog append-only, agregado sync e marcadores de conflito, mesmos scripts do CI; o escape `changelog-rewrite:` fica só no CI, body do PR). Não use `git push` nu. Fonte única: scripts `gate:*` e `scripts/git-push.mjs` em `package.json`; escape para WIP: `git push --no-verify`. Migrations NÃO são serializadas entre PRs: a cadeia é validada por `pnpm migrate` + `test:int` (incl. `campaignMigrationReconciliation`) em todo PR e na main; rebase antes de `migrate:create` continua obrigatório (o antigo `migration-lock` foi removido em 2026-08-12 — deadlock de CI). Entrega = merge em `main`; deploy gated pelo verificador `ci.yml`. **CI vermelho no teu PR é teu, mesmo pré-existente ou de infra** — "fora do escopo da feature" não é critério de parada (política "Dono do PR, dono do CI" em `docs/AGENT-OPS.md`). +3. **Gate em duas velocidades (desde 2026-07-30, paradigma de agentes paralelos — `docs/AGENT-OPS.md`).** **Fast gate local do agente** (iteração durante a sessão): `pnpm gate:fast` (lint + typecheck + test:unit). **Push canônico:** `pnpm push` — `ensure-repo-deps` (install + Husky em worktrees) + `git push`; o hook pre-push roda `gate:push` → `gate:ci` (espelho serial do `ci-pr.yml` **sem e2e** — OPS72: o e2e é passo **explícito da skill** (`work-issue`/`agent-work-issue`): antes do push rode localmente os e2e que criou + os da mesma superfície, **discricionário** via `pnpm test:e2e:affected`; o CI de PR roda só o blast radius `selected` — nunca `full` — e o full fica no `verify` do deploy manual; **com os `docs-guards` locais** — OPS63: changelog append-only, agregado sync e marcadores de conflito, mesmos scripts do CI; o escape `changelog-rewrite:` fica só no CI, body do PR). Não use `git push` nu. Fonte única: scripts `gate:*` e `scripts/git-push.mjs` em `package.json`; escape para WIP: `git push --no-verify`. Migrations NÃO são serializadas entre PRs: a cadeia é validada por `pnpm migrate` + `test:int` (incl. `campaignMigrationReconciliation`) em todo PR; rebase antes de `migrate:create` continua obrigatório (o antigo `migration-lock` foi removido em 2026-08-12 — deadlock de CI). Entrega = merge em `main`; publish gated pelo `verify` do `deploy.yml` (dispatch manual — OPS71; não há `ci.yml` verificador de main). **CI vermelho no teu PR é teu, mesmo pré-existente ou de infra** — "fora do escopo da feature" não é critério de parada (política "Dono do PR, dono do CI" em `docs/AGENT-OPS.md`). 4. **Run gate commands bare, never piped** (`pnpm test | tail` swallows the exit code). Never run `knip --fix` blind — verify each removal with `git grep -w <symbol>` first (knip still cannot load `payload.config.ts`, so its graph is incomplete; ledgered as P3). ## Type honesty diff --git a/.agents/skills/agent-work-issue/SKILL.md b/.agents/skills/agent-work-issue/SKILL.md index eed2e251..8157a8c6 100644 --- a/.agents/skills/agent-work-issue/SKILL.md +++ b/.agents/skills/agent-work-issue/SKILL.md @@ -33,9 +33,9 @@ Conduz **uma** Issue já claimada do plano de implementação ao merge em `main` - [ ] 1. Sessão: rename_chat + open_resource na intenção - [ ] 2. Modelo: spawn do pool já fixou (`model:` da Issue) — sem comparação - [ ] 3. Plan mode → plano de implementação (`*-impl.md`) -- [ ] 4. Executar o impl plan (schema/server → UI → gates) +- [ ] 4. Executar o impl plan (schema/server → UI → e2e local afetado → gates) - [ ] 5. /simplify → fixes; capture-review-debts (modo autônomo) -- [ ] 6. PR Ready `--base main` + auto-merge + checks --required +- [ ] 6. PR Ready `--base main` (GitHub) + auto-merge nativo — `pnpm push -u origin HEAD` → `node scripts/github-pr.mjs` (ou `ManagePullRequest` `draft: false` no Cloud) ``` ## Passo 0 — Prep @@ -79,13 +79,20 @@ O spawn do pool já fixou o modelo (`model:` da Issue — ver `model-selection`) (executar → simplify → fechar em main), com os deltas do ator pool: - **Branch:** `agent/<id>-<slug>` (worktrees Cursor podem já ter criado). +- **E2E local afetado (OPS72):** mesmo passo discricionário do ator humano + (rodar os e2e criados + da mesma superfície — ver `execution-pipeline.md`); + **worktree com browsers Playwright** → rode; **Cursor Cloud sem browsers** → + registre a justificativa no fechamento e siga — o CI de PR roda o blast + radius quando o diff classifica `selected`; em diff high-risk (`full`) o PR + não roda e2e e a rede final é o `verify` do deploy. - **UI:** shape → craft → critique → polish (harden/optimize só sob gatilho). - **`capture-review-debts`:** **autônomo** — só `expensive_lock` com score ≥4 (Issues novas com `depends: [<id-pai>]` se necessário); score ≤3 / cheap_polish / defer_trigger → defer com gatilho no `*-impl.md` ou descarte — **não** polua a fila. -- **Cloud:** PR via `ManagePullRequest` com `draft: false`; Prep Cloud no - Passo 0. +- **Cloud:** PR via `ManagePullRequest` com `draft: false` no **GitHub** + (repo/PR agora no GitHub; o tracker de Issues segue no Forgejo — OPS71); + Prep Cloud no Passo 0. ## Resumo final diff --git a/.agents/skills/capture-review-debts/SKILL.md b/.agents/skills/capture-review-debts/SKILL.md index 7362bf54..7ddf8d4a 100644 --- a/.agents/skills/capture-review-debts/SKILL.md +++ b/.agents/skills/capture-review-debts/SKILL.md @@ -144,7 +144,7 @@ Para cada lote com destino **registrar** ou **absorver**: Não implemente as fases aqui. Execução é via `work-issue` / `agent-work-issue`, só se o usuário (ou o pipeline autônomo) seguir. -**Próximo no fluxo de entrega:** após a triage (confirmada ou modo autônomo), o fechamento segue o Passo 6 de `agent-work-issue` / `work-issue` (`pnpm push` → PR `--base main` com `Closes #N` → auto-merge → CI). +**Próximo no fluxo de entrega:** após a triage (confirmada ou modo autônomo), o fechamento segue o Passo 6 de `agent-work-issue` / `work-issue` (`pnpm push` → PR no GitHub `--base main` com `Closes #N` → auto-merge nativo → CI). ## Modo autônomo (`work-issue` pós-plano / `agent-work-issue` / pool) diff --git a/.agents/skills/work-issue/SKILL.md b/.agents/skills/work-issue/SKILL.md index 01941312..36717b97 100644 --- a/.agents/skills/work-issue/SKILL.md +++ b/.agents/skills/work-issue/SKILL.md @@ -36,7 +36,7 @@ pular a pausa do impl plan; Draft / sem auto-merge. - [ ] 1. Contexto: Issue do prompt/`$ARGUMENTS` da sessão (ausente → UMA pergunta com validação; nunca claim) - [ ] 2. Abrir o plano de intenção do body da Issue (`Plano: docs/plans/...`; sem link → body é spec) - [ ] 3. Plan mode → escrever `docs/plans/<slug>-impl.md` → **PARAR e confirmar com o humano** -- [ ] 4. Após “ok”: Passo 4 (execução → /simplify → débitos autônomos → PR, via `execution-pipeline.md`) +- [ ] 4. Após “ok”: Passo 4 (execução → e2e local afetado → /simplify → débitos autônomos → PR, via `execution-pipeline.md`) ``` ## Passo 1 — Contexto da sessão @@ -96,6 +96,12 @@ nunca invente produto novo. fechar em main), com os deltas do ator humano: - **Branch:** `<Code>-<slug>` do worktree — nunca crie branch nova na sessão. +- **E2E local afetado (OPS72, discricionário):** antes do push, rode + localmente os e2e que você **criou** + os da **mesma superfície afetada** — + você decide quais (`pnpm test:e2e:affected` ou specs diretas). Não está no + `gate:push`; ver a seção "E2E local afetado" do `execution-pipeline.md` + (incl. a limitação da #72: `--no-deps` + projetos paralelos → `--workers=1` + ou a cadeia padrão de projetos). - **UI:** shape → craft → critique → polish. - **`capture-review-debts`:** **autônomo** — colha, dedupe, pontue e **decida você mesmo o destino** (registrar/absorver/deferir/descartar) pela triage diff --git a/.agents/skills/work-issue/execution-pipeline.md b/.agents/skills/work-issue/execution-pipeline.md index ce04e538..899ecd2c 100644 --- a/.agents/skills/work-issue/execution-pipeline.md +++ b/.agents/skills/work-issue/execution-pipeline.md @@ -12,12 +12,42 @@ Ordem: testes de domínio. Invariantes do engineering-brief. 2. **UI** — se Impeccable B/C/D: shape → craft → critique → polish. Tokens `data-theme='campaign'`; shells existentes. -3. **Gates** — `pnpm gate:fast` na iteração; entrega com `pnpm push` (não +3. **E2E local afetado (OPS72, discricionário)** — antes do push, rode + localmente os e2e que você **criou** + os da **mesma superfície afetada** + (você decide quais). Ver §E2E local afetado. +4. **Gates** — `pnpm gate:fast` na iteração; entrega com `pnpm push` (não `git push` nu). Tracer bullet cedo se o item for grande. Inclua o `*-impl.md` no commit da entrega. +## E2E local afetado (OPS72) + +Política de e2e: **local = discricionário, CI/PR = blast radius, main = full +antes do deploy**. + +- **O que rodar:** os specs e2e que o diff **criou** (novos `.e2e.spec.ts`) + + os da mesma superfície trabalhada (o `E2E_AFFECTED_MANIFEST` mapeia + `src/` → specs; a mecânica do CI é a mesma). **É discricionário: você + decide quais rodar** — não é um gate mecânico e não está no `gate:push`. +- **Ferramenta:** `pnpm test:e2e:affected` (espelho local do classifier: + modo `none`/`selected`/`full` conforme o diff; roda `migrate` + + `db:seed:minimal` antes). Alternativa para specs diretas: + `pnpm test:e2e --no-deps -- tests/e2e/<spec>.e2e.spec.ts` (ou + `--no-deps --project=<família>`). +- **CI/PR:** roda só o **blast radius** detectado (`ci-scope.mjs`, modo + `selected`) — nunca `full`. Diff high-risk (schema/lockfile/harness) + classifica `full` → o PR não roda e2e: o full fica para o `verify` do + deploy manual (antes de publicar) e, nesse mesmo diff, o espelho local + roda full. +- **Limitação da #72 (S3-FOLLOWUP):** e2e local com `--no-deps` + projetos + paralelos (`--project=a --project=b`) colide no `seedTestUser` (delete + + create concorrentes nos `beforeAll` → `ValidationError: email` e falhas + fantasma). Use `--workers=1` ou a cadeia padrão de projetos. Só afeta o + local: no CI/prod mode a cadeia de deps dos projects é descartada (as + famílias rodam em paralelo) e os runs selecionados filtram por arquivo — + quase sempre uma família — então o seed não colide. + ## /simplify + débitos 1. Rode o comando `/simplify` completo (3 reviewers paralelos via Task, @@ -34,10 +64,15 @@ entrega. e rode `pnpm changelog:build` para regenerar `docs/CHANGELOG-AGENTS.md` (insert-only: entradas históricas nunca mudam; o diff será só a entrada nova + o agregado). Rode `pnpm changelog:check` para confirmar. -3. **`pnpm push -u origin HEAD`** -4. PR via API/MCP (ou `ManagePullRequest` no Cloud) — **Ready** (nunca draft), base `main`, com `Closes #<N>` -5. O safety net `agent-pr-ready-automerge.yml` espera o rollup `CI (PR) / checks` e mergea por rebase — nada a armar -6. CI flipa `done`/`in-prod` no merge. Comente na Issue o desfecho em uma linha. +3. **`pnpm push -u origin HEAD`** — origin é o **GitHub** (OPS71; o tracker de + Issues continua no Forgejo por API). +4. PR no **GitHub** via `GITHUB_TOKEN=<PAT> node scripts/github-pr.mjs --head <branch> --title "<id> — <título>" --body-file <arquivo>` — **Ready** (nunca draft; o script não tem flag draft), base `main`, body com `Closes #<N>` (ou `Related #N` em plans-only). Em Cursor Cloud: `ManagePullRequest` com `draft: false`. +5. O safety net `agent-pr-ready-automerge.yml` arma o **auto-merge nativo do + GitHub** (`enablePullRequestAutoMerge`, rebase) — o servidor mergea quando + o required check `CI (PR) / checks` fica verde; nada a armar. +6. No merge, o workflow `issue-done-on-main-merge.yml` flipa `done`/`in-prod` + **no Forgejo** (lê o body do PR via API do GitHub, escreve no tracker por + `FORGEJO_API_TOKEN`). Comente na Issue o desfecho em uma linha. ## Deltas por ator diff --git a/.forgejo/workflows/agent-pr-ready-automerge.yml b/.forgejo/workflows/agent-pr-ready-automerge.yml deleted file mode 100644 index acb6e38a..00000000 --- a/.forgejo/workflows/agent-pr-ready-automerge.yml +++ /dev/null @@ -1,57 +0,0 @@ -# Safety net: every same-repo PR to main must be Ready + auto-merge. -# Covers Cloud ManagePullRequest defaulting to draft, agents that forget to -# arm the merge, and human/worktree PRs (the OPS52 incident was a human PR -# whose merge failed silently). Draft PRs from non-`cursor/*` branches are -# never touched — the draft flag is the actor's veto (the CLI skips them). -# Fork PRs are excluded (the repo is public; a merged fork PR would deploy). -# -# Forgejo port (OPS50): Forgejo's API has no auto-merge scheduling, so the -# workflow runs scripts/forgejo-pr-automerge.mjs (plain Node): marks draft → -# ready, waits for the commit statuses (the branch-protection `checks` gate), -# then merges by rebase. Same semantics as `gh pr merge --auto --rebase`, -# implemented as poll + merge. GitHub's cancel-workflow-on-failure action was -# dropped (no GITHUB_RUN_ID plumbing on Forgejo). -# -# OPS61: the job injects FORGEJO_API_TOKEN. The merge POST performs a git push -# of the rebased branch to main; Forgejo's pre-receive hook rejects that push -# when the caller's user has no repo write — and the native GITHUB_TOKEN acts -# as the Actions system user (no write, and the same 403 that broke issue -# label flips). Every pre-OPS61 "auto-merge" that ended success had actually -# found the PR already merged manually; the CLI's own merge attempts died on -# 409 "User permission denied for writing". -name: PR Ready + auto-merge - -on: - pull_request: - types: [opened, reopened, synchronize, ready_for_review, converted_to_draft] - branches: [main] - -jobs: - ready-automerge: - # Same-repo only. This MUST stay in the `if:` (pre-checkout): for a fork - # PR, actions/checkout checks out the fork's head, so a same-repo guard - # inside the CLI would already be running the fork's own script — RCE with - # the Actions token. Fork PRs are excluded here because the repo is public - # and a merged fork PR would deploy attacker code to production. The draft - # policy lives in the CLI (skips non-`cursor/*` drafts) because there the - # PR state is re-read from the API, not from webhook payload fields. - if: >- - github.event.pull_request.state == 'open' && - github.event.pull_request.head.repo.full_name == github.repository - runs-on: ubuntu-latest - # OPS61: the CLI now waits for the full CI (PR) cascade (the `checks` - # rollup posts only after every ci-pr job, incl. e2e shards) — 55 min keeps - # 10 min of slack over the CLI's 45 min timeout. - timeout-minutes: 55 - env: - FORGEJO_API_TOKEN: ${{ secrets.FORGEJO_API_TOKEN }} - steps: - - uses: actions/checkout@v5 - # Script is plain Node (stdlib) — no pnpm. Disable package-manager-cache: - # setup-node@v5 defaults to true and looks for pnpm before pnpm/action-setup. - - uses: actions/setup-node@v5 - with: - node-version: 24 - package-manager-cache: false - - name: Mark Ready and merge when checks pass - run: node scripts/forgejo-pr-automerge.mjs --pr "${{ github.event.pull_request.number }}" diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml deleted file mode 100644 index 266d2d9e..00000000 --- a/.forgejo/workflows/ci.yml +++ /dev/null @@ -1,229 +0,0 @@ -# Post-merge verifier on `main`, in a fixed 30-minute window (OPS65): the -# workflow no longer fires on every push. The `schedule` trigger polls every -# 30 min and a cheap gate (`gate`, runs-on host, no pnpm) classifies whether -# anything that would enter the docker build context changed since the -# revision running on the homeserver (the `org.opencontainers.image.revision` -# label of the running container, read over ssh). -# - Deployed == HEAD → suite + deploy skip; the run stays green. -# - Anything else (production change, unknown deployed state, dispatch) → -# full suite; deploy in the same window. -# - `workflow_dispatch` always runs the full pipeline (explicit operator -# intent); the deploy is idempotent via the "already deployed" guard in -# scripts/deploy-homeserver.sh. -# -# Fail-open: an unreadable/unknown deployed revision makes the gate report -# production=true, so the suite ALWAYS verifies main and a quiet day only -# skips when we KNOW deployed == HEAD. A gate that cannot classify at all -# (git/.dockerignore error) exits non-zero and turns the run RED — never a -# silent green blackout. The deploy job only runs when the gate succeeded -# AND production changed AND checks are green — never deploy blind. -# -# When the suite is green, the `deploy` job publishes teqo-1313 on the -# homeserver (OPS53): it streams scripts/deploy-homeserver.sh to the homeserver -# over SSH; the remote script builds the standalone image at the pushed SHA, -# pushes it to the local registry (localhost:5000), applies pending migrations -# through the compose maintenance service and rolls the container out. -# -# SINGLE sequential job (OPS62, X1): one container, one checkout, one pnpm -# install, one runner slot. Fail-fast is STRUCTURAL — a failed step skips -# every step after it, the job fails, the run fails and the slot is released -# immediately. No cancel-run API (Forgejo 9.0.3 has none), no watchdog job, -# no upgrades. `deploy` (needs: [checks]) never starts on a red run. -# -# Forgejo port (OPS50): Postgres services live on the job network and are -# reached by NAME (postgres-int / postgres-build on 5432) — no host publish, -# so concurrent runs never collide on host ports (OPS62 X1). No -# `permissions:` (Forgejo issues one native GITHUB_TOKEN) and no -# `concurrency:` (unsupported). `gh workflow run` requeues became -# `node scripts/forgejo-dispatch.mjs`. - -name: CI - -on: - schedule: - - cron: '*/30 * * * *' - workflow_dispatch: - -env: - PAYLOAD_SECRET: test-only-secret-not-used-in-production - NEXT_PUBLIC_SITE_URL: http://localhost:3000 - -jobs: - gate: - name: gate production change - # Host runner: needs `ssh homeserver` (same environment as the deploy - # job — the key lives on the workstation host) and a full clone to diff - # <deployed-rev>..HEAD. Main + dispatch only: a branch dispatch must not - # run that branch's code on the host — the skipped gate makes the suite - # run fail-open on ubuntu containers instead (test-only secrets). - if: github.ref == 'refs/heads/main' - runs-on: host - timeout-minutes: 10 - outputs: - production: ${{ steps.classify.outputs.production }} - steps: - - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - uses: actions/setup-node@v5 - with: - node-version: 24 - package-manager-cache: false - - name: Read deployed revision and classify production change - id: classify - run: | - set -euo pipefail - # Truth = the RUNNING container's revision label (the compose file - # can lie after a failed rollback — it is swapped before the - # rollout). Empty/malformed = unknown deployed state → the - # classifier reports production=true (full run, green). - deployed_sha="$(ssh homeserver "docker inspect -f '{{index .Config.Labels \"org.opencontainers.image.revision\"}}' teqo-1313 2>/dev/null" || true)" - if ! printf '%s' "$deployed_sha" | grep -Eq '^[0-9a-f]{40}$'; then - deployed_sha="" - fi - echo "deployed_sha: ${deployed_sha:-<none>}" - out="$(node scripts/ci-classify-production.mjs --deployed "$deployed_sha")" - printf '%s\n' "$out" - echo "production=$(printf '%s' "$out" | node -pe 'JSON.parse(require("fs").readFileSync(0,"utf8")).production')" >> "$GITHUB_OUTPUT" - - checks: - runs-on: ubuntu-latest - timeout-minutes: 50 - needs: [gate] - # Fail-open: an errored gate never skips the verifier; only a gate that - # succeeded AND found no production change skips the suite. - if: needs.gate.result != 'success' || needs.gate.outputs.production == 'true' - services: - # Services are reached by NAME on the job network (no host publish — - # OPS62 X1): concurrent runs never collide on host ports, which the - # single long-lived job would hold for the whole run. Two isolated - # services — int and build+e2e — keep e2e from inheriting int fixture - # residue. Forgejo 9 does not expand expressions in `services.ports`, - # so a per-run port scheme was dropped in favour of the job-network DNS. - postgres-int: - image: postgres:17-alpine - env: - POSTGRES_USER: teqo - POSTGRES_PASSWORD: teqo - POSTGRES_DB: teqo_test - options: >- - --health-cmd "pg_isready -U teqo" - --health-interval 5s - --health-timeout 5s - --health-retries 12 - postgres-build: - image: postgres:17-alpine - env: - POSTGRES_USER: teqo - POSTGRES_PASSWORD: teqo - POSTGRES_DB: teqo_test - options: >- - --health-cmd "pg_isready -U teqo" - --health-interval 5s - --health-timeout 5s - --health-retries 12 - steps: - - uses: actions/checkout@v5 - - uses: pnpm/action-setup@v6 - - uses: actions/setup-node@v5 - with: - node-version: 24 - cache: pnpm - - run: pnpm install --frozen-lockfile - - - name: Check test file locations - run: node scripts/check-test-locations.mjs - - name: Lint - run: pnpm lint - - name: Format check - run: pnpm format:check - - name: Typecheck - run: pnpm typecheck - - name: Knip - run: pnpm knip - - name: Check cycles - run: pnpm check:cycles - - name: Unit tests (full suite) - run: pnpm test:unit - - - name: Prepare database URL (int service) - run: | - set -euo pipefail - echo "DATABASE_URL=postgresql://teqo:teqo@postgres-int:5432/teqo_test" >> "$GITHUB_ENV" - # vitest re-loads .env.test over the process env, so the service URL - # must win through the .env.test.local layer (same mechanism - # `pnpm worktree next` uses). OPS50. - echo "DATABASE_URL=postgresql://teqo:teqo@postgres-int:5432/teqo_test" > .env.test.local - - name: Migrate (int database) - run: pnpm migrate - - name: Seed minimal (int database) - run: pnpm db:seed:minimal - - name: Integration tests (full suite) - env: - # Self-hosted runner has 8 CPUs; vitest forks ≈ CPUs and each fork - # holds a Payload pool (max 10) → overflows the service Postgres' 100 - # clients ("too many clients already"). 4 forks stay well under. The - # unit step above stays CPU-relative (no DB). - VITEST_MAX_WORKERS: 4 - run: pnpm test:int - - - name: Prepare database URL (build/e2e service) - run: | - set -euo pipefail - echo "DATABASE_URL=postgresql://teqo:teqo@postgres-build:5432/teqo_test" >> "$GITHUB_ENV" - echo "DATABASE_URL=postgresql://teqo:teqo@postgres-build:5432/teqo_test" > .env.test.local - - name: Migrate (build/e2e database) - run: pnpm migrate - - name: Seed minimal (build/e2e database) - run: pnpm db:seed:minimal - - - name: Build (against the service database) - run: pnpm build - - - name: Cache Playwright browsers - uses: actions/cache@v4 - with: - path: ~/.cache/ms-playwright - key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }} - restore-keys: | - playwright-${{ runner.os }}- - - name: Install Playwright chromium - run: pnpm exec playwright install --with-deps chromium - - name: Build for e2e - # Same dir the Playwright webServer serves (playwright.config.ts - # falls back to '.next-e2e'; gate-ci.mjs builds there too). - run: pnpm build - env: - NEXT_DIST_DIR: .next-e2e - - name: E2E tests (full suite, single job, 4 workers) - # Explicit CI workers; the local default stays 2 (playwright.config.ts). - # 4 workers in one process replace the former 2 shards × 2 workers. - # No `--` separator: pnpm would forward it verbatim to playwright and - # the flag would be parsed as a file pattern ("No tests found"). - env: - PLAYWRIGHT_WORKERS: 4 - run: pnpm test:e2e - - deploy: - name: deploy homeserver (teqo-1313) - # OPS53. Runs directly on the runner HOST (not a job container): the - # deploy needs the workstation's SSH key to reach the homeserver - # (`ssh homeserver`) and streams the whole remote build/migrate/rollout - # to this job's logs. Deliberately wired ONLY here — main window + - # workflow_dispatch — never in ci-pr.yml: a host job is code execution as - # `fsolla` on the workstation, and main only ever moves through a merged - # PR whose full suite was already green. - # OPS65: double-gated — the verifier must be green AND the gate must have - # found a production change. The explicit `if` prevents both "deploy - # blind on a failed gate" and "deploy with a red suite" (a bare output - # condition would run despite failed needs). - needs: [checks, gate] - if: needs.checks.result == 'success' && needs.gate.outputs.production == 'true' - runs-on: host - timeout-minutes: 60 - steps: - - uses: actions/checkout@v5 - - name: Deploy teqo-1313 on the homeserver - run: | - set -euo pipefail - ssh homeserver "bash -s -- $GITHUB_SHA" < scripts/deploy-homeserver.sh diff --git a/.forgejo/workflows/issue-done-on-main-merge.yml b/.forgejo/workflows/issue-done-on-main-merge.yml deleted file mode 100644 index 52bbe6f2..00000000 --- a/.forgejo/workflows/issue-done-on-main-merge.yml +++ /dev/null @@ -1,30 +0,0 @@ -# Main merge flips claimed issues to done + in-prod. -# Forgejo port (OPS50): the gh loop became scripts/forgejo-issue-transition.mjs -# (plain Node, reads the PR body from the API itself). -# OPS61: the job injects FORGEJO_API_TOKEN — the native GITHUB_TOKEN 403s on -# issue label writes, and the flip was silently skipped (script logged the 403 -# and the job still ended success). The fork guard is the same class as -# agent-pr-ready-automerge.yml: on a merged fork PR the checkout pulls the -# fork's own script, which would run with the repo PAT (RCE). -name: Main merge flips claimed issues to done + in-prod - -on: - pull_request: - types: [closed] - -jobs: - issue-done: - if: github.event.pull_request.base.ref == 'main' && github.event.pull_request.merged == true && github.event.pull_request.head.repo.full_name == github.repository - runs-on: ubuntu-latest - env: - FORGEJO_API_TOKEN: ${{ secrets.FORGEJO_API_TOKEN }} - steps: - - uses: actions/checkout@v5 - # Script is plain Node (stdlib) — no pnpm. Disable package-manager-cache: - # setup-node@v5 defaults to true and looks for pnpm before pnpm/action-setup. - - uses: actions/setup-node@v5 - with: - node-version: 24 - package-manager-cache: false - - name: Flip Closes/Fixes #N issues to done and in-prod - run: node scripts/forgejo-issue-transition.mjs --pr "${{ github.event.pull_request.number }}" diff --git a/.forgejo/workflows/plan-issue-ready-on-main-merge.yml b/.forgejo/workflows/plan-issue-ready-on-main-merge.yml deleted file mode 100644 index 88b7c354..00000000 --- a/.forgejo/workflows/plan-issue-ready-on-main-merge.yml +++ /dev/null @@ -1,44 +0,0 @@ -# Safety net OPS18: when a PR merges to main with `Related #N`, promote Issues -# that are still awaiting their intention plan (`blocked` + docs/plans/ link) -# to `ready`. Idempotent with `pnpm agent:ready` (agent happy path). -# -# Does NOT run on open PRs — only after merge — so the claim race stays closed. -# Job is gated on Related # in the body so non-plan merges skip checkout/Node. -# Forgejo port (OPS50): script reads the PR via the Forgejo API (plain Node). -# OPS61: the job injects FORGEJO_API_TOKEN — the native GITHUB_TOKEN 403s on -# issue label writes (same silent-skip failure as issue-done-on-main-merge). -# Fork guard: on a merged fork PR the checkout pulls the fork's own script, -# which would run with the repo PAT (RCE — same class as -# agent-pr-ready-automerge.yml). -name: Promote plan-issue Related on main merge - -on: - pull_request: - types: [closed] - branches: [main] - -jobs: - plan-issue-ready: - if: >- - github.event.pull_request.merged == true && - github.event.pull_request.head.repo.full_name == github.repository && - ( - contains(github.event.pull_request.body, 'Related #') || - contains(github.event.pull_request.body, 'related #') || - contains(github.event.pull_request.body, 'RELATED #') - ) - runs-on: ubuntu-latest - timeout-minutes: 5 - env: - FORGEJO_API_TOKEN: ${{ secrets.FORGEJO_API_TOKEN }} - steps: - - uses: actions/checkout@v5 - # Script is plain Node (stdlib) — no pnpm. Disable - # package-manager-cache: setup-node@v5 defaults to true and looks for - # pnpm (packageManager in package.json) before pnpm/action-setup. - - uses: actions/setup-node@v5 - with: - node-version: 24 - package-manager-cache: false - - name: Promote Related #N awaiting plan → ready - run: node scripts/agent-promote-related-on-merge.mjs --pr "${{ github.event.pull_request.number }}" diff --git a/.github/workflows/agent-pr-ready-automerge.yml b/.github/workflows/agent-pr-ready-automerge.yml new file mode 100644 index 00000000..82470666 --- /dev/null +++ b/.github/workflows/agent-pr-ready-automerge.yml @@ -0,0 +1,64 @@ +# Safety net (OPS71 — GitHub): every same-repo PR to main must be Ready + +# auto-merge armed. Covers Cloud ManagePullRequest defaulting to draft, +# agents that forget to arm the merge, and human/worktree PRs (the OPS52 +# incident was a human PR whose merge failed silently). Draft PRs from +# non-`cursor/*` branches are never touched — the draft flag is the actor's +# veto (the CLI skips them). Fork PRs are excluded (the repo is public; a +# merged fork PR would deploy) — same RCE-guard class as the other workflows. +# +# GitHub's NATIVE auto-merge replaces the Forgejo-era poll CLI (removed in +# OPS71 Fase 2): `enablePullRequestAutoMerge` (GraphQL, REBASE — +# the repo's canonical merge style) is server-side — GitHub only merges when +# every required check (the `CI (PR) / checks` rollup of ci-pr.yml, OPS62) is +# green and the PR is mergeable. The OPS64 "rollup can lie" defense is +# structural here: a single sequential job posts one honest check-run and the +# server enforces it — nothing to poll, nothing to trust. +# +# Permissions: contents+pull-requests write — enough for PATCH draft:false +# and the GraphQL auto-merge mutation with the built-in GITHUB_TOKEN. +name: PR Ready + auto-merge + +on: + pull_request: + types: [opened, reopened, synchronize, ready_for_review, converted_to_draft] + branches: [main] + +permissions: + contents: write + pull-requests: write + +jobs: + ready-automerge: + # Same-repo only. This MUST stay in the `if:` (pre-checkout): for a fork + # PR, actions/checkout checks out the fork's head, so a same-repo guard + # inside the CLI would already be running the fork's own script — RCE + # with the Actions token. The draft policy lives in the CLI (skips + # non-`cursor/*` drafts) because there the PR state is re-read from the + # API, not from webhook payload fields. + if: >- + github.event.pull_request.state == 'open' && + github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v5 + # Script is plain Node (stdlib) — no pnpm. Disable package-manager-cache: + # setup-node@v5 defaults to true and looks for pnpm before pnpm/action-setup. + - uses: actions/setup-node@v5 + with: + node-version: 24 + package-manager-cache: false + - name: Mark Ready and arm auto-merge + env: + # Explicit `github.token` — the built-in GITHUB_TOKEN env is not + # guaranteed to be injected on every event path; passing it via the + # documented context is the robust form. + GITHUB_TOKEN: ${{ github.token }} + # OPS71-FLIP: arming with the built-in GITHUB_TOKEN makes the merge + # happen as github-actions[bot], and GitHub's anti-recursion never + # creates workflow runs for events triggered by GITHUB_TOKEN actions + # — the post-merge `closed` flips would silently never run (PR #746). + # AUTOMERGE_PAT (real user PAT) attributes the merge to a user, so + # the `closed` event creates runs again. Fail-closed in the CLI. + AUTOMERGE_PAT: ${{ secrets.AUTOMERGE_PAT }} + run: node scripts/github-pr-automerge.mjs --pr "${{ github.event.pull_request.number }}" diff --git a/.forgejo/workflows/archive-cursor-agent.yml b/.github/workflows/archive-cursor-agent.yml similarity index 98% rename from .forgejo/workflows/archive-cursor-agent.yml rename to .github/workflows/archive-cursor-agent.yml index 54856ef2..614c5708 100644 --- a/.forgejo/workflows/archive-cursor-agent.yml +++ b/.github/workflows/archive-cursor-agent.yml @@ -11,6 +11,8 @@ on: description: Cursor run id (run-…) optional cancel first required: false type: string +permissions: + contents: read jobs: archive: runs-on: ubuntu-latest diff --git a/.forgejo/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml similarity index 62% rename from .forgejo/workflows/ci-pr.yml rename to .github/workflows/ci-pr.yml index 1035650f..33299df5 100644 --- a/.forgejo/workflows/ci-pr.yml +++ b/.github/workflows/ci-pr.yml @@ -1,53 +1,71 @@ -# PR gate: feature → main. SINGLE sequential job (OPS62, X1): one container, -# one checkout, one pnpm install, one runner slot. Fail-fast is STRUCTURAL — -# a failed step skips every step after it, the job fails, the run fails and -# the slot is released immediately. No cancel-run API (Forgejo 9.0.3 has -# none), no watchdog job, no upgrades: with one job there are no siblings to -# cancel. Steps keep their own logs, so diagnosis is intact after a failure. -# The job id `checks` preserves the `CI (PR) / checks` status context the -# automerger waitForChecks and branch protection rely on (OPS61). +# PR gate: feature → main (OPS71 — GitHub Actions). SINGLE sequential job +# (OPS62, X1): one container, one checkout, one pnpm install, one runner slot. +# Fail-fast is STRUCTURAL — a failed step skips every step after it, the job +# fails and the run fails. GitHub-native `concurrency` cancels the in-flight +# run when the PR gets a new push (the Forgejo era had no cancel API; here it +# is free and matches the OPS62 "one slot per PR" spirit). +# +# The job id `checks` preserves the `CI (PR) / checks` check-run that the +# branch protection rule and the native auto-merge rely on (OPS71): GitHub +# names it <workflow name> / <job name> — keep both literals frozen. GitHub's +# auto-merge is server-side: it only merges with every required check green, +# so the OPS64 "rollup can lie" defense is structural here (single job posts +# one honest check-run; no matrix, no shards). # # Job-level skips come from the `scope` classifier (scripts/ci-scope.mjs) run -# as a step; its outputs drive `if:` on the heavy steps (same modes as the -# former per-job cascade: none/changed/full). +# as a step; its outputs drive `if:` on the heavy steps (code/build/test: +# none/changed/full; e2e: none/selected/full). e2e policy (OPS72): the PR +# runs ONLY the blast radius — the specs the classifier detected as touched +# (mode `selected`), never `full`. A high-risk diff (schema/lockfile/test +# harness) classifies as `full` → the PR simply runs no e2e: the local agent +# mirror (`pnpm test:e2e:affected`) runs full in that same case, and the +# deploy `verify` job runs the full suite before publishing. # # Schema migrations are NOT serialized across PRs (migration-lock removed # 2026-08-12): a stale chain is caught by `pnpm migrate` + `test:int` on every -# PR and on main (ci.yml), incl. campaignMigrationReconciliation — the check's -# hard fail deadlocked every open schema PR the moment two coexisted. +# PR, incl. campaignMigrationReconciliation. # -# Forgejo port (OPS50): Postgres services live on the job network and are -# reached by NAME (postgres-int / postgres-build on 5432) — no host publish, -# so concurrent runs never collide on host ports (OPS62 X1). Two isolated -# services keep e2e from inheriting int fixture residue. +# Fork-PRs are excluded at job level (`if:` same-repo): the repo is public and +# a fork PR would burn hosted minutes running arbitrary code. Same guard class +# as the RCE guard on the post-merge workflows. +# +# Postgres services (OPS71 — GitHub hosted): jobs run directly on the runner +# VM (NOT inside the job-container network of the Forgejo era), so services +# are reached at localhost on PUBLISHED host ports. Each hosted run is its own +# VM, so fixed ports never collide across runs. Two isolated services — int +# on 5432 and build+e2e on 5433 — keep e2e from inheriting int fixture +# residue. name: CI (PR) on: pull_request: branches: [main] +permissions: + contents: read + +concurrency: + group: ci-pr-${{ github.head_ref }} + cancel-in-progress: true + env: PAYLOAD_SECRET: test-only-secret-not-used-in-production NEXT_PUBLIC_SITE_URL: http://localhost:3000 jobs: checks: + if: github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest timeout-minutes: 50 services: - # Services are reached by NAME on the job network (no host publish — - # OPS62 X1): concurrent runs never collide on host ports, which the - # single long-lived job would hold for the whole run. Two isolated - # services — int and build+e2e — keep e2e from inheriting int fixture - # residue (same isolation the former jobs had). Forgejo 9 does not - # expand expressions in `services.ports`, so a per-run port scheme was - # dropped in favour of the job-network DNS. postgres-int: image: postgres:17-alpine env: POSTGRES_USER: teqo POSTGRES_PASSWORD: teqo POSTGRES_DB: teqo_test + ports: + - 5432:5432 options: >- --health-cmd "pg_isready -U teqo" --health-interval 5s @@ -59,6 +77,8 @@ jobs: POSTGRES_USER: teqo POSTGRES_PASSWORD: teqo POSTGRES_DB: teqo_test + ports: + - 5433:5432 options: >- --health-cmd "pg_isready -U teqo" --health-interval 5s @@ -133,11 +153,11 @@ jobs: - name: Prepare database URL (int service) run: | set -euo pipefail - echo "DATABASE_URL=postgresql://teqo:teqo@postgres-int:5432/teqo_test" >> "$GITHUB_ENV" + echo "DATABASE_URL=postgresql://teqo:teqo@localhost:5432/teqo_test" >> "$GITHUB_ENV" # vitest re-loads .env.test over the process env, so the service URL # must win through the .env.test.local layer (same mechanism - # `pnpm worktree next` uses). OPS50. - echo "DATABASE_URL=postgresql://teqo:teqo@postgres-int:5432/teqo_test" > .env.test.local + # `pnpm worktree next` uses). + echo "DATABASE_URL=postgresql://teqo:teqo@localhost:5432/teqo_test" > .env.test.local - name: Migrate (int database) run: pnpm migrate - name: Seed minimal (int database) @@ -146,10 +166,10 @@ jobs: - name: Integration tests (full suite) if: steps.scope.outputs.test_mode == 'full' env: - # Self-hosted runner has 8 CPUs; vitest forks ≈ CPUs and each fork - # holds a Payload pool (max 10) → overflows the service Postgres' 100 - # clients ("too many clients already", CI run 180). 4 forks stay - # well under. The unit step above stays CPU-relative (no DB). + # vitest forks ≈ CPUs and each fork holds a Payload pool (max 10) → + # overflows the service Postgres' 100 clients ("too many clients + # already"). 4 forks stay well under. The unit step above stays + # CPU-relative (no DB). VITEST_MAX_WORKERS: 4 run: pnpm test:int - name: Integration tests (changed since PR base) @@ -159,16 +179,16 @@ jobs: run: pnpm test:int -- --changed origin/${{ github.base_ref }} --passWithNoTests - name: Prepare database URL (build/e2e service) - if: steps.scope.outputs.build_mode != 'none' || steps.scope.outputs.e2e_mode != 'none' + if: steps.scope.outputs.build_mode != 'none' || steps.scope.outputs.e2e_mode == 'selected' run: | set -euo pipefail - echo "DATABASE_URL=postgresql://teqo:teqo@postgres-build:5432/teqo_test" >> "$GITHUB_ENV" - echo "DATABASE_URL=postgresql://teqo:teqo@postgres-build:5432/teqo_test" > .env.test.local + echo "DATABASE_URL=postgresql://teqo:teqo@localhost:5433/teqo_test" >> "$GITHUB_ENV" + echo "DATABASE_URL=postgresql://teqo:teqo@localhost:5433/teqo_test" > .env.test.local - name: Migrate (build/e2e database) - if: steps.scope.outputs.build_mode != 'none' || steps.scope.outputs.e2e_mode != 'none' + if: steps.scope.outputs.build_mode != 'none' || steps.scope.outputs.e2e_mode == 'selected' run: pnpm migrate - name: Seed minimal (build/e2e database) - if: steps.scope.outputs.build_mode != 'none' || steps.scope.outputs.e2e_mode != 'none' + if: steps.scope.outputs.build_mode != 'none' || steps.scope.outputs.e2e_mode == 'selected' run: pnpm db:seed:minimal - name: Build (against the local service database) @@ -176,7 +196,7 @@ jobs: run: pnpm build - name: Cache Playwright browsers - if: steps.scope.outputs.e2e_mode != 'none' + if: steps.scope.outputs.e2e_mode == 'selected' uses: actions/cache@v4 with: path: ~/.cache/ms-playwright @@ -184,26 +204,23 @@ jobs: restore-keys: | playwright-${{ runner.os }}- - name: Install Playwright chromium - if: steps.scope.outputs.e2e_mode != 'none' + if: steps.scope.outputs.e2e_mode == 'selected' run: pnpm exec playwright install --with-deps chromium - name: Build for e2e # Selected mode needs the build too (the Playwright webServer serves - # .next-e2e; the fallback `.next-e2e` dir is what gate-ci.mjs builds). - if: steps.scope.outputs.e2e_mode != 'none' + # the `.next-e2e` production build). + if: steps.scope.outputs.e2e_mode == 'selected' run: pnpm build env: NEXT_DIST_DIR: .next-e2e - - name: E2E tests (full suite, single job, 4 workers) - if: steps.scope.outputs.e2e_mode == 'full' + - name: E2E tests (blast radius — manifest-selected specs) + if: steps.scope.outputs.e2e_mode == 'selected' + # OPS72: the PR never runs e2e full — mode `full` (high-risk diff) + # skips this step by design (local mirror + deploy verify cover it). # Explicit CI workers; the local default stays 2 (playwright.config.ts). # 4 workers in one process replace the former 2 shards × 2 workers. - # No `--` separator: pnpm would forward it verbatim to playwright and - # the flag would be parsed as a file pattern ("No tests found"). - env: - PLAYWRIGHT_WORKERS: 4 - run: pnpm test:e2e - - name: E2E tests (manifest-selected specs) - if: steps.scope.outputs.e2e_mode == 'selected' env: PLAYWRIGHT_WORKERS: 4 + # The `--` separates pnpm from the scope paths: pnpm consumes it and + # playwright receives the bare spec paths (no flag parsing drift). run: pnpm test:e2e -- ${{ steps.scope.outputs.e2e_specs }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..5463c715 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,170 @@ +# Manual production deploy (OPS71 — GitHub Actions). `workflow_dispatch` +# only: there is NO automatic main verifier anymore (ci.yml eliminated in the +# gate 2026-08-19) — publishing is a deliberate operator act, and this +# workflow runs the FULL suite (no scope skips, e2e included) before the +# deploy job touches the homeserver. +# +# TWO jobs, same workflow: +# verify — ubuntu-latest (hosted): full cascade (guards for PR contracts are +# left out — they gate PRs, not production). +# deploy — needs: [verify], runs-on [self-hosted, homeserver]: the GitHub +# self-hosted runner ON the homeserver executes +# scripts/deploy-homeserver.sh LOCALLY (no SSH; outbound-only +# runner, works behind the Cloudflare tunnel). The hosted job +# never reaches the homeserver; the self-hosted job never counts +# hosted minutes. +# +# The production build CANNOT leave the homeserver (migrate pre-build on the +# prod DB — OPS66 — and the compose network / local registry localhost:5000), +# which is exactly why the hosted job only verifies and the self-hosted job +# only builds + deploys. Deploy is idempotent ("already deployed" guard in +# deploy-homeserver.sh) and serialized by flock on the homeserver. +# +# The `deploy` job fetches the repo from github.com (public) — TEQO_REPO_URL +# in deploy-homeserver.sh now defaults there; the script's HEAD guard only +# deploys when the dispatched SHA is main's HEAD (a stale dispatch skips). + +name: Deploy (manual) + +on: + workflow_dispatch: + +permissions: + contents: read + +env: + PAYLOAD_SECRET: test-only-secret-not-used-in-production + NEXT_PUBLIC_SITE_URL: http://localhost:3000 + +jobs: + verify: + runs-on: ubuntu-latest + timeout-minutes: 50 + services: + # OPS71 — GitHub hosted: jobs run on the runner VM, so services are + # reached at localhost on PUBLISHED host ports (fixed — each run is its + # own VM). Two isolated services — int on 5432, build+e2e on 5433 — + # keep e2e from inheriting int fixture residue. + postgres-int: + image: postgres:17-alpine + env: + POSTGRES_USER: teqo + POSTGRES_PASSWORD: teqo + POSTGRES_DB: teqo_test + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U teqo" + --health-interval 5s + --health-timeout 5s + --health-retries 12 + postgres-build: + image: postgres:17-alpine + env: + POSTGRES_USER: teqo + POSTGRES_PASSWORD: teqo + POSTGRES_DB: teqo_test + ports: + - 5433:5432 + options: >- + --health-cmd "pg_isready -U teqo" + --health-interval 5s + --health-timeout 5s + --health-retries 12 + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + - uses: pnpm/action-setup@v6 + - uses: actions/setup-node@v5 + with: + node-version: 24 + cache: pnpm + - run: pnpm install --frozen-lockfile + + - name: Check test file locations + run: node scripts/check-test-locations.mjs + - name: Lint + run: pnpm lint + - name: Format check + run: pnpm format:check + - name: Typecheck + run: pnpm typecheck + - name: Knip + run: pnpm knip + - name: Check cycles + run: pnpm check:cycles + - name: Unit tests (full suite) + run: pnpm test:unit + + - name: Prepare database URL (int service) + run: | + set -euo pipefail + echo "DATABASE_URL=postgresql://teqo:teqo@localhost:5432/teqo_test" >> "$GITHUB_ENV" + # vitest re-loads .env.test over the process env, so the service URL + # must win through the .env.test.local layer (same mechanism + # `pnpm worktree next` uses). + echo "DATABASE_URL=postgresql://teqo:teqo@localhost:5432/teqo_test" > .env.test.local + - name: Migrate (int database) + run: pnpm migrate + - name: Seed minimal (int database) + run: pnpm db:seed:minimal + - name: Integration tests (full suite) + env: + # vitest forks ≈ CPUs and each fork holds a Payload pool (max 10) → + # overflows the service Postgres' 100 clients. 4 forks stay well + # under. The unit step above stays CPU-relative (no DB). + VITEST_MAX_WORKERS: 4 + run: pnpm test:int + + - name: Prepare database URL (build/e2e service) + run: | + set -euo pipefail + echo "DATABASE_URL=postgresql://teqo:teqo@localhost:5433/teqo_test" >> "$GITHUB_ENV" + echo "DATABASE_URL=postgresql://teqo:teqo@localhost:5433/teqo_test" > .env.test.local + - name: Migrate (build/e2e database) + run: pnpm migrate + - name: Seed minimal (build/e2e database) + run: pnpm db:seed:minimal + - name: Build (against the service database) + run: pnpm build + + - name: Cache Playwright browsers + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }} + restore-keys: | + playwright-${{ runner.os }}- + - name: Install Playwright chromium + run: pnpm exec playwright install --with-deps chromium + - name: Build for e2e + # Same dir the Playwright webServer serves (playwright.config.ts + # falls back to '.next-e2e'; gate-ci.mjs builds there too). + run: pnpm build + env: + NEXT_DIST_DIR: .next-e2e + - name: E2E tests (full suite, single job, 4 workers) + # Explicit CI workers; the local default stays 2 (playwright.config.ts). + # No `--` separator: pnpm would forward it verbatim to playwright and + # the flag would be parsed as a file pattern ("No tests found"). + env: + PLAYWRIGHT_WORKERS: 4 + run: pnpm test:e2e + + deploy: + name: deploy homeserver (teqo-1313) + # OPS53 + OPS71. Runs on the GitHub self-hosted runner ON the homeserver + # (labels self-hosted + homeserver): the script executes locally — no SSH + # hop, no workstation involvement. Deliberately wired ONLY here (manual + # dispatch), never in ci-pr.yml: a self-hosted job is code execution on + # the production machine, and only an explicit dispatch with the full + # verify suite green can reach it. + needs: [verify] + if: needs.verify.result == 'success' + runs-on: [self-hosted, homeserver] + timeout-minutes: 60 + steps: + - uses: actions/checkout@v5 + - name: Deploy teqo-1313 on the homeserver + run: bash scripts/deploy-homeserver.sh "$GITHUB_SHA" diff --git a/.github/workflows/issue-done-on-main-merge.yml b/.github/workflows/issue-done-on-main-merge.yml new file mode 100644 index 00000000..5a3ba77f --- /dev/null +++ b/.github/workflows/issue-done-on-main-merge.yml @@ -0,0 +1,80 @@ +# Main merge flips claimed issues to done + in-prod. +# OPS71: the PR lives on GitHub; the ISSUES stay on the Forgejo tracker. The +# job passes the PR body via PR_BODY (GitHub event payload) and +# scripts/forgejo-issue-transition.mjs flips labels/comments and closes the +# issue on the Forgejo API with the FORGEJO_API_TOKEN PAT (the built-in +# GITHUB_TOKEN cannot touch Forgejo issues — OPS61). The fork guard is the +# same class as agent-pr-ready-automerge.yml: on a merged fork PR the checkout +# pulls the fork's own script, which would run with the repo PAT (RCE). +# +# OPS71-FLIP: `workflow_dispatch` is the MANUAL recovery path. The `closed` +# event does NOT create a run when the merge was performed by +# github-actions[bot] (auto-merge armed with the built-in GITHUB_TOKEN — +# GitHub's anti-recursion; live finding PR #746). Dispatch with the PR number: +# the job fetches the PR via `gh` (same contract: merged + base main + +# same-repo) and runs the same flip. +name: Main merge flips claimed issues to done + in-prod + +on: + pull_request: + types: [closed] + workflow_dispatch: + inputs: + pr: + description: 'PR number (manual recovery when the closed event did not create a run)' + required: true + type: number + +permissions: + contents: read + pull-requests: read + +jobs: + issue-done: + if: >- + (github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'main' && github.event.pull_request.merged == true && github.event.pull_request.head.repo.full_name == github.repository) || + (github.event_name == 'workflow_dispatch' && inputs.pr != '') + runs-on: ubuntu-latest + env: + FORGEJO_API_TOKEN: ${{ secrets.FORGEJO_API_TOKEN }} + # Explicit base: in the GitHub Actions env, GITHUB_SERVER_URL points at + # github.com and forgejo-api.mjs would resolve the wrong base (410). + FORGEJO_API_URL: https://git.solla.dev/api/v1 + GH_TOKEN: ${{ github.token }} + steps: + - uses: actions/checkout@v5 + # Script is plain Node (stdlib) — no pnpm. Disable package-manager-cache: + # setup-node@v5 defaults to true and looks for pnpm before pnpm/action-setup. + - uses: actions/setup-node@v5 + with: + node-version: 24 + package-manager-cache: false + - name: Fetch PR metadata (manual recovery — bot auto-merges skip the closed event) + if: github.event_name == 'workflow_dispatch' + id: pr + run: | + PR="${{ inputs.pr }}" + STATE="$(gh pr view "$PR" --json state -q .state)" + if [ "$STATE" != "MERGED" ]; then + echo "PR #$PR não mergeada (state=$STATE) — skip" + exit 0 + fi + BASE="$(gh pr view "$PR" --json baseRefName -q .baseRefName)" + REPO="$(gh pr view "$PR" --json headRepository -q .headRepository.nameWithOwner)" + if [ "$BASE" != "main" ] || [ "$REPO" != "${{ github.repository }}" ]; then + echo "PR #$PR fora do contrato (base=$BASE repo=$REPO — precisa main + same-repo) — skip" + exit 0 + fi + BODY="$(gh pr view "$PR" --json body -q .body)" + { + echo "valid=true" + echo "pr_number=$PR" + echo "pr_body<<EOF" + echo "$BODY" + echo "EOF" + } >> "$GITHUB_OUTPUT" + - name: Flip Closes/Fixes #N issues to done and in-prod + if: steps.pr.outputs.valid == 'true' || github.event_name == 'pull_request' + env: + PR_BODY: ${{ steps.pr.outputs.pr_body || github.event.pull_request.body }} + run: node scripts/forgejo-issue-transition.mjs --pr "${{ steps.pr.outputs.pr_number || github.event.pull_request.number }}" diff --git a/.github/workflows/plan-issue-ready-on-main-merge.yml b/.github/workflows/plan-issue-ready-on-main-merge.yml new file mode 100644 index 00000000..2c45d59b --- /dev/null +++ b/.github/workflows/plan-issue-ready-on-main-merge.yml @@ -0,0 +1,88 @@ +# Safety net OPS18: when a PR merges to main with `Related #N`, promote Issues +# that are still awaiting their intention plan (`blocked` + docs/plans/ link) +# to `ready`. Idempotent with `pnpm agent:ready` (agent happy path). OPS71: +# the PR lives on GitHub, the issues on the Forgejo tracker — the job passes +# the PR body via PR_BODY and scripts/agent-promote-related-on-merge.mjs flips +# labels on the Forgejo API (FORGEJO_API_TOKEN; the built-in GITHUB_TOKEN +# cannot touch Forgejo issues — OPS61). +# +# Does NOT run on open PRs — only after merge — so the claim race stays closed. +# Job is gated on Related # in the body so non-plan merges skip checkout/Node. +# Fork guard: on a merged fork PR the checkout pulls the fork's own script, +# which would run with the repo PAT (RCE — same class as +# agent-pr-ready-automerge.yml). +# +# OPS71-FLIP: `workflow_dispatch` is the MANUAL recovery path — the `closed` +# event does NOT create a run when the merge was performed by +# github-actions[bot] (auto-merge armed with the built-in GITHUB_TOKEN; +# GitHub's anti-recursion, live finding PR #746). Dispatch with the PR number: +# the job fetches the PR via `gh` (same contract: merged + base main + +# same-repo) and runs the same promotion. +name: Promote plan-issue Related on main merge + +on: + pull_request: + types: [closed] + branches: [main] + workflow_dispatch: + inputs: + pr: + description: 'PR number (manual recovery when the closed event did not create a run)' + required: true + type: number + +permissions: + contents: read + pull-requests: read + +jobs: + plan-issue-ready: + if: >- + (github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.head.repo.full_name == github.repository && (contains(github.event.pull_request.body, 'Related #') || contains(github.event.pull_request.body, 'related #') || contains(github.event.pull_request.body, 'RELATED #'))) || + (github.event_name == 'workflow_dispatch' && inputs.pr != '') + runs-on: ubuntu-latest + timeout-minutes: 5 + env: + FORGEJO_API_TOKEN: ${{ secrets.FORGEJO_API_TOKEN }} + # Explicit base: in the GitHub Actions env, GITHUB_SERVER_URL points at + # github.com and forgejo-api.mjs would resolve the wrong base (410). + FORGEJO_API_URL: https://git.solla.dev/api/v1 + GH_TOKEN: ${{ github.token }} + steps: + - uses: actions/checkout@v5 + # Script is plain Node (stdlib) — no pnpm. Disable + # package-manager-cache: setup-node@v5 defaults to true and looks for + # pnpm (packageManager in package.json) before pnpm/action-setup. + - uses: actions/setup-node@v5 + with: + node-version: 24 + package-manager-cache: false + - name: Fetch PR metadata (manual recovery — bot auto-merges skip the closed event) + if: github.event_name == 'workflow_dispatch' + id: pr + run: | + PR="${{ inputs.pr }}" + STATE="$(gh pr view "$PR" --json state -q .state)" + if [ "$STATE" != "MERGED" ]; then + echo "PR #$PR não mergeada (state=$STATE) — skip" + exit 0 + fi + BASE="$(gh pr view "$PR" --json baseRefName -q .baseRefName)" + REPO="$(gh pr view "$PR" --json headRepository -q .headRepository.nameWithOwner)" + if [ "$BASE" != "main" ] || [ "$REPO" != "${{ github.repository }}" ]; then + echo "PR #$PR fora do contrato (base=$BASE repo=$REPO — precisa main + same-repo) — skip" + exit 0 + fi + BODY="$(gh pr view "$PR" --json body -q .body)" + { + echo "valid=true" + echo "pr_number=$PR" + echo "pr_body<<EOF" + echo "$BODY" + echo "EOF" + } >> "$GITHUB_OUTPUT" + - name: Promote Related #N awaiting plan → ready + if: steps.pr.outputs.valid == 'true' || github.event_name == 'pull_request' + env: + PR_BODY: ${{ steps.pr.outputs.pr_body || github.event.pull_request.body }} + run: node scripts/agent-promote-related-on-merge.mjs --pr "${{ steps.pr.outputs.pr_number || github.event.pull_request.number }}" diff --git a/AGENTS.md b/AGENTS.md index ab3eca86..83e164e6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,7 +12,7 @@ These are product/architecture constraints — not a ban on editing existing cod - Single Next.js app, three route groups: `(frontend)` public site, `(payload)` admin, `(campaign)` internal campaign tool. No separate Rust service or frontend app for `/campanha`; it is modeled with Payload collections. The campaign roles are `coordinator` (label "Coordenador Geral"), `advisor` ("Assessor"), `leader` ("Liderança"), and `candidate` ("Candidato", unrestricted visibility), all on the separate auth collection `campaignUser` (see "Campaign auth" below). An advisor sees and manages ONLY the municipalities they administer; a leader is locked down to the supporter contact tool. - **2026-07-23 remodel (DEPLOYED to production 2026-07-23): "Município" is the operational unit** (it replaced "Praça", which replaced "Núcleo Eleitoral"). Municípios are PREDEFINED (seeded, geography read-only): 435 rows — one per Bahia municipality (416), except Salvador (19 zone Municípios, ZE 1–19); Camaçari is a single whole municipality (ZE 170/171 aggregated); shared/leaky TSE zones are cut at the municipal boundary. Code identifier: `municipality`. Leaderships are one record per person (`contact` UNIQUE) linked to N municipalities and N organizations; vote pledges are per leadership×municipality with `declaredVotes` (staff-entered, leader-visible) vs `estimatedVotes` (three scenarios, staff-only, hidden from the leader). Campaign production data was reset by the remodel migrations (public-site data untouched). Master plan: `docs/plans/remodelagem-municipios.md` (supersedes `remodelagem-pracas.md`). Details: "Campaign Municípios model" below. -- Production is self-hosted on the homeserver (`jorgesolla1313.com.br`, behind the Cloudflare tunnel; container/database `teqo-1313`/`teqo_1313`). **There is no Vercel deploy anymore** — the Vercel deploy job, `vercel-promote.yml`, the Vercel scripts/`vercel.json` and the `VERCEL_*` repo secrets were removed in OPS50 (2026-08-17). The CI (`ci.yml` on `main`) is the verifier — full suite green — **plus the deploy step (OPS53, 2026-08-17; windowed since OPS65, 2026-08-18)**: `ci.yml` no longer fires on every push — it runs in a **fixed 30-minute window** (`schedule */30` + `workflow_dispatch`), where a cheap gate job (`gate`, host runner, no pnpm) reads the SHA deployed on the homeserver from `~/stack/docker-compose.yml` over SSH, diffs it against `main` HEAD and classifies "production change" using `.dockerignore` as the source of truth (docs/`.agents`/`.env*` changes skip; `scripts/`, `tests/`, `.forgejo/`, `AGENTS.md` count as production — conservative). No production change → suite + deploy skipped, run stays green; production change → full suite + deploy in the same window; dispatch always runs the full pipeline. The `deploy` job (runs-on `host` on the workstation runner) streams `scripts/deploy-homeserver.sh` to the homeserver over SSH; the remote script is idempotent ("already deployed" → skip), builds the migrator image first, applies pending migrations through the compose maintenance service `teqo-1313-migrate` **before the runner build** (OPS66: the static build reads the new schema — build-after-migrate, never the reverse), builds the standalone image at the pushed SHA (BuildKit secrets from `~/stack/teqo-1313.env`), pushes it to the local registry (`localhost:5000`), and recreates the container. Merge em `main` com mudança de produção verde == site atualizado em até ~1 h (janela de 30 min + suite + deploy). Runbook (rollback, falhas conhecidas): `docs/ops/teqo-1313-deploy.md`. **The agent-pool supervisor workflow was killed in OPS65** (the `*/10` tick cost 144 full `pnpm install`/day on the workstation; `agent-pool.yml` is gone, pool scripts/skills stay dormant). **The production database lives on the homeserver Postgres (`teqo_1313`, migrated from Neon in OPS51, 2026-08-17)** — the full production content (contacts, signatures, subscriptions, campaign) was restored there via `pg_dump`/`pg_restore` (dump in `/srv/hdd/backups/teqo-neon-pre-migracao/`); Neon remains alive and untouched as the documented rollback (re-point `DATABASE_URL` in `~/stack/teqo-1313.env`). Media moved to the homeserver Garage S3 in OPS52 (2026-08-18): the `media` collection uploads via `@payloadcms/storage-s3` when the `S3_*` envs are set, and Payload proxies the files through `/api/media/file/...` (the bucket stays private). Production env lives in `~/stack/teqo-1313.env` (bucket `teqo-media`, endpoint `http://host.docker.internal:3900` — o container alcança o Garage no host via `extra_hosts: host-gateway` + regras ufw `allow from 10.0.11.0/24 to any port 3900` (INPUT e route/FWD); a workstation/tailnet usa `http://100.119.220.31:3900`, key `teqo-key`); a partial `S3_*` config aborts the boot, and no `S3_*` means local disk (dev/test). The 40 pre-existing media rows are RECOVERED (OPS52-media, 2026-08-18): 38/40 objects restaurados no bucket a partir do WordPress (38 capas servindo 200 em `/api/media/file/...`; exceções sem fonte: `fim-escala-6x1.jpg` e `jorgesolla.jpg`) — re-executar via `pnpm media:recover` (restaura objetos sem tocar no DB; runbook em `docs/plans/ops52-media-recuperar-arquivos-impl.md`; desde OPS52-media-guard o reconcile exige `MEDIA_RECOVER_CONFIRM=1` — guard de intenção explícita para escrita no bucket, `--dry-run`/`--verify` seguem sem flag); the URLs themselves are the same contract the proxy now serves. Never point dev/test databases at the homeserver prod DB either — the local guards (`guard-dev-db`, `assertTestDatabase`) still enforce `localhost`/`_test`-suffixed targets. +- Production is self-hosted on the homeserver (`jorgesolla1313.com.br`, behind the Cloudflare tunnel; container/database `teqo-1313`/`teqo_1313`). **There is no Vercel deploy anymore** — the Vercel deploy job, `vercel-promote.yml`, the Vercel scripts/`vercel.json` and the `VERCEL_*` repo secrets were removed in OPS50 (2026-08-17). **CI/PR/deploy live on GitHub Actions (OPS71, 2026-08-19); the ISSUE TRACKER stays on the Forgejo** (`git.solla.dev/fsolla/teqo` — labels, claims, `pnpm issue`/`agent:*` and the post-merge flips all keep talking to the Forgejo API; GitHub is only the Actions host). The PR CI (`ci-pr.yml`, job único `checks` — OPS62) runs the same cascade as always (guards → lint → format → typecheck → knip → cycles → unit → int → build → e2e selected/afetado, skips via `ci-scope.mjs`); its check-run `CI (PR) / checks` is the required check of `main` — literal de match na proteção: `checks` (o GitHub casa pelo nome do check-run/job; a UI exibe `CI (PR) / checks` — pin ao vivo PR #742) — and the gate of the **native auto-merge** (`agent-pr-ready-automerge.yml` → `scripts/github-pr-automerge.mjs`, GraphQL `enablePullRequestAutoMerge`, rebase — armed with the `AUTOMERGE_PAT` real-user PAT, fail-closed without it; the built-in GITHUB*TOKEN would make the merge happen as `github-actions[bot]`, whose events never create workflow runs — the post-merge `closed` flips would silently never run, live finding PR #746 (OPS71-FLIP) — the server only merges with the required check green). **There is NO main verifier (`ci.yml`) anymore**: publishing is a **manual `workflow_dispatch`** (`.github/workflows/deploy.yml`) with two jobs — `verify` (hosted `ubuntu-latest`, suíte **full** incl. e2e full) → `deploy` (`needs: [verify]`, `runs-on: [self-hosted, homeserver]`): the GitHub self-hosted runner ON the homeserver executes `scripts/deploy-homeserver.sh` **locally** (no SSH; outbound-only runner, works behind the tunnel; the hosted job never touches the homeserver). The remote script is idempotent ("already deployed" → skip), builds the migrator image first, applies pending migrations through the compose maintenance service `teqo-1313-migrate` **before the runner build** (OPS66: the static build reads the new schema — build-after-migrate, never the reverse), builds the standalone image at the pushed SHA (BuildKit secrets from `~/stack/teqo-1313.env`), pushes it to the local registry (`localhost:5000`), and recreates the container; the repo comes from `TEQO_REPO_URL` (default `https://github.com/fsolla/teqo.git`, public). `pnpm push` pushes to `origin` — **the GitHub remote** (one-time local cutover: `git remote set-url origin git@github.com:fsolla/teqo.git`; the Forgejo stays as a secondary remote and its git repo freezes at the cutover point — tracker intacto por API). The Forgejo-era files (`.forgejo/workflows/`, `forgejo-pr-automerge.mjs`, `forgejo-dispatch.mjs`, `ci-classify-production.mjs`) were removed in the OPS71 Fase 2 (2026-08-19); rollback to the Forgejo CI flow is a revert of that removal — the frozen Forgejo `main` still holds the full pre-cutover tree. Runbook (rollback, falhas conhecidas, passos do cutover): `docs/ops/teqo-1313-deploy.md`. **The agent-pool supervisor workflow was killed in OPS65** (the `*/10`tick cost 144 full`pnpm install`/day on the workstation; `agent-pool.yml` is gone, pool scripts/skills stay dormant). **The production database lives on the homeserver Postgres (`teqo\*1313`, migrated from Neon in OPS51, 2026-08-17)** — the full production content (contacts, signatures, subscriptions, campaign) was restored there via `pg_dump`/`pg_restore`(dump in`/srv/hdd/backups/teqo-neon-pre-migracao/`); Neon remains alive and untouched as the documented rollback (re-point `DATABASE_URL`in`~/stack/teqo-1313.env`). Media moved to the homeserver Garage S3 in OPS52 (2026-08-18): the `media`collection uploads via`@payloadcms/storage-s3`when the`S3\*\*`envs are set, and Payload proxies the files through`/api/media/file/...`(the bucket stays private). Production env lives in`~/stack/teqo-1313.env`(bucket`teqo-media`, endpoint `http://host.docker.internal:3900` — o container alcança o Garage no host via `extra_hosts: host-gateway` + regras ufw `allow from 10.0.11.0/24 to any port 3900` (INPUT e route/FWD); a workstation/tailnet usa `http://100.119.220.31:3900`, key `teqo-key`); a partial `S3\__`config aborts the boot, and no`S3\_\*`means local disk (dev/test). The 40 pre-existing media rows are RECOVERED (OPS52-media, 2026-08-18): 38/40 objects restaurados no bucket a partir do WordPress (38 capas servindo 200 em`/api/media/file/...`; exceções sem fonte: `fim-escala-6x1.jpg`e`jorgesolla.jpg`) — re-executar via `pnpm media:recover`(restaura objetos sem tocar no DB; runbook em`docs/plans/ops52-media-recuperar-arquivos-impl.md`; desde OPS52-media-guard o reconcile exige `MEDIA_RECOVER_CONFIRM=1`— guard de intenção explícita para escrita no bucket,`--dry-run`/`--verify` seguem sem flag); the URLs themselves are the same contract the proxy now serves. Never point dev/test databases at the homeserver prod DB either — the local guards (`guard-dev-db`, `assertTestDatabase`) still enforce `localhost`/`\_test`-suffixed targets. - Donations are NOT handled in this app — they run through QueroApoiar (`apoiar.me/jorgesolla`, TSE-homologated). The site only needs a "Doar" link/CTA out to that URL. - **Embasamento de produto/domínio:** `docs/research/` guarda o compêndio de literatura (campanha de DF na Bahia, geografia do voto, cartografia eleitoral) e o relatório de discovery aprovado (2026-07-21) com o playbook dado→decisão. Para métricas, análises territoriais, mapa e priorização em `/campanha`, consulte-o antes de inventar — o kernel: a disputa de DF é conta de quociente fragmentada; leitura útil é relativa e local (captura, LQ, quantis, cobertura de meta por pledges), nunca % estadual absoluto. @@ -81,6 +81,7 @@ curl -X POST "https://<prod-domain>/api/revalidate?tag=global_privacy-policy" \ - **Auth:** send the secret via `x-revalidate-secret` or `Authorization: Bearer <secret>`; it is compared to `REVALIDATE_SECRET` with a constant-time check. - `REVALIDATE_SECRET` must be set in the **production** env (and in your local env if you want to test the endpoint). It is documented in `.env.example`. - **Tag:** optional `?tag=` query or JSON body `{ "tag": "..." }` (query wins). Allowlist: `posts` (default), `global_privacy-policy`, `election-tse` (bust after re-running `pnpm db:seed:tse` so the cached elections-tab reads refresh), `municipality-catalog` (bust after a migration/seed that adds or renames a município so chip indexes and `unstable_cache` loaders refresh), `social-feed` (bust after a direct-DB write to the `social-feed-settings` global — e.g. an admin-less seed of the YouTube/Instagram feed config; admin edits self-revalidate). Unknown tag → `400`. +- **Instagram sync status (S11):** the global's `instagramSyncStatus` jsonb (hidden) is written by the render path, by `afterChange` (re-sync only when IG credentials changed), and by the admin button `POST /api/social-feed/sync` (admin session via `payload.auth` + same-origin guard); the `InstagramSyncStatusPanel` ui field renders it. Runbook de credenciais: `docs/ops/instagram-feed-token-runbook.md`. - **Responses:** `200 { revalidated: true, tag: '...' }` on success; `401` if the secret is missing/wrong; `500` if `REVALIDATE_SECRET` is not configured on the server. - This POST is the required last step of the post-seed / direct-DB-change runbook. @@ -158,7 +159,7 @@ The artifact is at **v3**: v2 added E8's `campoFederalVotesByYear` / `federalTal 1. Confirm `DATABASE_URL` is `postgresql://teqo:teqo@localhost:5432/teqo` and `.env.test` points to `teqo_test`; never use the homeserver prod DB for development, tests, E2E, or local builds. In a provisioned worktree (`pnpm worktree next`) the values come from its `.env.local` / `.env.test.local` (`teqo_wt<slot>` / `teqo_wt<slot>_test`, dev port `3100+slot`) — read them there instead of assuming the main-repo names. 2. Run `pnpm db:start`, `pnpm migrate:status`, and `DATABASE_URL=postgresql://teqo:teqo@localhost:5432/teqo_test pnpm migrate:status`; every migration must show `Ran: Yes`. -3. Run `pnpm generate:types`, `pnpm generate:importmap` when components changed — **com as envs `S3_*` setadas (valores dummy ok) enquanto o plugin `s3Storage` for condicional: sem elas o importMap fica órfão do `S3ClientUploadHandler` e o admin abre em branco em produção (OPS69, P0 em 2026-08-19)** —, `pnpm exec tsc --noEmit`, `pnpm lint` (zero warnings — the script enforces `--max-warnings=0`), `pnpm format:check` (Prettier is the formatting authority; `pnpm format` fixes), `pnpm exec knip` (dead files/dependencies are CI-blocking; delete what your change orphaned), `pnpm check:cycles` (madge — zero circular imports), `pnpm test` (unit + int), and `pnpm build` against the local database. Payload CLI scripts (`migrate`, `generate:*`, `payload`) and the build migrate step use `--conditions=react-server` so `server-only` modules load correctly outside Next.js. **O e2e não roda mais no gate local (OPS59):** ele roda **uma vez, no CI** (PR e main — gateia merge e deploy); local opcional para feedback antecipado: `pnpm test:e2e:affected` (gera `--no-deps` para runs filtrados — S6-FOLLOWUP: no dev mode a cadeia de dependencies dos projects arrasta os arquivos das dependências via closure; filtros manuais locais: `pnpm test:e2e --no-deps -- <path>` ou `--no-deps --project=<família>`). +3. Run `pnpm generate:types`, `pnpm generate:importmap` when components changed — **com as envs `S3_*` setadas (valores dummy ok) enquanto o plugin `s3Storage` for condicional: sem elas o importMap fica órfão do `S3ClientUploadHandler` e o admin abre em branco em produção (OPS69, P0 em 2026-08-19)** —, `pnpm exec tsc --noEmit`, `pnpm lint` (zero warnings — the script enforces `--max-warnings=0`), `pnpm format:check` (Prettier is the formatting authority; `pnpm format` fixes), `pnpm exec knip` (dead files/dependencies are CI-blocking; delete what your change orphaned), `pnpm check:cycles` (madge — zero circular imports), `pnpm test` (unit + int), and `pnpm build` against the local database. Payload CLI scripts (`migrate`, `generate:*`, `payload`) and the build migrate step use `--conditions=react-server` so `server-only` modules load correctly outside Next.js. **Política de e2e (OPS72):** o e2e **não roda no gate local** (`gate:push`) — é passo **explícito da skill** (`work-issue`/`agent-work-issue`): antes do push o agente roda localmente os e2e que **criou** + os da **mesma superfície afetada** — **discricionário** (o agente decide quais; ferramenta: `pnpm test:e2e:affected`, espelho local do classifier — gera `--no-deps` para runs filtrados, S6-FOLLOWUP: no dev mode a cadeia de dependencies dos projects arrasta os arquivos das dependências via closure; filtros manuais locais: `pnpm test:e2e --no-deps -- <path>` ou `--no-deps --project=<família>`; limitação #72: `--no-deps` + projetos paralelos colide no `seedTestUser` → `--workers=1` ou a cadeia padrão de projetos). O **CI de PR roda apenas o blast radius** (modo `selected` via `ci-scope.mjs` — **nunca `full`**; diff high-risk classifica `full` e o PR simplesmente não roda e2e). O e2e **full** roda no `verify` do deploy manual (gateia merge e deploy — rede de segurança final antes de publicar). - Never run `knip --fix` blind: when knip's module graph is incomplete (it still cannot load `payload.config.ts` — ledgered as P3), `--fix` deletes exports that migrations/tests still reference. Verify each removal (e.g. `git grep -w <symbol>`) before deleting. - Run gate commands bare — never piped (`pnpm test | tail` swallows the exit code, and a real failure was masked exactly this way during Pass 2). Large outputs are captured by the terminal; read them from there. 4. Scan every edited runtime source file with Aikido and resolve new findings before handoff. Do not expand hardening work into unrelated pre-existing findings without an explicit scope change. diff --git a/README.md b/README.md index 98b0101e..3dad0584 100644 --- a/README.md +++ b/README.md @@ -6,16 +6,16 @@ > **Cheatsheet de operação agentic (humanos)** — o fluxo em 5 linhas: > -> 1. O agente roda `pnpm agent:claim` (ou `pnpm worktree next --issue N`) e pega a próxima Issue `ready` por prioridade (`prio:P0..P3`). +> 1. O agente roda `pnpm agent:claim` (ou `pnpm worktree next --issue N`) e pega a próxima Issue `ready` por prioridade (`prio:P0..P3`). O **tracker de Issues vive no Forgejo** (`git.solla.dev/fsolla/teqo`); o código/PR/CI vive no **GitHub**. > 2. Ele implementa, roda o fast gate (`lint + typecheck + unit`) e `pnpm push -u origin HEAD` — abre a PR para `main` (Ready, `Closes #N`). -> 3. CI verde → o safety net `agent-pr-ready-automerge.yml` mergea em `main` por rebase; o verificador `ci.yml` do main roda a suíte completa na janela fixa de 30 min. -> 4. Com mudança de produção → o job `deploy` publica no homeserver (`jorgesolla1313.com.br`, self-hosted) — migrações aplicadas antes do rollout. -> 5. Secrets humanos (uma vez): `FORGEJO_API_TOKEN` no Forgejo; envs de prod em `~/stack/teqo-1313.env` no homeserver. Branch protection de `main` já aplicada (reaplicar: `pnpm configure:branch-protection`). +> 3. CI verde (`CI (PR) / checks`) → o safety net `agent-pr-ready-automerge.yml` arma o **auto-merge nativo** (rebase) — o servidor só mergea com o required check verde. +> 4. Publicar é **manual**: dispatch de `deploy.yml` roda a suíte full (`verify`) e o job `deploy` publica no homeserver (`jorgesolla1313.com.br`, runner self-hosted) — migrações aplicadas antes do rollout. Nada é automático pós-merge. +> 5. Secrets humanos (uma vez): `FORGEJO_API_TOKEN` no GitHub (flips pós-merge no Forgejo); envs de prod em `~/stack/teqo-1313.env` no homeserver. Branch protection de `main` já aplicada (reaplicar: `pnpm configure:branch-protection`). > > Comandos: `pnpm agent:claim | agent:register | agent:prioritize | agent:file-miss | worktree next` e `pnpm db:seed:minimal`. > Labels: estado `ready|in-progress|blocked|done|in-prod`, `prio:P0..P3`, `kind:*`, `needs:migration|consent`, `requirements-changed`. -> **Agente faz sozinho:** claim → implementar → PR → main. **Só humano:** envs do homeserver, branch protection, runbook de rollback. -> Tudo em detalhe: [`docs/AGENT-OPS.md`](docs/AGENT-OPS.md) · CI: `.forgejo/workflows/ci-pr.yml` + `ci.yml`. +> **Agente faz sozinho:** claim → implementar → PR → main. **Só humano:** deploy (dispatch), envs do homeserver, branch protection, runbook de rollback. +> Tudo em detalhe: [`docs/AGENT-OPS.md`](docs/AGENT-OPS.md) · CI: `.github/workflows/ci-pr.yml` · Deploy: `.github/workflows/deploy.yml`. Teqo starts as the official digital platform for **deputado Jorge Solla** and evolves into a **white-label civic engagement platform** for politicians in Brazil. diff --git a/docs/AGENT-OPS.md b/docs/AGENT-OPS.md index 4bb416b1..d887924d 100644 --- a/docs/AGENT-OPS.md +++ b/docs/AGENT-OPS.md @@ -1,27 +1,27 @@ # AGENT-OPS — operação do paradigma de agentes paralelos -Uma página. Norte: simplicidade (só Forgejo + Cursor), agente entrega em `main`, deploy de produção só após o verificador full-suite verde. +Uma página. Norte: simplicidade (Forgejo = tracker, GitHub = Actions), agente entrega em `main`, deploy de produção é **manual** com verificação full (OPS71). ## Ambientes de dados (test ladder — nunca misturar) -| Ambiente | Conteúdo | Quem usa | -| ------------------------------------------- | ------------------------------------------------------------ | -------------------------------- | -| **Mínimo** (`teqo_test` local / service CI) | schema migrado + `pnpm db:seed:minimal` (sintético, sem PII) | agentes, Cursor Cloud, CI | -| **Prod** (`teqo_1313`, homeserver) | real | job `deploy` do `ci.yml` (OPS53) | +| Ambiente | Conteúdo | Quem usa | +| ------------------------------------------- | ------------------------------------------------------------ | -------------------------------------------- | +| **Mínimo** (`teqo_test` local / service CI) | schema migrado + `pnpm db:seed:minimal` (sintético, sem PII) | agentes, Cursor Cloud, CI | +| **Prod** (`teqo_1313`, homeserver) | real | job `deploy` do `deploy.yml` (OPS53 + OPS71) | Agentes **nunca** recebem `DATABASE_URL` de prod e nunca setam `ALLOW_REMOTE_DB`. Não há mais smoke `stage` / `ALLOW_STAGE_TEST_DB` / `ci-stage.yml`. ## Fluxo ```text -claim → feature branch → PR --base main → CI PR green (cascade + skips) → auto-merge em main -main → ci.yml (janela de 30 min, OPS65) full suite verde → job `deploy` (homeserver, OPS53 — merge com mudança de produção == site atualizado em ~30 min + suite/deploy) +claim → feature branch → PR --base main (GitHub) → CI PR green (cascade + skips) → auto-merge nativo em main (rebase) +main → (opcional, deliberado) workflow_dispatch deploy.yml → verify full suite verde → job deploy no homeserver (OPS53) ``` -**Skills:** `plan-issue` (intenção + Issues) → `work-issue` (humano: Issue já claimada → impl plan → confirmação → execução) ou `agent-work-issue` (pool: já claimada → impl plan → execução sem pausa) → `/simplify` → `capture-review-debts` → PR `--base main` → `project-status`. `docs/roadmap.md` = legado congelado; fonte canônica = Issues do Forgejo (`git.solla.dev/fsolla/teqo`). +**Skills:** `plan-issue` (intenção + Issues) → `work-issue` (humano: Issue já claimada → impl plan → confirmação → execução) ou `agent-work-issue` (pool: já claimada → impl plan → execução sem pausa) → `/simplify` → `capture-review-debts` → PR `--base main` (GitHub) → `project-status`. `docs/roadmap.md` = legado congelado; fonte canônica = Issues do Forgejo (`git.solla.dev/fsolla/teqo`). -- **Agente faz sozinho:** claim (pool-supervisor no pool; humano via `pnpm agent:claim` fora da sessão — OPS33: `worktree next` claima) → implementa → **`pnpm push`** → PR via API/MCP **Ready** (nunca draft) com `Closes #N` → o safety net `agent-pr-ready-automerge.yml` espera o rollup `CI (PR) / checks` e mergea por rebase. Regra always-on: `.agents/rules/agent-pr-workflow.mdc`. Em Cursor Cloud: `ManagePullRequest` com `draft: false` (o default draft da tool **não** vale neste repo) — o merge continua pelo safety net. -- **Só humano:** envs de produção no homeserver (`~/stack/teqo-1313.env`, fora do repo — o deploy as lê via BuildKit secrets), `pnpm configure:branch-protection` (idempotente; reaplicar se o drift voltar), runbook manual/rollback (`docs/ops/teqo-1313-deploy.md`). +- **Agente faz sozinho:** claim (pool-supervisor no pool; humano via `pnpm agent:claim` fora da sessão — OPS33: `worktree next` claima) → implementa → **e2e local afetado** (OPS72, discricionário: roda os e2e que criou + os da mesma superfície — `pnpm test:e2e:affected`; ver skill) → **`pnpm push`** (origin = GitHub) → PR via `node scripts/github-pr.mjs` **Ready** (nunca draft) com `Closes #N` → o safety net `agent-pr-ready-automerge.yml` arma o **auto-merge nativo do GitHub** (`enablePullRequestAutoMerge`, rebase) — o servidor mergea quando o required check `CI (PR) / checks` fica verde. Regra always-on: `.agents/rules/agent-pr-workflow.mdc`. Em Cursor Cloud: `ManagePullRequest` com `draft: false` (o default draft da tool **não** vale neste repo) — o merge continua pelo safety net. +- **Só humano:** envs de produção no homeserver (`~/stack/teqo-1313.env`, fora do repo — o deploy as lê via BuildKit secrets), `GITHUB_TOKEN` local (PAT) para os scripts GitHub, secrets no repo GitHub, `pnpm configure:branch-protection` (idempotente; reaplicar se o drift voltar), runbook manual/rollback (`docs/ops/teqo-1313-deploy.md`). ### Dono do PR, dono do CI @@ -36,28 +36,29 @@ main → ci.yml (janela de 30 min, OPS65) full suite verde → job `deploy` (hom | Comando | Faz | | ------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | -| `pnpm agent:claim [-- --dry-run]` | Fila ready+unblocked por prio → `in-progress` + brief | +| `pnpm agent:claim [-- --dry-run]` | Fila ready+unblocked por prio → `in-progress` + brief (Forgejo) | | `pnpm worktree next [--issue N] [--stay]` | Claim determinístico (mesma fila/lock do claim) → worktree + provisionamento | -| `pnpm agent:register -- --id X --title T [--prio P1] [--depends A,B] [--plan docs/plans/x.md] [--model slug]` | Cria Issue (`--plan` ⇒ `blocked` até `agent:ready`; sem plano ⇒ `ready`) | +| `pnpm agent:register -- --id X --title T [--prio P1] [--depends A,B] [--plan docs/plans/x.md] [--model slug]` | Cria Issue no Forgejo (`--plan` ⇒ `blocked` até `agent:ready`; sem plano ⇒ `ready`) | | `pnpm agent:ready -- --issue N[,N…]` | Pós-merge do plano: `blocked`→`ready` (só Issues com link `docs/plans/`) | | `pnpm agent:status` | Overview / fila / mermaid | | `pnpm agent:prioritize -- <issue> <P0..P3>` | Troca `prio:*` | | `pnpm agent:file-miss -- --title ...` | Issue `kind:agent-miss` | | `pnpm agent:pool -- status\|tick --dry-run\|doctor` | Pool Cloud **dormente (OPS65)** — o workflow `agent-pool.yml` foi removido; dispatch falha 404; scripts/skill ficam para histórico | -| `pnpm push [-- -u origin HEAD …]` | Canônico: ensure-deps + gate:push + push | -| `pnpm configure:branch-protection [-- --dry-run]` | Proteção de `main`: required status check `CI (PR) / checks` (rollup do ci-pr; idempotente — cria/atualiza/no-op) | +| `pnpm push [-- -u origin HEAD …]` | Canônico: ensure-deps + gate:push + push para o **GitHub** (origin) | +| `GITHUB_TOKEN=… node scripts/github-pr.mjs --head <b> --title <t> --body-file <f>` | Abre PR no **GitHub** (Ready, base `main`, nunca draft) | +| `GITHUB_TOKEN=… pnpm configure:branch-protection [-- --dry-run]` | Proteção de `main` no **GitHub**: required check `CI (PR) / checks` (idempotente — cria/atualiza/no-op) | | `pnpm db:seed:minimal` | DB mínimo sintético | -Labels: `ready|in-progress|blocked|done|in-prod`, `prio:*`, `kind:*`, `needs:*`. +Labels (Forgejo): `ready|in-progress|blocked|done|in-prod`, `prio:*`, `kind:*`, `needs:*`. **plan-issue (OPS17 + OPS18):** gate pós-overview fecha o lote antes de qualquer Issue/PR; register com `--plan` nasce `blocked` (não entra na fila do claim/pool); após o PR de planos mergear em `main` (`Related #N`), promote dual idempotente: (A) `pnpm agent:ready` na sessão; (B) Action `plan-issue-ready-on-main-merge.yml` lê `Related #N` e promove Issues ainda “aguardando plano” (`blocked` + link `docs/plans/`). Chores sem plano continuam nascendo `ready`. ## Contrato de PR 1. Schema → atualizar `db:seed:minimal` no mesmo PR (`needs:migration`). -2. Migrations NÃO são serializadas entre PRs: o CI (migrate + int, incl. `campaignMigrationReconciliation`) valida a cadeia em todo PR e na main; rebase antes de `migrate:create` continua obrigatório. -3. Ready + auto-merge: `pnpm push -u origin HEAD` → PR via API/MCP (Ready, base `main`, `Closes #N`) → o safety net `agent-pr-ready-automerge.yml` (`scripts/forgejo-pr-automerge.mjs`) espera o rollup `CI (PR) / checks` e mergea por rebase — não há `gh` em nenhuma máquina (OPS50), o safety net é quem espera os checks. **Merge é por rebase** (nunca merge commit): preserva a linha histórica limpa do `main` e é o modo que o CI guarda contra conflitos latentes. -4. **Changelog da entrega (OPS44):** toda entrega escreve `docs/changelog/<data>-<id>.md` (ex. `2026-08-13-ops44.md`) e roda `pnpm changelog:build` antes do push — o agregado `docs/CHANGELOG-AGENTS.md` é **insert-only** (nunca remove/reescreve entradas; entradas históricas não são migradas). Guard de CI `docs-guards`: (a) agregado é append-only (multiset sobre blobs — perda de linha falha); (b) `docs/changelog/` é additions-only (M/D falham); (c) agregado está up to date com `docs/changelog/` (`changelog:check`); (d) nenhum marcador de conflito em diffs de markdown (docs/, AGENTS.md, .agents/ — cobre PRs docs-only, onde a suíte unit não roda). **Desde OPS63 os mesmos três checks rodam também no pre-push local** (`gate:push` → `gate:ci`, fase 1 — mesmos scripts, default `origin/main`). Restauração legítima (header muda, restauração estilo D8) escapa escrevendo **`changelog-rewrite: <motivo>` como linha própria** no body do PR — definição canônica aqui; o texto do checkbox do PR template nunca ativa (linha-ancorado). **O escape é CI-only (body do PR):** no pre-push local a restauração legítima falha com a mesma mensagem do CI — bypass documentado é `git push --no-verify` direto (o CI, com o escape no body, continua sendo a via da restauração). A própria troca `--merge` → `--rebase` deste doc usou o escape — header do agregado mudou. +2. Migrations NÃO são serializadas entre PRs: o CI (migrate + int, incl. `campaignMigrationReconciliation`) valida a cadeia em todo PR; rebase antes de `migrate:create` continua obrigatório. +3. Ready + auto-merge: `pnpm push -u origin HEAD` (GitHub) → PR via `node scripts/github-pr.mjs` (Ready, base `main`, `Closes #N`) → o safety net `agent-pr-ready-automerge.yml` (`scripts/github-pr-automerge.mjs`) arma o **auto-merge nativo do GitHub por rebase** — o servidor espera o required check `CI (PR) / checks` e mergea; nada a armar por PR, não há `gh` (OPS50) e não há poll (OPS71). **Merge é por rebase** (nunca merge commit): preserva a linha histórica limpa do `main` e é o modo do auto-merge. +4. **Changelog da entrega (OPS44):** toda entrega escreve `docs/changelog/<data>-<id>.md` (ex. `2026-08-13-ops44.md`) e roda `pnpm changelog:build` antes do push — o agregado `docs/CHANGELOG-AGENTS.md` é **insert-only** (nunca remove/reescreve entradas; entradas históricas não são migradas). Guard de CI `docs-guards`: (a) agregado é append-only (multiset sobre blobs — perda de linha falha); (b) `docs/changelog/` é additions-only (M/D falham); (c) agregado está up to date com `docs/changelog/` (`changelog:check`); (d) nenhum marcador de conflito em diffs de markdown (docs/, AGENTS.md, .agents/ — cobre PRs docs-only, onde a suíte unit não roda). **Desde OPS63 os mesmos três checks rodam também no pre-push local** (`gate:push` → `gate:ci`, fase 1 — mesmos scripts, default `origin/main`). Restauração legítima (header muda, restauração estilo D8) escapa escrevendo **`changelog-rewrite: <motivo>` como linha própria** no body do PR — definição canônica aqui; o texto do checkbox do PR template nunca ativa (linha-ancorado). **O escape é CI-only (body do PR):** no pre-push local a restauração legítima falha com a mesma mensagem do CI — bypass documentado é `git push --no-verify` direto (o CI, com o escape no body, continua sendo a via da restauração). ## Registries compartilhados (serialização por arquivo, não por PR) @@ -67,39 +68,46 @@ Arquivos de "lista" que N agentes escrevem em paralelo (changelog, plans, migra - **`serializes` no plano de intenção** nomeia o registro exato que a Issue toca (ex. `serializes: 'docs/plans/'`), para o executor agendar com clareza — é dica de ordem, não lock (rebase continua obrigatório). - O rebase (Contrato de PR item 3) é o mecanismo real de conciliação: conflitos de append resolvem com `git checkout --ours/--theirs` por lado. -## CI por alvo +## CI por alvo (GitHub Actions, OPS71) -| Workflow | Trigger | Banco | Passos | -| ------------------------------------ | ----------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `ci-pr.yml` | PR → `main` | 2× service Postgres 17 | Job único `checks` sequencial (OPS62 X1): guards → lint→format→typecheck→knip→cycles→unit→int→build→e2e; skips via `ci-scope.mjs` (step `scope`); **fail-fast estrutural** — step vermelho interrompe o job, o run falha e o slot é liberado; e2e 1 processo com `PLAYWRIGHT_WORKERS=4`; 2 services por nome na rede do job (`postgres-int`/`postgres-build`, sem publish host) | -| `ci.yml` | schedule `*/30` / dispatch `main` | 2× service Postgres 17 | Janela fixa (OPS65): gate `gate` (host, main-only, sem pnpm) classifica mudança de produção desde a **revision do container rodando** no homeserver (`.dockerignore` = fonte da verdade; fail-open; incapaz de classificar → run **vermelho**); sem mudança → suite + deploy skipped, run verde; com mudança → job único `checks` sequencial (mesma mecânica X1) + job `deploy` (runs-on `host`) publica no homeserver via `scripts/deploy-homeserver.sh` (OPS53, guard "already deployed") | -| `issue-done-on-main-merge.yml` | PR merged → `main` (same-repo) | — | `Closes`/`Fixes` → `done` + `in-prod` (PAT `FORGEJO_API_TOKEN`; `GITHUB_TOKEN` 403 em Issues) | -| `plan-issue-ready-on-main-merge.yml` | PR merged → `main` (same-repo) | — | `Related #N` aguardando plano → `ready` (OPS18; soft-skip; mesmo PAT) | -| `agent-pr-ready-automerge.yml` | PR same-repo → `main` (open/sync/…) | — | Safety net via `forgejo-pr-automerge.mjs` (plain Node): espera o rollup `checks` e mergea por rebase com `FORGEJO_API_TOKEN` (o `GITHUB_TOKEN` nativo não tem write no repo — o push do merge é rejeitado com 409); draft `cursor/*` → Ready; draft não-`cursor/*` = veto, skip | -| `archive-cursor-agent.yml` | dispatch manual (ops) | — | Helper: cancela run + arquiva um agente Cursor Cloud (`CURSOR_API_KEY`) — **dormente (OPS65)** como o pool | +| Workflow | Trigger | Banco | Passos | +| ------------------------------------ | --------------------------------------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `ci-pr.yml` | PR → `main` (same-repo) | 2× service Postgres 17 | Job único `checks` sequencial (OPS62 X1): guards → lint→format→typecheck→knip→cycles→unit→int→build→e2e; skips via `ci-scope.mjs` (step `scope`); **fail-fast estrutural** — step vermelho interrompe o job, o run falha; `concurrency` nativo cancela o run em push novo; e2e **apenas blast radius** (`selected`, nunca `full` — OPS72) em 1 processo com `PLAYWRIGHT_WORKERS=4`; 2 services por nome na rede do job (`postgres-int`/`postgres-build`, sem publish host); fork-PR excluída no `if:` do job (repo público — não queima minutes) | +| `deploy.yml` | **`workflow_dispatch` manual** | 2× service Postgres 17 | **Dois jobs:** `verify` (hosted, suíte **full** sem skips — lint→format→typecheck→knip→cycles→unit→int→build→e2e full) → `deploy` (`needs: [verify]`, `runs-on: [self-hosted, homeserver]`) executa `scripts/deploy-homeserver.sh <sha>` **localmente** no homeserver (sem SSH; idempotente; HEAD guard; flock; migrator→migrate→runner→rollout→smoke) | +| `issue-done-on-main-merge.yml` | PR merged → `main` (same-repo) + **dispatch manual (recovery)** | — | `Closes`/`Fixes` → `done` + `in-prod` + **close** **no Forgejo** (PAT `FORGEJO_API_TOKEN`; body do PR via `PR_BODY` do evento GitHub — no dispatch, busca via `gh` com o mesmo contrato: merged + main + same-repo) | +| `plan-issue-ready-on-main-merge.yml` | PR merged → `main` (same-repo) + **dispatch manual (recovery)** | — | `Related #N` aguardando plano → `ready` **no Forgejo** (OPS18; soft-skip; mesmo PAT) | +| `agent-pr-ready-automerge.yml` | PR same-repo → `main` (open/sync/…) | — | Safety net via `github-pr-automerge.mjs` (plain Node, REST+GraphQL): draft `cursor/*` → Ready (PATCH); arm `enablePullRequestAutoMerge` (rebase) **com o `AUTOMERGE_PAT`** (fail-closed — nunca o `GITHUB_TOKEN` nativo, cujo merge como `github-actions[bot]` não cria runs no `closed`; OPS71-FLIP) — o **servidor** espera o required check `CI (PR) / checks` (o OPS64 "rollup pode mentir" é estrutural aqui: job único posta um check-run honesto); draft não-`cursor/*` = veto, skip | +| `archive-cursor-agent.yml` | dispatch manual (ops) | — | Helper: cancela run + arquiva um agente Cursor Cloud (`CURSOR_API_KEY`) — **dormente (OPS65)** como o pool | -`agent-pool.yml` foi **removido no OPS65** (pool dormente; o tick `*/10` custava 144 `pnpm install`/dia na workstation). +`agent-pool.yml` foi **removido no OPS65** (pool dormente; o tick `*/10` custava 144 `pnpm install`/dia na workstation). O verificador de main (`ci.yml`) foi **eliminado no OPS71** (gate 2026-08-19): só o `verify` do dispatch manual roda a suíte full. -Action runtimes: `actions/checkout@v5`, `actions/setup-node@v5`, `pnpm/action-setup@v6` e `styfle/cancel-workflow-action@0.13.1` usam Node 24 nativo — sem `FORCE_JAVASCRIPT_ACTIONS_TO_NODE24`. +Action runtimes: `actions/checkout@v5`, `actions/setup-node@v5`, `pnpm/action-setup@v6` e `actions/cache@v4`. Fast gate: `pnpm gate:fast`. Push: `pnpm push`. -### Secrets (humano, uma vez) +### Secrets (humano, uma vez — repo GitHub) -| Secret | Uso | -| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `FORGEJO_API_TOKEN` | Supervisor do pool e scripts Forgejo (`forgejo-api.mjs`); também nos workflows pós-merge (`issue-done`, `plan-issue-ready`) e no safety net — o `GITHUB_TOKEN` nativo 403 em Issues e não tem write no repo (OPS61) | -| `CURSOR_API_KEY` | Supervisor do pool (Cursor Cloud) — **dormente (OPS65)**; só o helper `archive-cursor-agent.yml` a usa hoje | +| Secret | Uso | +| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `FORGEJO_API_TOKEN` | Scripts Forgejo (`forgejo-api.mjs`) e flips pós-merge (`issue-done`, `plan-issue-ready`) — o `GITHUB_TOKEN` nativo do GitHub não toca Issues do Forgejo (OPS61) | +| `AUTOMERGE_PAT` | **Arm do auto-merge nativo** (`agent-pr-ready-automerge.yml`) — PAT de usuário real (fsolla) com `pull-requests: write` + `contents: write` no repo. **Sem ele o job falha fail-closed**: o `GITHUB_TOKEN` nativo armado faria o merge como `github-actions[bot]` e o `closed` do PR não criaria runs (anti-recursão do GitHub) — os flips pós-merge ficariam silenciosamente mortos (OPS71-FLIP, PR #746) | +| `CURSOR_API_KEY` | Supervisor do pool (Cursor Cloud) — **dormente (OPS65)**; só o helper `archive-cursor-agent.yml` a usa hoje | -`POOL_GITHUB_TOKEN` é **legado** (era da transição GitHub): o `scripts/agent-pool.mjs` o ignora quando presente; não criar no Forgejo. Envs de produção (DB, S3, VAPID, `REVALIDATE_SECRET`, `NEXT_PUBLIC_SITE_URL`) vivem em `~/stack/teqo-1313.env` no homeserver — nunca como secrets do Forgejo. +Local: `GITHUB_TOKEN` (PAT com escopo `repo`) para `node scripts/github-pr.mjs`, `pnpm configure:branch-protection` e qualquer script GitHub — o push SSH usa a chave `~/.ssh/id_ed25519` registrada no GitHub. Envs de produção (DB, S3, VAPID, `REVALIDATE_SECRET`, `NEXT_PUBLIC_SITE_URL`) vivem em `~/stack/teqo-1313.env` no homeserver — nunca como secrets do GitHub. `POOL_GITHUB_TOKEN` é **legado** (era da transição GitHub pré-OPS50). -Branch protection de `main` (aplicada em 2026-08-18, OPS61): required status check `CI (PR) / checks` (o rollup do `ci-pr.yml` — único contexto que cobre a cascata inteira), `strict=false`, 0 reviews. O `waitForChecks` do safety net espera esse mesmo rollup antes de mergear (o próprio status do job é `pending` durante o run e nunca entra no veredito); a regra no servidor é o gate real — o Forgejo bloqueia o merge POST com 405 se o contexto exigido não estiver verde, inclusive merges manuais via API (a PR #52 entrou em main com CI vermelho por merge manual). **OPS64:** o CLI nunca confia no rollup sozinho — varre os statuses individuais e falha em qualquer `failure`/`error` (pin em `forgejoApi.unit.spec.ts`, cenário run 607: rollup verde com shard de e2e vermelho → throw). Com o desenho de job único do OPS62 (sem matrix, sem shards) o status do job `checks` já é o veredito honesto da cascata — o bug de agregação do parent de matrix deixou de existir estruturalmente. Reaplicar/consertar drift: `pnpm configure:branch-protection` (idempotente). +### Flips pós-merge (OPS71-FLIP): o evento `closed` e a recuperação manual -Deploy (OPS53 + OPS65): o `ci.yml` do main roda em **janela fixa de 30 min** (`schedule */30` + `workflow_dispatch` — não dispara mais a cada push). O job `gate` (host, sem pnpm) lê a **revision do container rodando** no homeserver (label `org.opencontainers.image.revision` — o compose pode mentir após rollback falho) e classifica "mudança de produção" pelos padrões do `.dockerignore` (fonte da verdade do artefato; `docs/`, `.agents/`, `.env*` viram skip; `scripts/`, `tests/`, `.forgejo/`, `AGENTS.md` contam como produção — conservador). Deployed == HEAD → suite e deploy skipped (run verde); estado desconhecido (sem revision) → suite roda fail-open; gate incapaz de classificar → suite roda e o run fica **vermelho** (nunca blackout). Com mudança → suite full verde → job `deploy` (runs-on `host`) streama `scripts/deploy-homeserver.sh` para o homeserver via SSH — build no homeserver (secrets de `~/stack/teqo-1313.env`), registry `localhost:5000`, migrate pelo serviço de maintenance `teqo-1313-migrate` antes do rollout, smoke pós-deploy; o script é idempotente (revision do container já no SHA → skip verde "already deployed"). Nenhum secret novo no Forgejo. Runbook (rollback, falhas conhecidas): `docs/ops/teqo-1313-deploy.md`. O deploy Vercel (`vercel-promote.yml`, scripts, `vercel.json`) foi removido no OPS50. +O flip `issue-done`/`plan-issue-ready` roda no `pull_request: closed` do PR no GitHub — mas **o merge feito pelo auto-merge armado com o `GITHUB_TOKEN` nativo é atribuído a `github-actions[bot]` e NÃO cria workflow runs** (anti-recursão documentada do GitHub: eventos acionados por ações do `GITHUB_TOKEN` não geram runs; o `closed` fica no timeline do PR com 0 runs — achado ao vivo no PR #746). Isso só não acontece porque o arm agora usa o `AUTOMERGE_PAT` (merge atribuído a usuário real → runs criados, comportamento do PR #742). + +**Se mesmo assim o flip não rodou** (evento perdido, run falhou, PR mergeado por bot antes do fix): o workflow tem **`workflow_dispatch` de recuperação** — dispatch de `issue-done-on-main-merge.yml` (ou `plan-issue-ready-on-main-merge.yml`) com o input `pr` = número do PR; o job busca o PR via `gh` e aplica o mesmo contrato do evento (merged + base main + same-repo, senão skip). Fallback local (sem GitHub): `PR_BODY='Closes #N' node scripts/forgejo-issue-transition.mjs --pr <N>` com o `GITHUB_TOKEN` do ambiente (PAT). O flip agora também **fecha** a Issue (`PATCH state=closed` — o merge no GitHub não fecha Issues do Forgejo como o merge nativo da era Forgejo fazia). Falha em qualquer passo = job vermelho (OPS61, nunca mentir); o `forgejo-api.mjs` retenta 410/5xx transitórios (OPS67 + OPS71-FLIP: 410 do proxy retenta em qualquer método — a origem nunca viu o request). + +Branch protection de `main` no **GitHub** (aplicada no cutover do OPS71, via `pnpm configure:branch-protection`): required check `CI (PR) / checks` (a UI exibe workflow/job; o **literal de match é `checks`** — o GitHub casa pelo nome do check-run — pin ao vivo PR #742), `strict=false`, 0 reviews, `enforce_admins: true` (nem admin mergea com CI vermelho — mesmo invariante do rule-based Forgejo). O auto-merge nativo espera esse mesmo check antes de mergear; a regra no servidor é o gate real de qualquer caminho de merge, inclusive API manual. Reaplicar/consertar drift: `pnpm configure:branch-protection` (idempotente). + +Deploy (OPS53 + OPS71): **manual** via `workflow_dispatch` no `deploy.yml` — o job `verify` (hosted, suíte full incl. e2e full) roda **antes** do job `deploy` (`needs: [verify]`), que executa `scripts/deploy-homeserver.sh <sha>` no runner self-hosted do GitHub **no homeserver** (conecta outbound ao GitHub — funciona atrás do Cloudflare tunnel; o hosted nunca toca o homeserver; o self-hosted não conta minutos hosted). O script: HEAD guard (`TEQO_REPO_URL` — default `https://github.com/fsolla/teqo.git`, público) → flock → guard "already deployed" (revision do container) → build do migrator (BuildKit, secrets de `~/stack/teqo-1313.env`) → push/tag em `localhost:5000` → swap dos tags no compose (backup) → **migrations** (`teqo-1313-migrate` — antes do build do runner, OPS66) → build do runner (contra o banco JÁ migrado) → rollout → healthcheck → smoke. Falha = job vermelho; nada é publicado pela metade. Runbook (rollback, falhas conhecidas): `docs/ops/teqo-1313-deploy.md`. O deploy Vercel foi removido no OPS50; o verificador automático de main foi eliminado no OPS71 — se um dia o merge em main quiser rede de regressão, o `ci.yml` volta com 1 linha de trigger (sem deploy). ## Cursor Cloud -`.cursor/environment.json` instala deps + Postgres nativo + `db:seed:minimal`. Sem secrets de prod. Preferir `pnpm push`; escape `git push --no-verify` só no cutover documentado acima. +`.cursor/environment.json` instala deps + Postgres nativo + `db:seed:minimal`. Sem secrets de prod. Preferir `pnpm push`; escape `git push --no-verify` só no cutover documentado acima. PR via `ManagePullRequest` (draft: false) — o merge continua pelo safety net do GitHub. ## Agent pool diff --git a/docs/CHANGELOG-AGENTS.md b/docs/CHANGELOG-AGENTS.md index bbc3d942..3ca0ae4d 100644 --- a/docs/CHANGELOG-AGENTS.md +++ b/docs/CHANGELOG-AGENTS.md @@ -8,6 +8,28 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente **C141 — perfis de permissão por assessor: Visão × Edição configuráveis (2026-08-19):** o coordenador/candidato define por assessor, em `/campanha/assessores` (coluna "Permissão" com badge + popover, seção no detalhe e no draft de novo assessor), o que ele **vê** (`carteira|tudo`) e o que ele **edita** (`carteira|tudo|somente_leitura`) — contas novas e existentes seguem no padrão Carteira·Edita carteira até configurarem. Enforcement fail-closed no servidor: a leitura alarga com Visão "Tudo" via prólogo novo `resolveProfileScopedRead` (municípios, lideranças, updates, pledges, atividades, contatos derivados), a escrita segue o eixo de Edição (`advisorEditingAccess`) nos `canUpdate*`/`canCreate*` de todos os domínios, no check do giro e no create individual de atividade (ambos contra `getWritableMunicipalityIds`), e a ficha de contatos passou a ser autorizada pelo escopo de escrita (`assertPersonContactWritable` — antes era "você edita o que vê", que a Visão "Tudo" alargaria em escrita de PII). Caps mantidos: demandas seguem a regra própria (C143), apoiadores (PII) e telefones de contas ficam na carteira mesmo com Tudo, e escritas de coordenação (contas, `municipality.advisors`, nível de envolvimento E14, `allocationDecision`) continuam restritas. Combinação incoerente (Edição "Tudo" sem Visão "Tudo") é rejeitada no zod, na action e no hook `enforceCoherentAdvisorProfile` (REST/admin incluídos). Migration `20260819_214755_add_campaign_user_advisor_permission_profile` (duas colunas select com default `'carteira'`, sem backfill). +**OPS73 — deploy do fix do admin em produção (2026-08-20):** o `/admin` do Payload (jorgesolla1313.com.br) voltou a renderizar o formulário de login em produção — o bug de "tela branca" que travava o CMS do time editorial/campanha está resolvido e verificado. Durante o diagnóstico descobriu-se que a premissa do plano ("produção nunca recebeu o fix OPS69") estava errada: o deploy das 23:09 de 2026-08-19 (revision `8c734ba`) já continha o fix do importMap **no histórico**, mas o commit S9 (`8d2352c0`, captura de novidades na home) tinha regenerado o importMap **sem** as envs `S3_*`, removendo o handler `@payloadcms/storage-s3/client#S3ClientUploadHandler` e reintroduzindo o erro `getFromImportMap: PayloadComponent not found in importMap` — o admin abria em branco de novo ("funcionou e no deploy seguinte parou"). Verificado também que o **main do GitHub havia divergido do Forgejo** (pós-cutover OPS71): o GitHub tinha o fix + OPS71, mas não as features S8 (seção "Nossa história") e S9 (captura de novidades) que produção já rodava — deployar o main como estava regrediria as duas. A entrega: PR #751 re-homou no GitHub main as features S8+S9 (cherry-pick dos commits aprovados no Forgejo `8d2352c0` + `488dcf7d`), mantendo o handler no importMap (prova: `generate:importmap` com envs `S3_*` dummy → diff vazio vs main); CI verde; aguardando o merge. Deploy pós-merge via o pipeline canônico do OPS71 (dispatch manual do `deploy.yml`: `verify` full → job `deploy` no runner self-hosted recém-instalado no homeserver — 2 tentativas "stale run" por merges paralelos no meio do verify, terceira aplicou `dba75ff8`); migração C143 (responsáveis de demandas, aditiva+backfill) aplicada. Verificação pós-deploy: container na revision `dba75ff8`, healthy, **log sem nenhum `getFromImportMap`**, `/admin` renderizando o formulário de login (2 inputs email/senha + botão Login, título "Login - Payload", 0 erros de console), e a home pública mantendo "Nossa história" (S8) e o form de novidades (S9). Evidência: `docs/plans/ops73-deploy-admin-prod-fixed-evidence.png`. Lições registradas: a regeneração do importMap sem envs `S3_*` re-órfão o handler (classe exata que o guard #87 deve impedir no CI); divergência de plataformas pós-cutover — workspace do homeserver ainda apontava para o Forgejo local (`localhost:3000`) e o S10 (pixel) segue pendente de re-home no GitHub. + +**OPS73 — deploy do fix do admin em produção (2026-08-20):** o `/admin` do Payload (jorgesolla1313.com.br) voltou a renderizar o formulário de login em produção — o bug de "tela branca" que travava o CMS do time editorial/campanha está resolvido e verificado. O diagnóstico mostrou que a premissa do plano (fix nunca tinha chegado a prod) estava errada em dois pontos: (1) o deploy das 23:09 de 2026-08-19 (revision `8c734ba`) já continha o fix do importMap no histórico, mas o commit S9 (`8d2352c0`) regenerou o importMap **sem** as envs `S3_*`, removendo o handler `@payloadcms/storage-s3/client#S3ClientUploadHandler` e reintroduzindo `getFromImportMap: PayloadComponent not found` (admin em branco de novo); (2) o **main do GitHub havia divergido do Forgejo** (pós-cutover OPS71) — o GitHub tinha o fix + OPS71, mas não as features S8 ("Nossa história") e S9 (captura de novidades) que produção já rodava. A entrega (PR #751): re-home do S8+S9 no GitHub main (cherry-pick `8d2352c0`+`488dcf7d`), mantendo o handler no importMap (prova: `generate:importmap` com envs `S3_*` dummy → diff vazio vs main); deploy via `deploy.yml` no runner self-hosted do homeserver (`dba75ff8`), após 2 "stale run" por merges paralelos no verify; migração C143 (responsáveis de demandas, aditiva+backfill) aplicada. Verificação pós-deploy: revision `dba75ff8`, healthy, **log sem `getFromImportMap`**, `/admin` renderizando o login (2 inputs + botão, título "Login - Payload", 0 erros de console), home mantendo S8/S9. Evidência: `docs/plans/ops73-deploy-admin-prod-fixed-evidence.png`. Lições: regeneração do importMap sem envs `S3_*` re-órfão o handler (classe do guard OPS70/#87); divergência de plataformas pós-cutover (workspace homeserver ainda aponta Forgejo local; S10 pixel pendente de re-home). + +**S11** — o feed do Instagram configurado **sem aparecer na home** fica diagnosticável: fim do fail-closed silencioso da S3. A assessoria vê, na mesma global onde preenche o token, o estado da sincronização (`instagramSyncStatus` jsonb hidden na `social-feed-settings`) e o **porquê** de os posts não aparecerem — o painel `InstagramSyncStatusPanel` (campo `ui` irmão do picker) mostra 5 estados fiéis ao rascunho aprovado (desativado / ainda não configurado / falha com motivo em linguagem de produto + "Tentar sincronizar de novo" / sincronizado · há X min · N posts / aguardando 1ª sync). A causa da ocorrência de produção (board vazio com token+ID preenchidos) é agora confirmável em 1 clique após o deploy. Engenharia: `loadInstagramFeed` passou a parsear o body de erro da Graph API e lançar `InstagramApiError` (`status` + `{message,type}`); `describeInstagramError` mapeia para copy pt-BR com a correção — OAuthException/expired → "token inválido/expirado ou emitido via Facebook Login — gere pelo Instagram Login" (o refresh endpoint da Graph API só renova tokens de Instagram Login), "Invalid user id" → "confira o ID da conta Business/Creator", 5xx → indisponível, `TypeError` → rede. Estado escrito por 3 caminhos: render da home (`getInstagramFeed`, dentro do `unstable_cache`, SQL direto sem hooks — padrão anti-`revalidateTag`-no-cache da S2), `afterChange` da global (sync **awaited** quando a credencial IG muda — `instagramCredentialsChanged(doc, previousDoc)`, gate que pula save de exclusões/YouTube; o sync é importado lazy no hook para o `payload.config.ts` não puxar o módulo do feed no load, que é o que todo spec int importa) e a rota `POST /api/social-feed/sync` (botão; `payload.auth` sessão admin exigindo `users` + `isSameOriginRequest` CSRF — allowlistada no spec de convenção de POST routes com razão, é auth de admin ≠ envelope `/campanha`). **Bug real descoberto e corrigido na entrega:** o `afterChange` roda DENTRO da transação do save (a row lock fica presa até os hooks retornarem), então escrever o status/snapshot via pool numa segunda conexão **deadlockava no lock da própria linha** (~80-230s por save, transações-zumbi segurando o lock). A correção roteia os persists do sync-hook pela **mesma transação** (`getPostgresTransactionDatabase` — o helper transacional da C6) com um `req` opcional no `syncInstagramFeed`; o render e a rota sem transação aberta seguem no pool. Diagnóstico reproduzido em `pg_stat_activity` (UPDATE `transactionid` Lock vs `idle in transaction`) e fix verificado (45ms vs 232s). Stub IG ganha estado `invalid-token` (400 OAuthException) para o e2e da cena de falha; runbook `docs/ops/instagram-feed-token-runbook.md` (o que o painel mostra, validação via curl `/me` e `refresh_access_token`, geração pelo Instagram Login, o que NÃO fazer). Migration `20260820_003426_add_instagram_sync_status` (ADD COLUMN jsonb nullable, não destrutiva). **/simplify (3 reviewers):** absorvi a correção P1 de diagnóstico — o mapeamento de ID exigia só "user" (regex frouxo) e mensagens reais de token OAuthException ("the **user** must be logged in", "user changed password") caíam na branch de ID, mostrando a razão errada; passei a exigir "user" E "id" juntos (`isInvalidUserIdMessage`) e adicionei o mapeamento de `AbortError` (timeout → "demorou demais"). Painel: gate passou a respeitar o master `enabled` (master off → "Instagram desativado", nunca "Sincronizado no board"), rótulo do botão no estado de primeira sync ("Tentar sincronizar" vs "de novo"), `outcome` transiente limpo ao editar credencial e `aria-live` nos boxes interativos. Testes: caminho transacional do `syncInstagramFeed` (persist na mesma transação) + fallback pool cobertos por unit; e2e do painel ganhou as cenas fail-500 e "desativado"; cleanup do stub global virou `afterEach`. Débitos registrados: #124 (janela de lock: fetch de rede no sync-hook segura a row lock até 10s, acoplando a home ao save) e #125 (polish P3 — tipo de persist duplicado, rota 200 vs 400, last-writer-wins stale, `5 posts` hardcoded). Verificação: unit 2463 + int 791 verdes; lint/format/tsc/cycles verdes; `pnpm build` ok; e2e local — smoke admin 5/5 (painel ok → falha token → retry → falha 500 → desativado → não configurado) + describe serial frontend 19/19. + +**C143** — demandas visíveis apenas para responsáveis explícitos + candidato/coordenador. Novo campo `campaignDemand.responsibles` (hasMany → `campaignUser`, staff elegível via `eligibleCampaignStaffWhere` com hook `beforeValidate` que descarta ids não-staff, fail-closed) e regra de leitura/atualização por membership: assessor só vê demandas onde é responsável (`{ responsibles: { contains } }`), candidato/coordenador/admin sempre, leader nunca — a regra substitui o escopo municipal de demandas em `src/utilities/access/demands.ts` e todas as superfícies (lista, URL, busca global, demandas de atividade, C90) herdam via `overrideAccess: false`; criação sem responsáveis fica visível só a unrestricted (fail-closed). Criador entra automaticamente como responsável no hook `enforceDemandWorkflow` (cobre form, wizard e rascunhos C90); migration `20260819_233503_add_campaign_demand_responsibles` adiciona `campaign_user_id` a `campaign_demand_rels` (FK cascade) e faz backfill de `created_by_id` → responsável para demandas pré-C143. UI: picker multi-select `DemandResponsibleMultiSelect` (chips com avatar, chip do criador rotulado "(criador)" e travado na criação, sugestões = assessores do município) na criação (`/demandas/nova` em 2 colunas + wizard "Registrar pedido" empilhado) e card de gestão `DemandResponsiblesCard` no detalhe (add/remove com save full-replace + advisory lock, rótulo "Visível para: Responsáveis · Candidato · Coordenador"); busca por `searchDemandResponsibleOptions` (query curta → assessores do município; senão → staff por nome). Testes: int reescritos para a regra nova (workflow/homeSearch) + 4 casos novos (membership, fail-closed sem responsáveis, gestão add/remove/self-removal, responsável fora da carteira, descarte de id não-staff); e2e novo `campaignDemandVisibility.e2e.spec.ts` (assessor do mesmo município não vê lista/URL; hand-off de acesso add/revoke pela UI); fixture e2e ganhou `createCampaignDemand`. Verificação: unit 2444 + int 795 verdes, e2e 5/5 local (visibility + wizard + demanda do municipalidades), lint/format/knip/cycles/tsc/build limpos; importMap regenerado com `S3_*` dummy (OPS69). Nota de deploy: conferir na primeira aplicação em prod `SELECT count(*) FROM campaign_demand_rels WHERE path='responsibles'` = nº de demandas com `created_by_id`. Plano de implementação: `docs/plans/demandas-responsaveis-impl.md`. + +**OPS72** — política de e2e: **CI de PR sem e2e full; local afetado como passo da skill; full só antes do deploy**. Decisão do gate (2026-08-19): (1) **local = discricionário** — o agente roda os e2e que criou + os da mesma superfície afetada, decide quais (`pnpm test:e2e:affected` é a ferramenta, não um gate mecânico; não está no `gate:push`); (2) **CI de PR = blast radius** — `ci-pr.yml` perdeu o step `E2E tests (full suite)`; o e2e roda só em modo `selected` (specs que o classifier detectou como tocadas); diff high-risk (schema/lockfile/harness) classifica `full` e o PR simplesmente não roda e2e — deliberado: o espelho local roda full nesse mesmo diff e o `verify` do deploy roda full antes de publicar; (3) **main = full no deploy** — inegociável, já existia no `deploy.yml` (nada mudou lá). Docs: `work-issue` e `agent-work-issue` (checklist + Passo 4), `execution-pipeline.md` (nova seção "E2E local afetado" com o critério + a **limitação da #72**: `--no-deps` + projetos paralelos colide no `seedTestUser` → `--workers=1` ou a cadeia padrão de projetos), `AGENTS.md` (parágrafo OPS59 → política OPS72), `docs/AGENT-OPS.md` (fluxo do agente + linha do `ci-pr.yml` na tabela), `.agents/rules/agent-pr-workflow.mdc` (descrição do `gate:push`), mensagens informativas do `gate-ci.mjs` (texto-only — e2e continua fora do gate) e comentários do workflow. Mecânica intocada por escopo (`ci-scope.mjs`, manifest, `run-e2e-affected.mjs`); o fix da #72 fica na fila (só documentado). Verificação: format/lint/tsc verdes, unit 2444 + int 791 verdes; o e2e local afetado rodou a suíte **full** (o diff toca `scripts/gate-ci.mjs`, high-risk → modo `full` — a política em ação): 154 passed em dev mode; 8 falhas de ambiente (timeouts/cold-compile sob carga) triadas — 5/8 passaram em re-run e as 3 restantes passaram em `E2E_PROD=1` (o modo do CI/verify; 41 passed + 2 flaky que passaram isoladas), zero regressão de produto; o CI deste PR não roda e2e (modo `full` por design desta mesma política). Impl plan: `docs/plans/ops72-e2e-local-afetado-ci-sem-full-impl.md`. Nota: `E2E_PROD=1` local regenera o `importMap.js` **sem** as envs de S3 (classe OPS69 — o guard do OPS70/#87 cobre; restaurado na entrega com envs dummy). + +**OPS72** — política de e2e: **CI de PR sem e2e full; local afetado como passo da skill; full só antes do deploy**. Decisão do gate (2026-08-19): (1) **local = discricionário** — o agente roda os e2e que criou + os da mesma superfície afetada, decide quais (`pnpm test:e2e:affected` é a ferramenta, não um gate mecânico; não está no `gate:push`); (2) **CI de PR = blast radius** — `ci-pr.yml` perdeu o step `E2E tests (full suite)`; o e2e roda só em modo `selected` (specs que o classifier detectou como tocadas); diff high-risk (schema/lockfile/harness) classifica `full` e o PR simplesmente não roda e2e — deliberado: o espelho local roda full nesse mesmo diff e o `verify` do deploy roda full antes de publicar; (3) **main = full no deploy** — inegociável, já existia no `deploy.yml` (nada mudou lá). Docs: `work-issue` e `agent-work-issue` (checklist + Passo 4), `execution-pipeline.md` (nova seção "E2E local afetado" com o critério + a **limitação da #72**: `--no-deps` + projetos paralelos colide no `seedTestUser` → `--workers=1` ou a cadeia padrão de projetos), `AGENTS.md` (parágrafo OPS59 → política OPS72), `docs/AGENT-OPS.md` (fluxo do agente + linha do `ci-pr.yml` na tabela), `.agents/rules/agent-pr-workflow.mdc` (descrição do `gate:push`), mensagens informativas do `gate-ci.mjs` (texto-only — e2e continua fora do gate) e comentários do workflow. Mecânica intocada por escopo (`ci-scope.mjs`, manifest, `run-e2e-affected.mjs`); o fix da #72 fica na fila (só documentado). Verificação: format/lint/tsc verdes, unit 2444 + int 791 verdes; o e2e local afetado rodou a suíte **full** (o diff toca `scripts/gate-ci.mjs`, high-risk → modo `full` — a política em ação): 154 passed em dev mode; 8 falhas de ambiente (timeouts/cold-compile sob carga) triadas — 5/8 passaram em re-run e as 3 restantes passaram em `E2E_PROD=1` (o modo do CI/verify; 41 passed + 2 flaky que passaram isoladas), zero regressão de produto; o CI deste PR não roda e2e (modo `full` por design desta mesma política). Impl plan: `docs/plans/ops72-e2e-local-afetado-ci-sem-full-impl.md`. Nota: `E2E_PROD=1` local regenera o `importMap.js` **sem** as envs de S3 (classe OPS69 — o guard do #77 cobre; restaurado na entrega com envs dummy). + +**OPS72** — política de e2e: **CI de PR sem e2e full; local afetado como passo da skill; full só antes do deploy**. Decisão do gate (2026-08-19): (1) **local = discricionário** — o agente roda os e2e que criou + os da mesma superfície afetada, decide quais (`pnpm test:e2e:affected` é a ferramenta, não um gate mecânico; não está no `gate:push`); (2) **CI de PR = blast radius** — `ci-pr.yml` perdeu o step `E2E tests (full suite)`; o e2e roda só em modo `selected` (specs que o classifier detectou como tocadas); diff high-risk (schema/lockfile/harness) classifica `full` e o PR simplesmente não roda e2e — deliberado: o espelho local roda full nesse mesmo diff e o `verify` do deploy roda full antes de publicar; (3) **main = full no deploy** — inegociável, já existia no `deploy.yml` (nada mudou lá). Docs: `work-issue` e `agent-work-issue` (checklist + Passo 4), `execution-pipeline.md` (nova seção "E2E local afetado" com o critério + a **limitação da #72**: `--no-deps` + projetos paralelos colide no `seedTestUser` → `--workers=1` ou a cadeia padrão de projetos), `AGENTS.md` (parágrafo OPS59 → política OPS72), `docs/AGENT-OPS.md` (fluxo do agente + linha do `ci-pr.yml` na tabela), `.agents/rules/agent-pr-workflow.mdc` (descrição do `gate:push`), mensagens informativas do `gate-ci.mjs` (texto-only — e2e continua fora do gate) e comentários do workflow. Mecânica intocada por escopo (`ci-scope.mjs`, manifest, `run-e2e-affected.mjs`); o fix da #72 fica na fila (só documentado). Verificação: format/lint/tsc verdes, unit 2444 + int 791 verdes; o e2e local afetado rodou a suíte **full** (o diff toca `scripts/gate-ci.mjs`, high-risk → modo `full` — a política em ação): 154 passed em dev mode; 8 falhas de ambiente (timeouts/cold-compile sob carga) triadas — 5/8 passaram em re-run e as 3 restantes passaram em `E2E_PROD=1` (o modo do CI/verify; 41 passed + 2 flaky que passaram isoladas), zero regressão de produto; o CI deste PR não roda e2e (modo `full` por design desta mesma política). Impl plan: `docs/plans/ops72-e2e-local-afetado-ci-sem-full-impl.md`. Nota: `E2E_PROD=1` local regenera o `importMap.js` **sem** as envs envs de S3 (classe OPS69 — o guard do #77 cobre; restaurado na entrega com envs dummy). + +**OPS72** — política de e2e: **CI de PR sem e2e full; local afetado como passo da skill; full só antes do deploy**. Decisão do gate (2026-08-19): (1) **local = discricionário** — o agente roda os e2e que criou + os da mesma superfície afetada, decide quais (`pnpm test:e2e:affected` é a ferramenta, não um gate mecânico; não está no `gate:push`); (2) **CI de PR = blast radius** — `ci-pr.yml` perdeu o step `E2E tests (full suite)`; o e2e roda só em modo `selected` (specs que o classifier detectou como tocadas); diff high-risk (schema/lockfile/harness) classifica `full` e o PR simplesmente não roda e2e — deliberado: o espelho local roda full nesse mesmo diff e o `verify` do deploy roda full antes de publicar; (3) **main = full no deploy** — inegociável, já existia no `deploy.yml` (nada mudou lá). Docs: `work-issue` e `agent-work-issue` (checklist + Passo 4), `execution-pipeline.md` (nova seção "E2E local afetado" com o critério + a **limitação da #72**: `--no-deps` + projetos paralelos colide no `seedTestUser` → `--workers=1` ou a cadeia padrão de projetos), `AGENTS.md` (parágrafo OPS59 → política OPS72), `docs/AGENT-OPS.md` (fluxo do agente + linha do `ci-pr.yml` na tabela), `.agents/rules/agent-pr-workflow.mdc` (descrição do `gate:push`), mensagens informativas do `gate-ci.mjs` (texto-only — e2e continua fora do gate) e comentários do workflow. Mecânica intocada por escopo (`ci-scope.mjs`, manifest, `run-e2e-affected.mjs`); o fix da #72 fica na fila (só documentado). Verificação: format/lint/tsc verdes, unit 2444 + int 791 verdes; o e2e local afetado rodou a suíte **full** (o diff toca `scripts/gate-ci.mjs`, HIGH*RISK → modo `full` — a política em ação): 154 passed em dev mode; 8 falhas de ambiente (timeouts/cold-compile sob carga) triadas — 5/8 passaram em re-run e as 3 restantes passaram em `E2E_PROD=1` (o modo do CI/verify; 41 passed + 2 flaky que passaram isoladas), zero regressão de produto; o CI deste PR não roda e2e (modo `full` por design desta mesma política). Impl plan: `docs/plans/ops72-e2e-local-afetado-ci-sem-full-impl.md`. Nota: `E2E_PROD=1` local regenera o `importMap.js` **sem** as envs `S3*\*` (classe OPS69 — o guard do #77 cobre; restaurado na entrega com envs dummy). + +**OPS72** — política de e2e: **CI de PR sem e2e full; local afetado como passo da skill; full só antes do deploy**. Decisão do gate (2026-08-19): (1) **local = discricionário** — o agente roda os e2e que criou + os da mesma superfície afetada, decide quais (`pnpm test:e2e:affected` é a ferramenta, não um gate mecânico; não está no `gate:push`); (2) **CI de PR = blast radius** — `ci-pr.yml` perdeu o step `E2E tests (full suite)`; o e2e roda só em modo `selected` (specs que o classifier detectou como tocadas); diff high-risk (schema/lockfile/harness) classifica `full` e o PR simplesmente não roda e2e — deliberado: o espelho local roda full nesse mesmo diff e o `verify` do deploy roda full antes de publicar; (3) **main = full no deploy** — inegociável, já existia no `deploy.yml` (nada mudou lá). Docs: `work-issue` e `agent-work-issue` (checklist + Passo 4), `execution-pipeline.md` (nova seção "E2E local afetado" com o critério + a **limitação da #72**: `--no-deps` + projetos paralelos colide no `seedTestUser` → `--workers=1` ou a cadeia padrão de projetos), `AGENTS.md` (parágrafo OPS59 → política OPS72), `docs/AGENT-OPS.md` (fluxo do agente + linha do `ci-pr.yml` na tabela), `.agents/rules/agent-pr-workflow.mdc` (descrição do `gate:push`), mensagens informativas do `gate-ci.mjs` (texto-only — e2e continua fora do gate) e comentários do workflow. Mecânica intocada por escopo (`ci-scope.mjs`, manifest, `run-e2e-affected.mjs`); o fix da #72 fica na fila (só documentado). Verificação: format/lint/tsc verdes, unit 2444 + int 791 verdes; o e2e local afetado rodou a suíte **full** (o diff toca `scripts/gate-ci.mjs`, HIGH_RISK → modo `full` — a política em ação): 154 passed em dev mode; 8 falhas de ambiente (timeouts/cold-compile sob carga) triadas — 5/8 passaram em re-run e as 3 restantes passaram em `E2E_PROD=1` (o modo do CI/verify; 41 passed + 2 flaky que passaram isoladas), zero regressão de produto; o CI deste PR não roda e2e (modo `full` por design desta mesma política). Impl plan: `docs/plans/ops72-e2e-local-afetado-ci-sem-full-impl.md`. + +**OPS72** — política de e2e: **CI de PR sem e2e full; local afetado como passo da skill; full só antes do deploy**. Decisão do gate (2026-08-19): (1) **local = discricionário** — o agente roda os e2e que criou + os da mesma superfície afetada, decide quais (`pnpm test:e2e:affected` é a ferramenta, não um gate mecânico; não está no `gate:push`); (2) **CI de PR = blast radius** — `ci-pr.yml` perdeu o step `E2E tests (full suite)`; o e2e roda só em modo `selected` (specs que o classifier detectou como tocadas); diff high-risk (schema/lockfile/harness) classifica `full` e o PR simplesmente não roda e2e — deliberado: o espelho local roda full nesse mesmo diff e o `verify` do deploy roda full antes de publicar; (3) **main = full no deploy** — inegociável, já existia no `deploy.yml` (nada mudou lá). Docs: `work-issue` e `agent-work-issue` (checklist + Passo 4), `execution-pipeline.md` (nova seção "E2E local afetado" com o critério + a **limitação da #72**: `--no-deps` + projetos paralelos colide no `seedTestUser` → `--workers=1` ou a cadeia padrão de projetos), `AGENTS.md` (parágrafo OPS59 → política OPS72), `docs/AGENT-OPS.md` (fluxo do agente + linha do `ci-pr.yml` na tabela), `.agents/rules/agent-pr-workflow.mdc` (descrição do `gate:push`), mensagens informativas do `gate-ci.mjs` (texto-only — e2e continua fora do gate) e comentários do workflow. Mecânica intocada por escopo (`ci-scope.mjs`, manifest, `run-e2e-affected.mjs`); o fix da #72 fica na fila (só documentado). Verificação: format/lint/tsc/unit verdes; `test:e2e:affected` com modo `none` (diff sem blast radius e2e); CI do PR valida (e2e não roda — docs/skills/workflow não são superfície e2e). Impl plan: `docs/plans/ops72-e2e-local-afetado-ci-sem-full-impl.md`. + +**OPS71-INFRA (2026-08-19):** runner **self-hosted do GitHub instalado no homeserver** (Issue #113) — o job `deploy` do `deploy.yml` (dispatch manual) agora roda localmente no homeserver, sem SSH. Detalhes: `~/actions-runner` v2.336.0 linux-x64 (sha256 conferido contra a API de releases), nome `teqo-1313-runner`, labels `self-hosted` + `homeserver`, registrado com o PAT do OPS71 (registration token TTL 1h gerado e aplicado na sequência). Roda como **systemd user** `actions.runner.fsolla-teqo.teqo-1313-runner.service` (linger já ativo; `svc.sh install` exige root — unit user criado manualmente do template oficial), usuário `fsolla` (docker + `~/stack` envs), workdir `_work`, outbound-only (funciona atrás do Cloudflare tunnel). Verificado `online` na API do GitHub com as labels corretas. Registrado em infra-solla `STATE.md` (seção Teqo: runner + fluxo deploy manual atualizado; linha do repo/CI corrigida para o pós-cutover GitHub). Workspace `~/teqo-deploy` re-aponta para o GitHub no primeiro deploy via `set-url` idempotente do script. Pendência registrada: secret `CURSOR_API_KEY` no GitHub fica dormente (helper `archive-cursor-agent.yml`; a key não existe no ambiente — pool morto OPS65) até o usuário pedir a reativação. + **S9 — captura de novidades da campanha na home pública (2026-08-19):** a home de campanha (`jorgesolla1313.com.br`) ganhou a seção "Receba as novidades da campanha" logo acima do rodapé — nome + WhatsApp obrigatórios, e-mail/estado/cidade/comentário opcionais, toggle pré-selecionado "Quero fazer parte do time" (frequência + grupos de WhatsApp vs. comunicações esporádicas) e confirmação in-place com atalho para as bandeiras. O CTA secundário do hero virou o atalho da seção (âncora `#novidades`, scroll suave no container da home; "Conhecer bandeiras" sai, as bandeiras seguem no rodapé e no estado de sucesso). O registro é `Contact` + `Subscription` em transação com consentimento NOVO por chave estável `campanha-novidades` (fail-closed: sem o texto aprovado no admin, o form recusa a captura — nada é gravado); a escolha do toggle sobrevive como `subscription.campaignLevel` (`time|esporadico`, coluna própria na lista do admin). Migração `20260819_192605_add_campaign_newsletter_capture` (DROP NOT NULL em `contact.state` — estado opcional no form, ficha staff via zod segue exigindo — + coluna `campaign_level` com enum). Precedente `submitWhatsapp` preservado; action nova usa `withPayloadTransaction` (o dono do concern de transação, não o hand-roll gêmeo — `submitWhatsapp`/`submitPetitionSignature` ficam como débito de alinhamento). S10 (pixel Meta) dispara `Lead` na costura do sucesso do form. Chave de consentimento pendente de aprovação jurídica antes da ativação. **S7** — polimento dos cards da seção "Acompanhe de perto" da home de campanha (jorgesolla1313.com.br), look editorial e direto no lugar do "card de app": cantos retos (**sem `rounded-xl` no card e `rounded-lg` na capa**), capa de **borda a borda** (o `p-3` saiu do contêiner do card e foi para o bloco de texto — título/subtítulo/meta continuam respirando `p-3` das bordas; `mt-auto` da meta e `overflow-hidden` do zoom hover intactos) e, por decisão do humano posterior ao gate, **sem borda (`border --campaign-line`) e sem sombra** (`shadow-sm`/`hover:shadow-md`) — o card branco lê contra a banda do S5 só pela forma. **Chips de badge removidos de todos os cards** (decisão B do gate): nem os tipos de post do site (`POST_TYPE_BADGE_LABELS` deletado de `src/utilities/posts.ts`, junto com o describe no unit test) nem as origens externas YouTube/Instagram (sinalização externa segue por `target="_blank"`/`rel="noopener"`); `badgeLabel` saiu do tipo `CampaignContentCardData`. Superfície única no componente dono (`CampaignContentCard.tsx`) — bento e carrossel herdam; `ContentShareButton` (S4), banda do S5, carrossel e fail-closed da seção intocados. Zero migration/schema/Consent. e2e do describe da seção atualizados para o novo estado: loop de badges → ausência (`toHaveCount(0)` para os 4 tipos + YouTube/Instagram), pins CSS do card (`border-radius`/`border-width`/`box-shadow` + raio da capa) e pin geométrico da capa de borda a borda (boundingBox do card × capa). Verificação: gate:fast verde, unit 2415 + int 791 verdes, e2e frontend 19/19 local, format/knip/cycles limpos. Plano de implementação: `docs/plans/cards-acompanhe-de-perto-polimento-impl.md`. @@ -42,10 +64,16 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente **PUB5** — slogan da campanha "Mais saúde mais futuro" no hero e no rodapé da home (jorgesolla1313.com.br), no lugar de "Um mandato do tamanho da Bahia" (discurso do mandato vigente; o novo é da campanha 2026 — saúde é o tema-âncora do candidato). Troca de copy cirúrgica, sem redesenho nem migration: h1 do hero em duas linhas uppercase "MAIS SAÚDE / MAIS FUTURO" com `aria-label` acompanhando ("MAIS SAÚDE MAIS FUTURO"), segunda linha já em `font-black` (anatomia existente), e o rodapé em sentence case com ponto ("Mais saúde mais futuro."). Testes que pinavam a string antiga atualizados (`campaignHero.unit`, `campaignHome.unit`, `frontend.e2e`). Fora de escopo decidido no gate: cabeçalho de artigos ("O mandato do tamanho da Bahia") e docs históricos. Verificação: unit 2269 + int 791 verdes, e2e frontend 8/8 em prod mode (`E2E_PROD=1`, `.next-e2e`), lint/format/typecheck/cycles/knip/build verdes; render conferido por screenshot + DOM (hero duas linhas, rodapé com ponto). `/simplify` 3 reviewers: sem P0/P1; P2 corrigidos no impl plan (redação + line refs); débitos descartados por serem convenções pré-existentes ou decisões do gate. +**Recently resolved (2026-08-20):** **OPS71-FLIP** — flip pós-merge falhou/silencioso no fluxo GitHub, validado e endurecido (Issue #119, achado ao vivo no merge do PR #746). **Investigação (GitHub API pública):** o `closed` do PR #746 (merge por `github-actions[bot]`, auto-merge armado com o `GITHUB_TOKEN` nativo) **não criou run nenhum** — anti-recursão documentada do GitHub (eventos acionados por ações do `GITHUB_TOKEN` não geram runs; o `closed` fica no timeline com 0 runs; comparação com o #742, mergeado por `fsolla`, cujos runs nasceram 3s após o merge). O 410 do run do #742 é falha separada (API do Forgejo respondeu 410 Gone nginx, transitório). **Endurecimento:** (1) **fix de raiz** — o arm do auto-merge (`agent-pr-ready-automerge.yml` → `scripts/github-pr-automerge.mjs`) agora usa o secret novo **`AUTOMERGE_PAT`** (PAT real do fsolla; decisão pura `automergeArmingToken` em `github-pr-flow.mjs` com fail-closed — sem o secret o job sai 1, nunca fallback pro bot token; o merge passa a ser atribuído a usuário real e o `closed` volta a gerar runs); (2) **recuperação manual** — `workflow_dispatch` com input `pr` nos dois flips (`issue-done-on-main-merge.yml`, `plan-issue-ready-on-main-merge.yml`): o job busca o PR via `gh` com o mesmo contrato do evento (merged + base main + same-repo, senão skip) e repassa o body como `PR_BODY`; (3) **close da Issue** — `forgejo-issue-transition.mjs` agora `PATCH state=closed` via novo `closeIssue` do `forgejo-api.mjs` (era-Forgejo o merge fechava nativo; era-GitHub o PR não toca o tracker — antes, toda Issue mergeada ficava `OPEN` com `done`+`in-prod`); (4) **410 no retry** — `forgejo-api.mjs` retenta **410 em qualquer método** (proxy respondeu antes da origem — sem efeito duplicado; 5xx segue fail-closed em escrita) e corpo nginx vira marker compacto `corpo não-JSON (proxy/nginx…)` no erro. **Testes:** 8 specs novas (retry 410 GET/write, marker após 410 esgotado, `closeIssue` PATCH pin, `automergeArmingToken` ok/fail-closed/vazio) — 50 unit verdes nas specs tocadas. **Docs:** `AGENTS.md` (frase do auto-merge com o PAT + porquê), `docs/AGENT-OPS.md` (tabelas de workflows/secrets + runbook de recuperação manual com fallback local `PR_BODY`). Sem migration/access/Consent/UI. Impl: [`ops71-flip-pos-merge-falhou-silencioso-impl.md`](plans/ops71-flip-pos-merge-falhou-silencioso-impl.md). + **Recently resolved (2026-08-19):** **OPS66** — deadlock de migrations em caminho estático: o deploy buildava o runner **antes** do migrate e, se uma migration criasse tabela lida em geração estática (caso S2, 2026-08-18: `social_feed_settings` na home), o `next build` falhava para sempre (`relation ... does not exist`) e o migrate nunca rodava — 4 deploys seguidos vermelhos até o migrator manual. Fix em `scripts/deploy-homeserver.sh` (reordenação de fases, sem custo novo — migrate e build do migrator já rodavam em todo deploy): **build do migrator** (o estágio não roda `next build` — builda contra o schema antigo) → push/tag do migrator → swap do compose (backup + trap na mesma posição) → **migrate** via `docker compose run teqo-1313-migrate` (caminho canônico do serviço de maintenance, agora com a imagem do SHA novo) → **build do runner** contra o banco JÁ migrado → push/tag → rollout → healthcheck → smoke. Alternativas rejeitadas: `payload migrate` dentro do build do runner (efeito colateral de escrita em prod a partir de um processo de build; contraria o desenho deliberado do Dockerfile) e `docker run` direto do migrator com env-file (duplicaria a definição do serviço de maintenance — caminho paralelo ao canônico). Caveat do rollback ajustado no runbook: migrate antes do build do runner deixa o banco à frente do código se o build falhar depois (mesma classe do caveat atual — migrations append-only e revisadas). Testes: `tests/unit/deployScript.unit.spec.ts` com pin de ordem completo (migrator build < swap < migrate < runner build < rollout) + invariante novo do Dockerfile (estágio migrator nunca roda `next build`). Runbook `docs/ops/teqo-1313-deploy.md` atualizado (fluxo, caveat, falha conhecida nova). Dockerfile/AGENTS.md com a precisão build-after-migrate. **Recently resolved (2026-08-19):** **OPS68** — fechamento da parte 2 do OPS50: reescrita dos docs canônicos operacionais que ainda descreviam a era GitHub/Vercel. **12 arquivos, 78+/82−, edições linha-a-linha:** `docs/AGENT-OPS.md` (norte "só Forgejo + Cursor"; fonte canônica = Issues do Forgejo; fluxo do agente e Contrato de PR sem `gh` — criação via API/MCP + merge pelo safety net `agent-pr-ready-automerge.yml`, o único que espera o rollup `CI (PR) / checks`; "Só humano" = envs de `~/stack/teqo-1313.env` + branch protection + runbook; tabela de secrets sem `POOL_GITHUB_TOKEN` (legado — o script o ignora) e com nota de pool dormente; linha `archive-cursor-agent.yml` na tabela CI; "smoke Neon stage" → sem smoke stage), `README.md` (cheatsheet de 5 linhas reescrito: claim → push → PR via API/MCP → safety net mergea → ci.yml em janela de 30 min + deploy homeserver; Neon → homeserver Postgres; `PROD_DATABASE_URL` = `DATABASE_URL` de `~/stack/teqo-1313.env`; migrações no deploy via `teqo-1313-migrate`; "GitHub mirror" → Forgejo Actions; papéis `coordinator/advisor/leader/candidate`; "electoral nuclei" → municípios; Issues → link Forgejo), `docs/roadmap.md` (só o ponteiro da fonte canônica — o stub continua congelado), `AGENTS.md` (refs Neon → homeserver `teqo_1313`; job do gate `check`→`gate`, key real do YAML; "Vercel preview" → host genérico na rationale do WebAuthn), `.agents/rules/agent-pr-workflow.mdc` (comandos `gh pr create/merge/checks` morrem — `gh` não existe em nenhuma máquina (OPS50); regra descreve API/MCP + safety net; "There is no deploy from CI" corrigido — OPS53 colocou o deploy no `ci.yml`), e as skills `work-issue` (SKILL + `execution-pipeline`), `plan-issue` (frontmatter, gate "nada no Forgejo", dedup via `pnpm issue all`), `project-status`, `capture-review-debts`, `engineering-audit` (`gh issue list` → `pnpm issue all`/API; `issue-done-on-stage-merge.yml`→`on-main`; merge `stage`→`main`), `worktree-next-issue` ("resto do Forgejo") e `agent-pool` (descrição Forgejo Actions; workflow marcado removido OPS65; estado em `pool-state.json`, não mais variables). **Mantido como registro histórico** (exclusões explícitas do plano): menções de Vercel/Neon/POOL_GITHUB que documentam remoções (AGENTS.md L15/L105/L167, AGENT-OPS L94/L98, "Vercel/Git-integration deploy gate" como guardrail no rule). Fonte canônica única: `git.solla.dev/fsolla/teqo`. Sem migration/access/Consent/UI. Gate: grep de saída zero nas classes `gh pr`/`gh issue`/`github.com/fsolla`/`GitHub Issues`/`GitHub mirror`/`POOL_GITHUB`/Neon-em-uso; docs-only → CI roda os docs-guards. +**Recently resolved (2026-08-19):** **OPS71** — CI/PR/deploy do Forgejo Actions de volta para o **GitHub Actions**, com o **tracker de Issues permanecendo no Forgejo** (labels, claims, `pnpm issue`/`agent:*` e flips pós-merge continuam via API do Forgejo; o GitHub é só o host do Actions). O PR CI (`ci-pr.yml`, job único `checks` — OPS62) roda no GitHub com a mesma cascata (guards → lint → format → typecheck → knip → cycles → unit → int → build → e2e selected/afetado via `ci-scope.mjs`), com `concurrency` nativo cancelando o run em push novo e `if:` same-repo (repo público — fork não queima minutes). Seu check-run `CI (PR) / checks` é o required check do `main` e o gate do **auto-merge nativo** (`agent-pr-ready-automerge.yml` → `scripts/github-pr-automerge.mjs`: draft `cursor/*` → Ready via PATCH, arm `enablePullRequestAutoMerge` GraphQL com REBASE — o servidor espera o required check verde; sem poll; o pin OPS64 "rollup pode mentir" é estrutural aqui: job único posta um check-run honesto). **`ci.yml` (verificador de main) eliminado** — publicar é **dispatch manual** (`deploy.yml`): `verify` (hosted, suíte full incl. e2e full) → `deploy` (`needs`, `runs-on: [self-hosted, homeserver]`) executa `scripts/deploy-homeserver.sh <sha>` **localmente** no runner do GitHub instalado no homeserver (sem SSH; outbound-only; o hosted nunca toca o homeserver); o script re-aponta o workspace `~/teqo-deploy` para `TEQO_REPO_URL` (default `https://github.com/fsolla/teqo.git`, público). `pnpm push` passa a empurrar para `origin` = **GitHub** (cutover manual de remote documentado; o repo Forgejo congela em main, tracker vivo por API). Novos helpers plain-Node zero-dep: `scripts/lib/github-api.mjs` (REST+GraphQL, retry OPS67), `scripts/lib/github-pr-flow.mjs` (decisão draft/veto/skip, pura), `scripts/lib/github-branch-protection.mjs` (regra desejada + drift, `enforce_admins: true`), `scripts/github-pr.mjs` (criação de PR Ready — nunca draft), `scripts/configure-branch-protection.mjs` reescrito para a REST do GitHub; `forgejo-issue-transition.mjs` aceita `PR_BODY` (body do PR via evento GitHub; flip continua no Forgejo por `FORGEJO_API_TOKEN`). **Cutover em duas fases (gate):** nada do Forgejo foi apagado (`.forgejo/workflows/`, `forgejo-pr-automerge.mjs`, `forgejo-dispatch.mjs`, `ci-classify-production.mjs` preservados como rollback) — a remoção é entrega sucessora pós-validação. Testes: `githubApi` (10), `githubBranchProtection` (10), `githubPrFlow` (9) unit novos. Docs: `AGENTS.md`, `docs/AGENT-OPS.md`, `.agents/rules/agent-pr-workflow.mdc`, `work-issue/execution-pipeline.md`, `agent-work-issue`, `capture-review-debts`, runbook `docs/ops/teqo-1313-deploy.md` (dispatch manual, runner no homeserver, cutover com ordem, rollback). + +**Recently resolved (2026-08-19):** **OPS71 Fase 2** — remoção dos arquivos do fluxo Forgejo após validação ao vivo do GitHub Actions (PRs #742/#743: CI verde, auto-merge nativo, flips via API). **14 arquivos deletados:** `.forgejo/workflows/` (6 — ci-pr.yml, ci.yml, agent-pr-ready-automerge.yml, issue-done-on-main-merge.yml, plan-issue-ready-on-main-merge.yml, archive-cursor-agent.yml), `scripts/forgejo-pr-automerge.mjs`, `scripts/forgejo-dispatch.mjs`, `scripts/ci-classify-production.mjs`, `scripts/lib/dockerignore.mjs`, `scripts/lib/branch-protection.mjs` e as specs órfãs `ciClassifyProduction`/`dockerignoreMatch`/`branchProtection` `.unit.spec.ts`. **Consumidores funcionais achados pela varredura (fora do escopo literal do body) e ajustados na mesma mudança:** `scripts/lib/test-affected-core.mjs` perdeu as 2 entries de `HIGH_RISK_EXACT` dos scripts mortos; `tests/unit/ciSkipInvariants.unit.spec.ts` tirou os 2 `.forgejo/workflows/*` do loop `.next-e2e`, os 2 paths do pin de high-risk e **reescreveu o teste da janela de 30 min (OPS65) para o invariante vivo da era GitHub**: `deploy.yml` é `workflow_dispatch`-only (sem `push:`/`schedule:` — deploy nunca automático) e `ci-pr.yml` não referencia o gate morto. **Docs:** `AGENTS.md` (frase "preservados até Fase 2" → removidos; rollback = revert do PR ou clone do main congelado do Forgejo), runbook `docs/ops/teqo-1313-deploy.md` (Rollback do cutover idem), `README.md` (cheatsheet de 5 linhas era-Forgejo com janela de 30 min + deploy automático → era-GitHub: auto-merge nativo + deploy manual dispatch), comentários com referência morta em `scripts/gate-ci.mjs`, `scripts/github-pr-automerge.mjs` e `.github/workflows/agent-pr-ready-automerge.yml`. **Intocados (anti-goal tracker):** `forgejo-api.mjs`, `agent-*.mjs`, `issue.mjs`, `forgejo-issue-transition.mjs`, `agent-promote-related-on-merge.mjs`, `.forgejo/worktree.env` (consumido pelo provisionamento de worktree), `.dockerignore` (o arquivo — `docker build` do deploy o consome; só o lib de parse morreu). `docs/AGENT-OPS.md` já era era-GitHub (zero referências). Histórico (plans/changelogs antigos) congelado. Sem migration/access/Consent/UI. Gate: tsc/lint/format/knip/cycles/unit+int/build verdes; grep de saneamento `forgejo-pr-automerge|forgejo-dispatch|ci-classify|dockerignore.mjs|branch-protection.mjs` → só histórico. + **Recently resolved (2026-08-18):** **OPS52** — a media saiu do Vercel Blob para o Garage S3 do homeserver (bucket `teqo-media` + key `teqo-key`, já existentes no Garage v2.1.0). O plugin `@payloadcms/storage-vercel-blob` (e o `@vercel/blob` do seed) foi trocado por `@payloadcms/storage-s3` na config, no padrão R2 da doc do Payload (`endpoint` + `forcePathStyle: true`, region `garage`). **Decisão de arquitetura (revisão A→B durante a execução, gate com o humano):** o Garage v2.1.0 **não suporta acesso anônimo na API S3** (sem `--public-read`; source: "when we support anonymous access in the future"; testado: `anonymous`/`anon` → `NoSuchAccessKey`), então o bucket fica **privado** e a app **serve a media por proxy** — sem `disablePayloadAccessControl`, o `staticHandler` do plugin streama o objeto do Garage em `/api/media/file/<filename>` (range + etag/304 nativos) — o **mesmo contrato de URL relativa que as 40 rows pré-existentes já têm** (a recuperação dos arquivos é a Issue #10/OPS52-media). **Fail-closed:** `resolveS3StorageEnv` (`src/utilities/mediaStorage.ts`, unit-testado) — envs `S3_*` parciais → throw no boot com a lista exata de faltantes; nenhuma → storage local (dev/test). Envs novas: `S3_BUCKET`, `S3_ENDPOINT`, `S3_REGION` (default `garage`), `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY` (`.env.example`, `.env.test`, worktree.mjs com cópia all-or-nothing). Seed de posts: pre-delete do Blob removido (o S3 sobrescreve o key determinístico nativamente — idempotência intacta). Prova de contrato real: PUT/GET/DELETE via SDK contra o Garage (tailnet `http://100.119.220.31:3900`), depois media criada pelo Payload local com S3 ativo → `media.url` relativa + `GET /api/media/file/...` → 200 image/png; bucket devolvido a 0 objects e `.env.local` do worktree limpo (sem credenciais de prod). Sem migration (config-only; importMap regenerado sem `VercelBlobClientUploadHandler`). Runbook de cutover registrado no impl plan: setar as 5 `S3_*` no `~/stack/teqo-1313.env` (endpoint tailnet) antes do próximo deploy (OPS53). Repo: `docs/plans/ops52-blob-vercel-para-garage-impl.md`; infra-solla `STATE.md` atualizado à parte. **Recently resolved (2026-08-18):** **OPS52-media** — os objetos de media de produção (as ~40 capas dos posts, que existiam como rows com URL relativa `/api/media/file/<filename>` mas sem arquivo em lugar nenhum) são recuperáveis do WordPress de origem com `pnpm media:recover` (`scripts/recover-media.mjs`, novo): lê as rows de `media` + `post.coverImage` (mapeamento exato row→slug; fallback pelo filename determinístico `<slug>.<ext>`), resolve a capa com a MESMA lógica do seed (featured media do REST `_embed`, senão 1ª imagem inline do corpo — só 7/43 posts têm featured), baixa e faz `PutObject` no bucket das envs `S3_*` com key = `filename` da row, **sem nunca escrever no DB** (rows e URLs intocadas; o staticHandler do Payload passa a servir). Extraído para `scripts/lib/wpArticles.mjs` o fetch ao vivo do WordPress (seed-posts agora importa de lá — sem twin; unit-testado). Fail-closed: exige as 4 `S3_*` completas (`resolveS3StorageEnv` reusado; recuperação nunca vai pro storage local), `ALLOW_REMOTE_DB=true` explícito para DB remoto + echo do alvo (DB/bucket/endpoint), e timeouts curtos no SDK (`@smithy/node-http-handler`, 5s/15s) — endpoint inalcançável falha em segundos em vez de pendurar (a rota pública de media de prod pendurava >25s; evidência de possível falta de rota container→Garage, diagnosticada no runbook do impl plan). Modos: reconcile (padrão, upload idempotente por overwrite), `--dry-run` (plano + `headObject`), `--verify` (HEAD da URL pública por filename — o aceite da Issue, sem credencial). Runbook de execução no homeserver: `docs/plans/ops52-media-recuperar-arquivos-impl.md`. diff --git a/docs/TESTING.md b/docs/TESTING.md index a0d82450..d64ca4fb 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -8,9 +8,9 @@ Three layers, all local-database-only (see `local-database` skill; tests refuse - **Integration** (`tests/int/**/*.int.spec.ts`, 71 specs): real Payload + Postgres (`teqo_test`); campaign fixtures allocate seeded municipalities via a Postgres sequence so parallel spec files never share one, and purge residue on claim. Since the miss #73 guardrail (Pass 4), specs that mutate municipalities must use the allocator (no pinned slugs) — enforced by `testMunicipalityAllocatorConventions`. Shared consent rows with stable keys are leased (`tests/helpers/testDatabaseLease.ts`), never owned. - **E2E** (`tests/e2e`, Playwright, 17 specs / 51 cases (Pass 5, 2026-08-03; was 48 at Pass 4)): smoke-level; most campaign behavior is pinned at the int layer. -CI (`ci-pr.yml`) is a **single sequential job `checks`** (OPS62 X1): guards → lint (zero warnings) → format → typecheck → knip → cycles → unit/int (**affected by the PR diff**: `vitest --changed` against the PR base via `scripts/test-affected.mjs`, full-suite fallback on high-risk paths) → build → e2e (manifest path→spec via `scripts/e2e-affected.mjs` + `scripts/lib/e2e-affected-manifest.mjs`, blocking; production build + `list`/`github` reporters in CI) — against two isolated Postgres 17 services (int / build+e2e) with **`db:seed:minimal`**. Fail-fast is structural: a failed step stops the run; e2e runs with `PLAYWRIGHT_WORKERS=4`. Push to `main` (`ci.yml`) always runs the full net including e2e. +CI (`ci-pr.yml`) is a **single sequential job `checks`** (OPS62 X1): guards → lint (zero warnings) → format → typecheck → knip → cycles → unit/int (**affected by the PR diff**: `vitest --changed` against the PR base via `scripts/test-affected.mjs`, full-suite fallback on high-risk paths) → build → e2e (manifest path→spec via `scripts/e2e-affected.mjs` + `scripts/lib/e2e-affected-manifest.mjs`, blocking; **only in mode `selected` — OPS72: the PR never runs full e2e**; production build + `list`/`github` reporters in CI) — against two isolated Postgres 17 services (int / build+e2e) with **`db:seed:minimal`**. Fail-fast is structural: a failed step stops the run; e2e runs with `PLAYWRIGHT_WORKERS=4`. There is no main verifier (`ci.yml` eliminated in OPS71): the full net including e2e runs in the deploy `verify` job before publishing (manual dispatch). -**Local e2e before push (same blast radius as PR CI):** `pnpm test:e2e:affected` — runs `e2e-affected.mjs` against `origin/stage`, migrates/seeds, then `pnpm test:e2e` with the selected specs (or the full suite on high-risk diffs). Forward extra Playwright args after `--` for a narrowed repro. Opt into production mode (after fixing the known municipalities-list prod client bug) with `E2E_PROD=1 pnpm test:e2e:affected`. +**Local e2e (OPS72 — skill step, discretionary):** before push, run the e2e specs you created + the same affected surface — `pnpm test:e2e:affected` runs `e2e-affected.mjs` against `origin/main`, migrates/seeds, then `pnpm test:e2e` with the selected specs (full suite on high-risk diffs). High-risk diffs diverge from PR CI by design: the PR runs no e2e (selected only, never full) and the local mirror runs the full suite. Forward extra Playwright args after `--` for a narrowed repro. Opt into production mode with `E2E_PROD=1 pnpm test:e2e:affected` (CI and prod mode serve the production build via `pnpm start`; plain local runs use `pnpm dev` — dev-mode flakes like cold-compile timeouts and dev-only console warnings do not reproduce in prod mode). **Navigation discipline in e2e (miss #54, 2026-07-30):** `page.goto` is for cold loads and URL contracts (deep links, redirects, legacy params); when the scenario is _continuity inside the authenticated shell_ — especially after a heavy list route — navigate in-shell instead (sidebar links, buttons, form submits), the way the user does. Never fire two `page.goto` back-to-back with nothing settling the first navigation (an `await expect(...)`, `waitForURL`, or interaction): the second aborts the first's in-flight RSC fetch (`net::ERR_ABORTED`, measured in `campaignSavedFilters`). The `e2eNavigationConventions` unit guard fails the build on unsettled goto pairs. Two sibling rules of the same family: wait for **interactive state** before clicking dynamic controls — `toBeVisible` proves SSR paint, not hydration (pre-hydration clicks no-op silently; `checkRadixWhenHydrated` is the precedent, P3-C class) — and gate WebAuthn ceremonies on `expectCampaignBiometricsReady` before the island's one-shot probe runs (miss #53). **Dev-mode caveat (closed 2026-08-10, P4-L/OPS30):** `campaignLeaderships`' chip-cell spec was failing deterministically under `pnpm dev` before B34 rewrote it; with the OPS30 prewarm/login hardening it ran ×3 green in dev + ×1 `E2E_PROD=1` (evidence in TECH-DEBT). diff --git a/docs/changelog/2026-08-19-c143.md b/docs/changelog/2026-08-19-c143.md new file mode 100644 index 00000000..300da9a1 --- /dev/null +++ b/docs/changelog/2026-08-19-c143.md @@ -0,0 +1 @@ +**C143** — demandas visíveis apenas para responsáveis explícitos + candidato/coordenador. Novo campo `campaignDemand.responsibles` (hasMany → `campaignUser`, staff elegível via `eligibleCampaignStaffWhere` com hook `beforeValidate` que descarta ids não-staff, fail-closed) e regra de leitura/atualização por membership: assessor só vê demandas onde é responsável (`{ responsibles: { contains } }`), candidato/coordenador/admin sempre, leader nunca — a regra substitui o escopo municipal de demandas em `src/utilities/access/demands.ts` e todas as superfícies (lista, URL, busca global, demandas de atividade, C90) herdam via `overrideAccess: false`; criação sem responsáveis fica visível só a unrestricted (fail-closed). Criador entra automaticamente como responsável no hook `enforceDemandWorkflow` (cobre form, wizard e rascunhos C90); migration `20260819_233503_add_campaign_demand_responsibles` adiciona `campaign_user_id` a `campaign_demand_rels` (FK cascade) e faz backfill de `created_by_id` → responsável para demandas pré-C143. UI: picker multi-select `DemandResponsibleMultiSelect` (chips com avatar, chip do criador rotulado "(criador)" e travado na criação, sugestões = assessores do município) na criação (`/demandas/nova` em 2 colunas + wizard "Registrar pedido" empilhado) e card de gestão `DemandResponsiblesCard` no detalhe (add/remove com save full-replace + advisory lock, rótulo "Visível para: Responsáveis · Candidato · Coordenador"); busca por `searchDemandResponsibleOptions` (query curta → assessores do município; senão → staff por nome). Testes: int reescritos para a regra nova (workflow/homeSearch) + 4 casos novos (membership, fail-closed sem responsáveis, gestão add/remove/self-removal, responsável fora da carteira, descarte de id não-staff); e2e novo `campaignDemandVisibility.e2e.spec.ts` (assessor do mesmo município não vê lista/URL; hand-off de acesso add/revoke pela UI); fixture e2e ganhou `createCampaignDemand`. Verificação: unit 2444 + int 795 verdes, e2e 5/5 local (visibility + wizard + demanda do municipalidades), lint/format/knip/cycles/tsc/build limpos; importMap regenerado com `S3_*` dummy (OPS69). Nota de deploy: conferir na primeira aplicação em prod `SELECT count(*) FROM campaign_demand_rels WHERE path='responsibles'` = nº de demandas com `created_by_id`. Plano de implementação: `docs/plans/demandas-responsaveis-impl.md`. diff --git a/docs/changelog/2026-08-19-ops71-fase2.md b/docs/changelog/2026-08-19-ops71-fase2.md new file mode 100644 index 00000000..31c7dded --- /dev/null +++ b/docs/changelog/2026-08-19-ops71-fase2.md @@ -0,0 +1 @@ +**Recently resolved (2026-08-19):** **OPS71 Fase 2** — remoção dos arquivos do fluxo Forgejo após validação ao vivo do GitHub Actions (PRs #742/#743: CI verde, auto-merge nativo, flips via API). **14 arquivos deletados:** `.forgejo/workflows/` (6 — ci-pr.yml, ci.yml, agent-pr-ready-automerge.yml, issue-done-on-main-merge.yml, plan-issue-ready-on-main-merge.yml, archive-cursor-agent.yml), `scripts/forgejo-pr-automerge.mjs`, `scripts/forgejo-dispatch.mjs`, `scripts/ci-classify-production.mjs`, `scripts/lib/dockerignore.mjs`, `scripts/lib/branch-protection.mjs` e as specs órfãs `ciClassifyProduction`/`dockerignoreMatch`/`branchProtection` `.unit.spec.ts`. **Consumidores funcionais achados pela varredura (fora do escopo literal do body) e ajustados na mesma mudança:** `scripts/lib/test-affected-core.mjs` perdeu as 2 entries de `HIGH_RISK_EXACT` dos scripts mortos; `tests/unit/ciSkipInvariants.unit.spec.ts` tirou os 2 `.forgejo/workflows/*` do loop `.next-e2e`, os 2 paths do pin de high-risk e **reescreveu o teste da janela de 30 min (OPS65) para o invariante vivo da era GitHub**: `deploy.yml` é `workflow_dispatch`-only (sem `push:`/`schedule:` — deploy nunca automático) e `ci-pr.yml` não referencia o gate morto. **Docs:** `AGENTS.md` (frase "preservados até Fase 2" → removidos; rollback = revert do PR ou clone do main congelado do Forgejo), runbook `docs/ops/teqo-1313-deploy.md` (Rollback do cutover idem), `README.md` (cheatsheet de 5 linhas era-Forgejo com janela de 30 min + deploy automático → era-GitHub: auto-merge nativo + deploy manual dispatch), comentários com referência morta em `scripts/gate-ci.mjs`, `scripts/github-pr-automerge.mjs` e `.github/workflows/agent-pr-ready-automerge.yml`. **Intocados (anti-goal tracker):** `forgejo-api.mjs`, `agent-*.mjs`, `issue.mjs`, `forgejo-issue-transition.mjs`, `agent-promote-related-on-merge.mjs`, `.forgejo/worktree.env` (consumido pelo provisionamento de worktree), `.dockerignore` (o arquivo — `docker build` do deploy o consome; só o lib de parse morreu). `docs/AGENT-OPS.md` já era era-GitHub (zero referências). Histórico (plans/changelogs antigos) congelado. Sem migration/access/Consent/UI. Gate: tsc/lint/format/knip/cycles/unit+int/build verdes; grep de saneamento `forgejo-pr-automerge|forgejo-dispatch|ci-classify|dockerignore.mjs|branch-protection.mjs` → só histórico. diff --git a/docs/changelog/2026-08-19-ops71.md b/docs/changelog/2026-08-19-ops71.md new file mode 100644 index 00000000..ebe67b9e --- /dev/null +++ b/docs/changelog/2026-08-19-ops71.md @@ -0,0 +1 @@ +**Recently resolved (2026-08-19):** **OPS71** — CI/PR/deploy do Forgejo Actions de volta para o **GitHub Actions**, com o **tracker de Issues permanecendo no Forgejo** (labels, claims, `pnpm issue`/`agent:*` e flips pós-merge continuam via API do Forgejo; o GitHub é só o host do Actions). O PR CI (`ci-pr.yml`, job único `checks` — OPS62) roda no GitHub com a mesma cascata (guards → lint → format → typecheck → knip → cycles → unit → int → build → e2e selected/afetado via `ci-scope.mjs`), com `concurrency` nativo cancelando o run em push novo e `if:` same-repo (repo público — fork não queima minutes). Seu check-run `CI (PR) / checks` é o required check do `main` e o gate do **auto-merge nativo** (`agent-pr-ready-automerge.yml` → `scripts/github-pr-automerge.mjs`: draft `cursor/*` → Ready via PATCH, arm `enablePullRequestAutoMerge` GraphQL com REBASE — o servidor espera o required check verde; sem poll; o pin OPS64 "rollup pode mentir" é estrutural aqui: job único posta um check-run honesto). **`ci.yml` (verificador de main) eliminado** — publicar é **dispatch manual** (`deploy.yml`): `verify` (hosted, suíte full incl. e2e full) → `deploy` (`needs`, `runs-on: [self-hosted, homeserver]`) executa `scripts/deploy-homeserver.sh <sha>` **localmente** no runner do GitHub instalado no homeserver (sem SSH; outbound-only; o hosted nunca toca o homeserver); o script re-aponta o workspace `~/teqo-deploy` para `TEQO_REPO_URL` (default `https://github.com/fsolla/teqo.git`, público). `pnpm push` passa a empurrar para `origin` = **GitHub** (cutover manual de remote documentado; o repo Forgejo congela em main, tracker vivo por API). Novos helpers plain-Node zero-dep: `scripts/lib/github-api.mjs` (REST+GraphQL, retry OPS67), `scripts/lib/github-pr-flow.mjs` (decisão draft/veto/skip, pura), `scripts/lib/github-branch-protection.mjs` (regra desejada + drift, `enforce_admins: true`), `scripts/github-pr.mjs` (criação de PR Ready — nunca draft), `scripts/configure-branch-protection.mjs` reescrito para a REST do GitHub; `forgejo-issue-transition.mjs` aceita `PR_BODY` (body do PR via evento GitHub; flip continua no Forgejo por `FORGEJO_API_TOKEN`). **Cutover em duas fases (gate):** nada do Forgejo foi apagado (`.forgejo/workflows/`, `forgejo-pr-automerge.mjs`, `forgejo-dispatch.mjs`, `ci-classify-production.mjs` preservados como rollback) — a remoção é entrega sucessora pós-validação. Testes: `githubApi` (10), `githubBranchProtection` (10), `githubPrFlow` (9) unit novos. Docs: `AGENTS.md`, `docs/AGENT-OPS.md`, `.agents/rules/agent-pr-workflow.mdc`, `work-issue/execution-pipeline.md`, `agent-work-issue`, `capture-review-debts`, runbook `docs/ops/teqo-1313-deploy.md` (dispatch manual, runner no homeserver, cutover com ordem, rollback). diff --git a/docs/changelog/2026-08-19-ops72.md b/docs/changelog/2026-08-19-ops72.md new file mode 100644 index 00000000..8d4f6b77 --- /dev/null +++ b/docs/changelog/2026-08-19-ops72.md @@ -0,0 +1 @@ +**OPS72** — política de e2e: **CI de PR sem e2e full; local afetado como passo da skill; full só antes do deploy**. Decisão do gate (2026-08-19): (1) **local = discricionário** — o agente roda os e2e que criou + os da mesma superfície afetada, decide quais (`pnpm test:e2e:affected` é a ferramenta, não um gate mecânico; não está no `gate:push`); (2) **CI de PR = blast radius** — `ci-pr.yml` perdeu o step `E2E tests (full suite)`; o e2e roda só em modo `selected` (specs que o classifier detectou como tocadas); diff high-risk (schema/lockfile/harness) classifica `full` e o PR simplesmente não roda e2e — deliberado: o espelho local roda full nesse mesmo diff e o `verify` do deploy roda full antes de publicar; (3) **main = full no deploy** — inegociável, já existia no `deploy.yml` (nada mudou lá). Docs: `work-issue` e `agent-work-issue` (checklist + Passo 4), `execution-pipeline.md` (nova seção "E2E local afetado" com o critério + a **limitação da #72**: `--no-deps` + projetos paralelos colide no `seedTestUser` → `--workers=1` ou a cadeia padrão de projetos), `AGENTS.md` (parágrafo OPS59 → política OPS72), `docs/AGENT-OPS.md` (fluxo do agente + linha do `ci-pr.yml` na tabela), `.agents/rules/agent-pr-workflow.mdc` (descrição do `gate:push`), mensagens informativas do `gate-ci.mjs` (texto-only — e2e continua fora do gate) e comentários do workflow. Mecânica intocada por escopo (`ci-scope.mjs`, manifest, `run-e2e-affected.mjs`); o fix da #72 fica na fila (só documentado). Verificação: format/lint/tsc verdes, unit 2444 + int 791 verdes; o e2e local afetado rodou a suíte **full** (o diff toca `scripts/gate-ci.mjs`, high-risk → modo `full` — a política em ação): 154 passed em dev mode; 8 falhas de ambiente (timeouts/cold-compile sob carga) triadas — 5/8 passaram em re-run e as 3 restantes passaram em `E2E_PROD=1` (o modo do CI/verify; 41 passed + 2 flaky que passaram isoladas), zero regressão de produto; o CI deste PR não roda e2e (modo `full` por design desta mesma política). Impl plan: `docs/plans/ops72-e2e-local-afetado-ci-sem-full-impl.md`. Nota: `E2E_PROD=1` local regenera o `importMap.js` **sem** as envs de S3 (classe OPS69 — o guard do OPS70/#87 cobre; restaurado na entrega com envs dummy). diff --git a/docs/changelog/2026-08-19-s11.md b/docs/changelog/2026-08-19-s11.md new file mode 100644 index 00000000..4bed3363 --- /dev/null +++ b/docs/changelog/2026-08-19-s11.md @@ -0,0 +1 @@ +**S11** — o feed do Instagram configurado **sem aparecer na home** fica diagnosticável: fim do fail-closed silencioso da S3. A assessoria vê, na mesma global onde preenche o token, o estado da sincronização (`instagramSyncStatus` jsonb hidden na `social-feed-settings`) e o **porquê** de os posts não aparecerem — o painel `InstagramSyncStatusPanel` (campo `ui` irmão do picker) mostra 5 estados fiéis ao rascunho aprovado (desativado / ainda não configurado / falha com motivo em linguagem de produto + "Tentar sincronizar de novo" / sincronizado · há X min · N posts / aguardando 1ª sync). A causa da ocorrência de produção (board vazio com token+ID preenchidos) é agora confirmável em 1 clique após o deploy. Engenharia: `loadInstagramFeed` passou a parsear o body de erro da Graph API e lançar `InstagramApiError` (`status` + `{message,type}`); `describeInstagramError` mapeia para copy pt-BR com a correção — OAuthException/expired → "token inválido/expirado ou emitido via Facebook Login — gere pelo Instagram Login" (o refresh endpoint da Graph API só renova tokens de Instagram Login), "Invalid user id" → "confira o ID da conta Business/Creator", 5xx → indisponível, `TypeError` → rede. Estado escrito por 3 caminhos: render da home (`getInstagramFeed`, dentro do `unstable_cache`, SQL direto sem hooks — padrão anti-`revalidateTag`-no-cache da S2), `afterChange` da global (sync **awaited** quando a credencial IG muda — `instagramCredentialsChanged(doc, previousDoc)`, gate que pula save de exclusões/YouTube; o sync é importado lazy no hook para o `payload.config.ts` não puxar o módulo do feed no load, que é o que todo spec int importa) e a rota `POST /api/social-feed/sync` (botão; `payload.auth` sessão admin exigindo `users` + `isSameOriginRequest` CSRF — allowlistada no spec de convenção de POST routes com razão, é auth de admin ≠ envelope `/campanha`). **Bug real descoberto e corrigido na entrega:** o `afterChange` roda DENTRO da transação do save (a row lock fica presa até os hooks retornarem), então escrever o status/snapshot via pool numa segunda conexão **deadlockava no lock da própria linha** (~80-230s por save, transações-zumbi segurando o lock). A correção roteia os persists do sync-hook pela **mesma transação** (`getPostgresTransactionDatabase` — o helper transacional da C6) com um `req` opcional no `syncInstagramFeed`; o render e a rota sem transação aberta seguem no pool. Diagnóstico reproduzido em `pg_stat_activity` (UPDATE `transactionid` Lock vs `idle in transaction`) e fix verificado (45ms vs 232s). Stub IG ganha estado `invalid-token` (400 OAuthException) para o e2e da cena de falha; runbook `docs/ops/instagram-feed-token-runbook.md` (o que o painel mostra, validação via curl `/me` e `refresh_access_token`, geração pelo Instagram Login, o que NÃO fazer). Migration `20260820_003426_add_instagram_sync_status` (ADD COLUMN jsonb nullable, não destrutiva). **/simplify (3 reviewers):** absorvi a correção P1 de diagnóstico — o mapeamento de ID exigia só "user" (regex frouxo) e mensagens reais de token OAuthException ("the **user** must be logged in", "user changed password") caíam na branch de ID, mostrando a razão errada; passei a exigir "user" E "id" juntos (`isInvalidUserIdMessage`) e adicionei o mapeamento de `AbortError` (timeout → "demorou demais"). Painel: gate passou a respeitar o master `enabled` (master off → "Instagram desativado", nunca "Sincronizado no board"), rótulo do botão no estado de primeira sync ("Tentar sincronizar" vs "de novo"), `outcome` transiente limpo ao editar credencial e `aria-live` nos boxes interativos. Testes: caminho transacional do `syncInstagramFeed` (persist na mesma transação) + fallback pool cobertos por unit; e2e do painel ganhou as cenas fail-500 e "desativado"; cleanup do stub global virou `afterEach`. Débitos registrados: #124 (janela de lock: fetch de rede no sync-hook segura a row lock até 10s, acoplando a home ao save) e #125 (polish P3 — tipo de persist duplicado, rota 200 vs 400, last-writer-wins stale, `5 posts` hardcoded). Verificação: unit 2463 + int 791 verdes; lint/format/tsc/cycles verdes; `pnpm build` ok; e2e local — smoke admin 5/5 (painel ok → falha token → retry → falha 500 → desativado → não configurado) + describe serial frontend 19/19. diff --git a/docs/changelog/2026-08-20-ops71-flip.md b/docs/changelog/2026-08-20-ops71-flip.md new file mode 100644 index 00000000..f38173c9 --- /dev/null +++ b/docs/changelog/2026-08-20-ops71-flip.md @@ -0,0 +1 @@ +**Recently resolved (2026-08-20):** **OPS71-FLIP** — flip pós-merge falhou/silencioso no fluxo GitHub, validado e endurecido (Issue #119, achado ao vivo no merge do PR #746). **Investigação (GitHub API pública):** o `closed` do PR #746 (merge por `github-actions[bot]`, auto-merge armado com o `GITHUB_TOKEN` nativo) **não criou run nenhum** — anti-recursão documentada do GitHub (eventos acionados por ações do `GITHUB_TOKEN` não geram runs; o `closed` fica no timeline com 0 runs; comparação com o #742, mergeado por `fsolla`, cujos runs nasceram 3s após o merge). O 410 do run do #742 é falha separada (API do Forgejo respondeu 410 Gone nginx, transitório). **Endurecimento:** (1) **fix de raiz** — o arm do auto-merge (`agent-pr-ready-automerge.yml` → `scripts/github-pr-automerge.mjs`) agora usa o secret novo **`AUTOMERGE_PAT`** (PAT real do fsolla; decisão pura `automergeArmingToken` em `github-pr-flow.mjs` com fail-closed — sem o secret o job sai 1, nunca fallback pro bot token; o merge passa a ser atribuído a usuário real e o `closed` volta a gerar runs); (2) **recuperação manual** — `workflow_dispatch` com input `pr` nos dois flips (`issue-done-on-main-merge.yml`, `plan-issue-ready-on-main-merge.yml`): o job busca o PR via `gh` com o mesmo contrato do evento (merged + base main + same-repo, senão skip) e repassa o body como `PR_BODY`; (3) **close da Issue** — `forgejo-issue-transition.mjs` agora `PATCH state=closed` via novo `closeIssue` do `forgejo-api.mjs` (era-Forgejo o merge fechava nativo; era-GitHub o PR não toca o tracker — antes, toda Issue mergeada ficava `OPEN` com `done`+`in-prod`); (4) **410 no retry** — `forgejo-api.mjs` retenta **410 em qualquer método** (proxy respondeu antes da origem — sem efeito duplicado; 5xx segue fail-closed em escrita) e corpo nginx vira marker compacto `corpo não-JSON (proxy/nginx…)` no erro. **Testes:** 8 specs novas (retry 410 GET/write, marker após 410 esgotado, `closeIssue` PATCH pin, `automergeArmingToken` ok/fail-closed/vazio) — 50 unit verdes nas specs tocadas. **Docs:** `AGENTS.md` (frase do auto-merge com o PAT + porquê), `docs/AGENT-OPS.md` (tabelas de workflows/secrets + runbook de recuperação manual com fallback local `PR_BODY`). Sem migration/access/Consent/UI. Impl: [`ops71-flip-pos-merge-falhou-silencioso-impl.md`](plans/ops71-flip-pos-merge-falhou-silencioso-impl.md). diff --git a/docs/changelog/2026-08-20-ops71-infra.md b/docs/changelog/2026-08-20-ops71-infra.md new file mode 100644 index 00000000..7c443000 --- /dev/null +++ b/docs/changelog/2026-08-20-ops71-infra.md @@ -0,0 +1 @@ +**OPS71-INFRA (2026-08-19):** runner **self-hosted do GitHub instalado no homeserver** (Issue #113) — o job `deploy` do `deploy.yml` (dispatch manual) agora roda localmente no homeserver, sem SSH. Detalhes: `~/actions-runner` v2.336.0 linux-x64 (sha256 conferido contra a API de releases), nome `teqo-1313-runner`, labels `self-hosted` + `homeserver`, registrado com o PAT do OPS71 (registration token TTL 1h gerado e aplicado na sequência). Roda como **systemd user** `actions.runner.fsolla-teqo.teqo-1313-runner.service` (linger já ativo; `svc.sh install` exige root — unit user criado manualmente do template oficial), usuário `fsolla` (docker + `~/stack` envs), workdir `_work`, outbound-only (funciona atrás do Cloudflare tunnel). Verificado `online` na API do GitHub com as labels corretas. Registrado em infra-solla `STATE.md` (seção Teqo: runner + fluxo deploy manual atualizado; linha do repo/CI corrigida para o pós-cutover GitHub). Workspace `~/teqo-deploy` re-aponta para o GitHub no primeiro deploy via `set-url` idempotente do script. Pendência registrada: secret `CURSOR_API_KEY` no GitHub fica dormente (helper `archive-cursor-agent.yml`; a key não existe no ambiente — pool morto OPS65) até o usuário pedir a reativação. diff --git a/docs/changelog/2026-08-20-ops73.md b/docs/changelog/2026-08-20-ops73.md new file mode 100644 index 00000000..1326bd4e --- /dev/null +++ b/docs/changelog/2026-08-20-ops73.md @@ -0,0 +1 @@ +**OPS73 — deploy do fix do admin em produção (2026-08-20):** o `/admin` do Payload (jorgesolla1313.com.br) voltou a renderizar o formulário de login em produção — o bug de "tela branca" que travava o CMS do time editorial/campanha está resolvido e verificado. Durante o diagnóstico descobriu-se que a premissa do plano ("produção nunca recebeu o fix OPS69") estava errada: o deploy das 23:09 de 2026-08-19 (revision `8c734ba`) já continha o fix do importMap **no histórico**, mas o commit S9 (`8d2352c0`, captura de novidades na home) tinha regenerado o importMap **sem** as envs `S3_*`, removendo o handler `@payloadcms/storage-s3/client#S3ClientUploadHandler` e reintroduzindo o erro `getFromImportMap: PayloadComponent not found in importMap` — o admin abria em branco de novo ("funcionou e no deploy seguinte parou"). Verificado também que o **main do GitHub havia divergido do Forgejo** (pós-cutover OPS71): o GitHub tinha o fix + OPS71, mas não as features S8 (seção "Nossa história") e S9 (captura de novidades) que produção já rodava — deployar o main como estava regrediria as duas. A entrega: PR #751 re-homou no GitHub main as features S8+S9 (cherry-pick dos commits aprovados no Forgejo `8d2352c0` + `488dcf7d`), mantendo o handler no importMap (prova: `generate:importmap` com envs `S3_*` dummy → diff vazio vs main); CI verde; aguardando o merge. Deploy pós-merge via o pipeline canônico do OPS71 (dispatch manual do `deploy.yml`: `verify` full → job `deploy` no runner self-hosted recém-instalado no homeserver — 2 tentativas "stale run" por merges paralelos no meio do verify, terceira aplicou `dba75ff8`); migração C143 (responsáveis de demandas, aditiva+backfill) aplicada. Verificação pós-deploy: container na revision `dba75ff8`, healthy, **log sem nenhum `getFromImportMap`**, `/admin` renderizando o formulário de login (2 inputs email/senha + botão Login, título "Login - Payload", 0 erros de console), e a home pública mantendo "Nossa história" (S8) e o form de novidades (S9). Evidência: `docs/plans/ops73-deploy-admin-prod-fixed-evidence.png`. Lições registradas: a regeneração do importMap sem envs `S3_*` re-órfão o handler (classe exata que o guard #87 deve impedir no CI); divergência de plataformas pós-cutover — workspace do homeserver ainda apontava para o Forgejo local (`localhost:3000`) e o S10 (pixel) segue pendente de re-home no GitHub. diff --git a/docs/ops/instagram-feed-token-runbook.md b/docs/ops/instagram-feed-token-runbook.md new file mode 100644 index 00000000..8731def9 --- /dev/null +++ b/docs/ops/instagram-feed-token-runbook.md @@ -0,0 +1,71 @@ +# Runbook — Feed do Instagram: validar/gerar o token (S11) + +Issue: #115 · Entrega: S11 (painel de status da sincronização no admin) + +## O que o painel mostra + +Na global **Configurações → Feed de redes sociais**, o bloco "Instagram — estado +da sincronização" diz o estado da última tentativa: + +| Estado | Significado | Ação | +| --------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------ | +| **Sincronizado · há X min · N posts** | Credenciais aceitas; posts no board "Acompanhe de perto" | Nenhuma | +| **Falha na última sincronização** | A Graph API recusou a última tentativa; o texto diz o motivo e a correção | Corrigir conforme o motivo e clicar **Tentar sincronizar de novo** | +| **Instagram ainda não configurado** | Falta token ou ID | Preencher os dois campos | +| **Aguardando a primeira sincronização** | Configurado, nenhuma tentativa ainda | Clicar **Tentar sincronizar de novo** | + +O estado é atualizado no salvar (quando as credenciais mudam), em cada render da +home (cache de 5 min) e no clique do botão. A home pública nunca quebra quando a +API falha — o board segue com artigos + YouTube. + +## Diagnóstico da ocorrência de 2026-08-19 (causa provável) + +Produção mostrava picker vazio e nenhum card IG com token + ID preenchidos — o +fail-closed silencioso da S3 não dizia o porquê. A causa mais provável, na ordem: + +1. **Token emitido via Facebook Login** (page token) — a `graph.instagram.com` + recusa; o refresh automático não renova (o endpoint `refresh_access_token` + só aceita tokens do Instagram Login). +2. **Token expirado/revogado**. +3. **ID do usuário errado** (não é o da conta Business/Creator vinculada). + +**Confirmação em 1 clique:** no admin, **Tentar sincronizar de novo** — o painel +mostra o motivo exato devolvido pela API. Se a causa for a 1, aparece a mensagem +de token recusado com a correção; se for a 3, a mensagem de ID não reconhecido. + +## Validar um token sem o admin + +```bash +# O endpoint /me responde com o username e o tipo de conta se o token é válido +# para a Graph API do Instagram (Business/Creator): +curl -s "https://graph.instagram.com/me?fields=user_id,username&access_token=SEU_TOKEN" + +# Teste o refresh (só tokens emitidos pelo Instagram Login passam): +curl -s "https://graph.instagram.com/refresh_access_token?grant_type=ig_refresh_token&access_token=SEU_TOKEN" +``` + +- `/me` com `OAuthException` → token inválido/expirado ou emitido via + **Facebook Login** (a Graph API do Instagram só aceita tokens do Instagram + Login). +- `/me` ok mas o ID configurado não bate com `user_id` → corrigir o campo + "ID do usuário". + +## Gerar um token de longa duração (Instagram Login) + +1. Acessar `developers.facebook.com` com a conta que administra a página → + criar/usar um app do tipo **Business** (qualquer tipo serve para o fluxo de + negócios, mas o app precisa estar em modo Live para uso fora do sandbox). +2. Produto **Instagram** → **Instagram Login with Basic Display** → **Gerar + token** com a conta Business/Creator do @depjorgesolla. +3. Trocar o token de curta duração pelo de longa duração usando o + **Long-Lived Token Exchange** da documentação do Instagram Basic Display + (ou usar o endpoint `refresh_access_token` do passo anterior após a troca). +4. Colar o token e o ID numérico da conta no admin e clicar + **Tentar sincronizar de novo**. + +## O que NÃO fazer + +- Não usar token da API Graph do Facebook (página) — o app recusa. +- Não colar o token em arquivos, Issues ou chat — o campo é admin-only e deve + continuar. +- Não zerar o token para "testar" — o painel de status dispensa tentativa-e-erro. diff --git a/docs/ops/teqo-1313-deploy.md b/docs/ops/teqo-1313-deploy.md index e4332c00..1684d2ea 100644 --- a/docs/ops/teqo-1313-deploy.md +++ b/docs/ops/teqo-1313-deploy.md @@ -1,40 +1,30 @@ -# Runbook: deploy do site 1313 no homeserver (OPS53, janela OPS65) +# Runbook: deploy do site 1313 no homeserver (OPS53 + OPS71 — dispatch manual) -O deploy de produção é uma etapa do CI (`ci.yml` no Forgejo): mergir em `main` -com a suite verde publica o site em `jorgesolla1313.com.br` — sem passo manual. -Merge **com mudança de produção** = site atualizado em ~30 min (janela) + tempo -da suite/deploy (~15–25 min cada; tipicamente < 1 h no total). +O deploy de produção é uma **action manual** (`workflow_dispatch` no GitHub +Actions, `.github/workflows/deploy.yml`): mergir em `main` **não** publica +nada. O operador dispara o deploy quando decide publicar; antes de tocar o +homeserver, o job `verify` (hosted) roda a suíte **full** (incl. e2e full). ## Gatilho e fluxo -1. `git push` para `main` (via PR merge) — o `ci.yml` **não dispara mais a cada - push**: roda por **janela fixa de 30 min** (`schedule */30`) ou via - `workflow_dispatch` (deploy manual, imediato, sempre full suite). -2. Job `gate` (barato, host, sem pnpm): lê a **revision do container rodando** - no homeserver (`docker inspect` → label `org.opencontainers.image.revision` - do `teqo-1313` — a fonte de verdade; o compose pode mentir após rollback - falho, pois é trocado antes do rollout) e classifica "mudança de produção" - com o `.dockerignore` como fonte da verdade (o que não entra na imagem não - justifica build novo; `docs/`, `.agents/`, `.env*` viram skip; - `scripts/`, `tests/`, `.forgejo/`, `AGENTS.md` contam como produção — - conservador, qualquer dúvida roda a suite). - - Deployed == HEAD → suite e deploy **skipped**; run verde. - - Sem revision conhecida (label ausente, container sem label) → fail-open: - suite + deploy rodam na janela (run verde; comportamento conservador). - - Gate incapaz de classificar (erro de git/`.dockerignore`) → suite roda e - o run fica **vermelho** — nunca blackout silencioso. -3. Com mudança de produção: suite completa verde (static/int/build/e2e/checks). -4. Job `deploy` (runs-on `host`, na **workstation**) roda - `ssh homeserver "bash -s -- <sha>" < scripts/deploy-homeserver.sh`. - O script é **idempotente**: se o container rodando já tem a revision do SHA - (ex.: dispatch duplicado), sai verde "already deployed" sem rebuild. -5. O script, no **homeserver**: guarda de HEAD (`main` remoto == SHA do job, - senão skip) → `flock` (serializa) → guard "already deployed" (revision do - container) → **build do migrator** (o estágio migrator não roda `next -build` — builda mesmo contra o schema antigo; BuildKit, secrets do - `~/stack/teqo-1313.env`) → push/tag do migrator em `localhost:5000` → - swap dos tags de imagem no `~/stack/docker-compose.yml` (backup antes) → - **migrations** +1. GitHub → Actions → **Deploy (manual)** → Run workflow (ref: `main`). +2. Job `verify` (hosted `ubuntu-latest`, ~50 min): suíte full sem skips — + check-test-locations → lint → format → typecheck → knip → cycles → unit → + int (migrate+seed nos services) → build → e2e full. +3. Se `verify` verde, o job `deploy` (`needs: [verify]`, + `runs-on: [self-hosted, homeserver]`) roda no **runner do GitHub instalado + no homeserver** e executa `scripts/deploy-homeserver.sh <sha>` **localmente** + (sem SSH; o runner conecta outbound ao GitHub — funciona atrás do + Cloudflare tunnel; o hosted nunca toca o homeserver; o self-hosted não + conta minutos hosted). +4. O script, no **homeserver**: guarda de HEAD (`git ls-remote` de + `TEQO_REPO_URL` — default `https://github.com/fsolla/teqo.git`, público — + == SHA do job, senão "stale run" skip) → `flock` (serializa) → guard + "already deployed" (revision do container rodando) → **build do migrator** + (o estágio migrator não roda `next build` — builda mesmo contra o schema + antigo; BuildKit, secrets do `~/stack/teqo-1313.env`) → push/tag do + migrator em `localhost:5000` → swap dos tags de imagem no + `~/stack/docker-compose.yml` (backup antes) → **migrations** (`docker compose --profile maintenance run --rm teqo-1313-migrate </dev/null`, já com a imagem do SHA novo) → **build do runner** (contra o banco JÁ migrado — o `next build` da geração estática lê o schema novo, OPS66; @@ -43,27 +33,63 @@ build` — builda mesmo contra o schema antigo; BuildKit, secrets do → push/tag do runner → `docker compose up -d teqo-1313` → healthcheck → smoke (`/`, `/campanha/login`, `/admin`, barreira 307, WebAuthn login-options, `api/revalidate` com o secret real). -6. Falha = job vermelho no commit; nada é publicado pela metade. +5. Falha = job vermelho; nada é publicado pela metade (rollback automático + após o swap). -**Primeira janela (verificação ao vivo):** após o merge do OPS65, acompanhe um -run agendado — confira no log do job `gate` o `deployed_sha` lido do container e -o veredito da classificação, e que um dispatch manual roda suite + deploy -(idempotência: segundo dispatch no mesmo SHA sai "already deployed"). Isso -prova ao vivo a paridade Forgejo de `needs.<gate>.outputs` com gate falho -(suite roda; deploy pula; run vermelho). +**Primeiro deploy (verificação ao vivo):** depois do cutover OPS71, dispare o +deploy manual e confira no log do job `deploy` o `set-url` do workspace +(origem Forgejo → GitHub), o `deployed_sha` lido do container e o guard +"already deployed" num segundo dispatch do mesmo SHA. ## Onde roda cada coisa -| Máquina | Papel | -| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **workstation** (16c/61GB) | runner Forgejo (`workstation-runner`); job `deploy` roda no host (chave SSH `~/.ssh/id_ed25519` → `ssh homeserver`) | -| **homeserver** (8c/16GB) | todo o stack (`~/stack/docker-compose.yml`): postgres `teqo_1313`, forgejo, registry `localhost:5000`, cloudflared, `teqo-1313` na 127.0.0.1:1313; workspace `~/teqo-deploy` (clone do Forgejo local) | +| Máquina | Papel | +| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **GitHub hosted** (ubuntu) | CI de PR (`ci-pr.yml`, job `checks`) e job `verify` do `deploy.yml` (suíte full). Nunca toca o homeserver. | +| **homeserver** (8c/16GB) | todo o stack (`~/stack/docker-compose.yml`): postgres `teqo_1313`, forgejo, registry `localhost:5000`, cloudflared, `teqo-1313` na 127.0.0.1:1313; **runner self-hosted do GitHub** (labels `self-hosted`, `homeserver`; instalado 2026-08-19 — Issue #113 OPS71-INFRA; `~/actions-runner` v2.336.0, systemd user `actions.runner.fsolla-teqo.teqo-1313-runner.service` — `systemctl --user status/restart`); workspace `~/teqo-deploy` (clone de `github.com/fsolla/teqo`, público) | +| **workstation** | dev/agentes apenas — o runner do Forgejo foi **desligado** no cutover (o schedule do `ci.yml` antigo não tem mais razão; religar é reversível) | -Segredos **não** ficam no Forgejo: o script sourceia `~/stack/teqo-1313.env` e -`~/stack/.env` (chmod 600) no próprio homeserver. Nenhum secret novo em -Settings → Actions. +Segredos **não** ficam no GitHub: o script sourceia `~/stack/teqo-1313.env` e +`~/stack/.env` (chmod 600) no próprio homeserver. O repo GitHub é público — o +clone do workspace não precisa de credencial. No GitHub ficam apenas os +secrets de integração: `FORGEJO_API_TOKEN` (flips pós-merge) e +`CURSOR_API_KEY` (archive helper, dormente). -## Rollback +## Cutover OPS71 (passos manuais, ordem) + +1. Remotes locais (config compartilhada pelos worktrees): + `git remote set-url origin git@github.com:fsolla/teqo.git` e + `git remote add forgejo ssh://git@192.168.15.142:2222/fsolla/teqo.git` + (ou `https://git.solla.dev/fsolla/teqo.git`) — o repo Forgejo congela em + main; o tracker (Issues) segue vivo por API. +2. GitHub repo: secrets `FORGEJO_API_TOKEN` + `CURSOR_API_KEY` + (Settings → Secrets and variables → Actions) e **`Allow auto-merge`** + (Settings → General → Pull Requests; ou `PATCH /repos/...` com + `allow_auto_merge: true`). Repo é público: em Settings → Actions → + General, defina fork PR workflows como não executar (ou exigir aprovação). +3. PAT local: criar token GitHub com escopo `repo` e exportar `GITHUB_TOKEN` + (para `node scripts/github-pr.mjs` e `pnpm configure:branch-protection`). +4. `GITHUB_TOKEN=… pnpm configure:branch-protection` — required check + `CI (PR) / checks` (literal de match: `checks` — o GitHub casa pelo nome do + check-run; a UI exibe workflow/job; pin PR #742), 0 reviews, + `enforce_admins: true`. **Antes do primeiro push** (senão o PR mergearia + sem required check). +5. **Desligar o runner do Forgejo** na workstation (ex.: + `systemctl --user stop forgejo-runner`) — após o primeiro PR GitHub + validado; para o schedule do `ci.yml` antigo. +6. Instalar o runner self-hosted do GitHub no homeserver (labels + `self-hosted` + `homeserver`; usuário com docker + acesso a `~/stack`; + registrar em infra-solla: `STATE.md`). +7. Push do PR do OPS71 → CI verde → auto-merge → flip da Issue no Forgejo → + deploy manual de validação. + +Rollback do cutover (se o GitHub falhar cedo): religar o runner do Forgejo e +abrir PRs no Forgejo a partir do main congelado — os workflows de +`.forgejo/workflows/` foram removidos na OPS71 Fase 2, mas restauram do git +history (`git revert` do PR da remoção, ou clone do main congelado do Forgejo) +— remotes: `git remote set-url origin ssh://git@192.168.15.142:2222/fsolla/teqo.git`. + +## Rollback (do deploy) A imagem anterior **não** fica mais local após a limpeza pós-deploy (INF3/F2: `docker builder prune` + remoção das tags locais antigas, exceto o SHA em uso) @@ -91,39 +117,40 @@ roda após migrator build+push+swap ok; rollout só após runner build ok). Desd o OPS66 o migrate roda **antes** do build do runner (o build precisa do schema novo): um build do runner que falhe depois do migrate deixa o banco à frente do código — migrations são append-only e revisadas antes do deploy (checklist); -a correção se re-mergeia e a próxima janela completa o deploy. +a correção se re-mergeia e o próximo deploy completo publica. ## Falhas conhecidas -| Sintoma | Causa | Tratamento | -| ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Job verde sem deploy ("stale run") | Push mais novo venceu; o deploy antigo saiu da fila | Nada a fazer — o run do SHA novo deploya | -| Job verde sem deploy ("already deployed") | O container rodando já tem a revision do SHA do job (ex.: `workflow_dispatch` duplicado da mesma HEAD) | Nada a fazer — o site já roda esse SHA. Forçar rebuild da MESMA HEAD não é suportado via dispatch: edite o tag da imagem no compose (`image: teqo-1313:<sha>-rebuild`) e `docker compose up -d`, ou espere um commit novo | -| Gate `gate` vermelho (erro de git/`.dockerignore`) | SHA deployado ilegível para o git (ex.: história reescrita) ou `.dockerignore` corrompido no checkout | Fail-open: suite roda; deploy pula (nunca deploy às cegas). Corrigir a causa e esperar a próxima janela (auto-cura). Se `ssh homeserver` estiver fora, o gate lê revision vazia e roda tudo (verde) — checar tailnet se a suite roda sem deploy recorrente | -| Sem revision no container (label ausente no compose) | O compose do homeserver não define `org.opencontainers.image.revision` (deploy pré-OPS53 ou edição manual) | Degradação conservadora: toda janela roda suite + rebuild (verde). Corrigir adicionando o label ao serviço `teqo-1313` no compose e re-deployando | -| Todo job docker quebra com `exec: "node": executable file not found in $PATH` | `runner.envs.PATH` do `~/.forgejo-runner/config.yaml` (workstation) com paths só de host (nvm/bun) — o container perde o node do toolcache (`/opt/acttoolcache`); edição do config exige o toolcache primeiro (ver comentário no próprio config) | Corrigir o PATH no config (toolcache primeiro — ver comentário no próprio arquivo) + `systemctl --user restart forgejo-runner` (incidente 2026-08-17) | -| Deploy para logo após o migrate ("Done." e nada mais, EXIT=0) | `docker compose run` anexa stdin por padrão — o container consome o resto do script que vai no pipe do `bash -s`; o bash chega a EOF e termina sem rodar o rollout | O script já usa `< /dev/null` no `run --rm` do migrate (não remover); sintoma visto 2026-08-17 no primeiro deploy | -| Build falha: `network mode "stack_default" not supported by buildkit` | BuildKit (drivers docker e docker-container) recusa rede bridge custom no `--network` | O script builda com `--network host` + proxy socat `teqo-1313-build-proxy` (na `stack_default`, publicado em 127.0.0.1:5433, criado idempotentemente com `restart: unless-stopped`) + `DATABASE_URL` reescrita para o loopback | -| Build OOM no homeserver | Laptop 8c/16GB com o stack ativo (~12GB livres medidos) | Re-dispatch; se recorrente, item futuro: build na workstation com túnel | -| Build do runner falha: `relation "..." does not exist` no `next build` | Migration nova criou tabela lida em geração estática. Pré-OPS66 a ordem era build→migrate e o deploy morria aqui para sempre (incidente 2026-08-18, S2) | Pós-OPS66 não deve ocorrer: migrate roda antes do build do runner. Se reaparecer, confira no log se o passo migrate rodou; recovery manual: `docker build --target migrator` + `docker run --rm --network stack_default --env-file ~/stack/teqo-1313.env localhost:5000/teqo-1313-migrator:<sha>` e re-dispatch do workflow | -| `checkout@v5` falha no job `host` | Label host do act_runner | Fallback: baixar o script direto do Forgejo (`curl -s https://git.solla.dev/fsolla/teqo/raw/branch/main/scripts/deploy-homeserver.sh`) | -| Migrate falha | Drift/erro de schema | Job vermelho; site segue no container antigo; corrigir e re-mergear | -| Smoke falha pós-up | Regressão de runtime | Rollback automático (restore + `up -d`) + job vermelho; investigar | +| Sintoma | Causa | Tratamento | +| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Job verde sem deploy ("stale run") | Push mais novo venceu; o dispatch apontava um SHA que não é mais o HEAD do main do GitHub | Nada a fazer — dispatch novo no main atual | +| Job verde sem deploy ("already deployed") | O container rodando já tem a revision do SHA do job (ex.: `workflow_dispatch` duplicado da mesma HEAD) | Nada a fazer — o site já roda esse SHA. Forçar rebuild da MESMA HEAD não é suportado via dispatch: edite o tag da imagem no compose (`image: teqo-1313:<sha>-rebuild`) e `docker compose up -d`, ou espere um commit novo | +| Job `deploy` nunca roda / runner offline | Runner self-hosted do homeserver não instalado ou parado (passo manual do cutover OPS71) | Instalar/religar o runner (labels `self-hosted`, `homeserver`); os PRs e o `verify` não dependem dele — só o deploy | +| Deploy dispara mas o job `verify` falha | Regressão real na suíte full | Corrigir e re-dispatchar — o homeserver nunca é tocado | +| Deploy para logo após o migrate ("Done." e nada mais, EXIT=0) | `docker compose run` anexa stdin por padrão — o container consome o resto do script; o bash chega a EOF e termina sem rodar o rollout | O script já usa `< /dev/null` no `run --rm` do migrate (não remover); sintoma visto 2026-08-17 no primeiro deploy | +| Build falha: `network mode "stack_default" not supported by buildkit` | BuildKit (drivers docker e docker-container) recusa rede bridge custom no `--network` | O script builda com `--network host` + proxy socat `teqo-1313-build-proxy` (na `stack_default`, publicado em 127.0.0.1:5433, criado idempotentemente com `restart: unless-stopped`) + `DATABASE_URL` reescrita para o loopback | +| Build OOM no homeserver | Laptop 8c/16GB com o stack ativo (~12GB livres medidos) | Re-dispatch; se recorrente, item futuro: build na workstation com túnel | +| Build do runner falha: `relation "..." does not exist` no `next build` | Migration nova criou tabela lida em geração estática. Pré-OPS66 a ordem era build→migrate e o deploy morria aqui para sempre (incidente 2026-08-18, S2) | Pós-OPS66 não deve ocorrer: migrate roda antes do build do runner. Se reaparecer, confira no log se o passo migrate rodou; recovery manual: `docker build --target migrator` + `docker run --rm --network stack_default --env-file ~/stack/teqo-1313.env localhost:5000/teqo-1313-migrator:<sha>` e re-dispatch do workflow | +| Workspace clone sem o SHA novo | O workspace antigo apontava para o Forgejo local (pré-OPS71) e o fetch não achou o SHA | O script agora re-aponta `origin` para `TEQO_REPO_URL` (idempotente) antes do fetch — se ainda falhar, `git -C ~/teqo-deploy remote -v` e corrigir manualmente | +| Migrate falha | Drift/erro de schema | Job vermelho; site segue no container antigo; corrigir e re-mergear | +| Smoke falha pós-up | Regressão de runtime | Rollback automático (restore + `up -d`) + job vermelho; investigar | ## Segurança (decisão deliberada) -- O job `deploy` usa `runs-on: host` — executa como `fsolla` na workstation. - Está **apenas** no `ci.yml` (janela `*/30` em `main` + dispatch), nunca no - `ci-pr.yml`. O job `gate` tem o mesmo perfil e é restrito a - `github.ref == 'refs/heads/main'` (dispatch em branch não roda código do - branch no host — a suite roda fail-open em containers com secrets de teste). - `main` só anda por PR mergeado com suite verde. Fork-PRs não disparam - workflows no Forgejo (default off — verificado). +- O job `deploy` usa `runs-on: [self-hosted, homeserver]` — executa como o + usuário do runner no homeserver. Está **apenas** no `deploy.yml` + (dispatch manual), nunca no `ci-pr.yml`; o hosted (`verify`) não tem acesso + ao homeserver e o self-hosted não recebe secrets de produção (as envs vivem + no próprio homeserver). `main` só anda por PR mergeado com CI verde; o + deploy só roda com `verify` full verde. Fork-PRs não rodam CI (same-repo + gate + setting do repo). - Segredos nunca ecoados: sem `set -x`, senhas via `--password-stdin` / build-secrets; envs só no homeserver. ## Referências -- `scripts/deploy-homeserver.sh` — o script remoto (fonte da verdade do fluxo) -- `docs/plans/ops53-ci-deploy-homeserver*.md` — intenção e decisões +- `scripts/deploy-homeserver.sh` — o script (fonte da verdade do fluxo) +- `.github/workflows/deploy.yml` — verify → deploy +- `docs/plans/ops53-ci-deploy-homeserver*.md` — intenção e decisões (era Forgejo) +- `docs/plans/ops71-ci-github-actions-tracker-forgejo*.md` — o cutover - infra-solla: `STATE.md`, `plano-infra-final.md` §"Arquitetura de deploy" diff --git a/docs/plans/demandas-responsaveis-impl.md b/docs/plans/demandas-responsaveis-impl.md new file mode 100644 index 00000000..ea8a1502 --- /dev/null +++ b/docs/plans/demandas-responsaveis-impl.md @@ -0,0 +1,89 @@ +# Impl: Demandas visíveis apenas para responsáveis explícitos + candidato/coordenador + +Status: aprovado +Atualizado em: 2026-08-19 +Issue: #106 +Intenção: docs/plans/demandas-responsaveis.md +Appetite restante: herdado (~1–1,5 dias eng; tracer primeiro, polish depois) + +## Leitura da intenção + +- **Outcome:** a demanda tem um conjunto de **responsáveis** (staff da campanha); candidato e coordenador sempre a veem; assessor do município relacionado que não é responsável **não vê** — nem lista, nem URL, nem busca. Criador entra automaticamente como responsável. Quem atualiza a demanda gerencia os responsáveis. Leader lockdown intacto; custo/comprovantes seguem a visibilidade da demanda. +- **O que NÃO negociar:** visibilidade **fail-closed** por vínculo explícito (nada de herança do município); regra nos dados (access), não escondida no cliente; leader nunca vê demandas; escalada continua candidato/coordenador. +- **O que reavaliar:** a hipótese da intenção era "`canReadCampaignDemand` passa a ser unrestricted ou responsável". Confirma-se no código: hoje o escopo vive **só** em `src/utilities/access/demands.ts:33` (`resolveActorScopedRead` com `municipality`), e **todos** os consumidores (lista, detalhe, busca global do dashboard, demandas vinculadas da atividade, C90, omnibox) herdam via Local API `overrideAccess: false` — não há REST nem client query. O corte é cirúrgico: regra nova + campo + migration + UI de gestão. + +## Abordagem recomendada + +```mermaid +flowchart LR + F[CampaignDemand.responsibles<br/>hasMany → campaignUser] --> A[canRead/UpdateCampaignDemand<br/>advisor → responsibles contains id] + H[enforceDemandWorkflow<br/>criador vira responsável na criação] --> F + M[Migration: campaign_demand_rels<br/>+campaign_user_id + backfill createdBy] --> F + S[updateCampaignDemandResponsibles<br/>tx + advisory lock] --> A + U[DemandResponsibleMultiSelect<br/>nova + wizard + detalhe] --> S +``` + +**Opções consideradas:** + +- **A — Campo `responsibles` hasMany → `campaignUser` + regra `where` de membership** (espelho de `municipality.advisors` / `{ advisors: { contains } }`). +- **B — Relação polimórfica `responsible` (union campaignUser/leadership/stateDeputy) como a `activity`.** +- **C — `or: [responsibles contains, createdBy equals]`** (criador visível mesmo se removido dos responsáveis). + +**Recomendação:** **A** — single collection, sem union desnecessária (responsável é sempre staff da campanha — `eligibleCampaignStaffWhere`), query escalar `{ responsibles: { contains: id } }` (mesmo mecanismo do `advisors`), e a join table `campaign_demand_rels` **já existe** (hoje usada por `receipts`/media) — a migration só adiciona a coluna `campaign_user_id` + FK + índice. + +**Rejeitadas:** **B** porque responsável é sempre `campaignUser` (não há liderança/dobradinha como responsável de demanda — a intenção fala de "pessoas da campanha"/staff; union polimórfica traria o custo do `equals { relationTo, value }` e catalog triplo sem ganho). **C** porque fura o modelo de responsável explícito: remover o criador da lista não o tiraria da visibilidade, e a intenção fixou "criador entra **como responsável**" (vínculo explícito, único source of truth). O criador entra na lista por hook **na criação** + **backfill** dos existentes. + +### Decisões de engenharia + +1. **Regra de leitura/atualização** — `canReadCampaignDemand` deixa de usar `resolveActorScopedRead` (que é o prólogo municipal) e vira função própria: + `admin → true; leader → false; unrestricted → true; advisor → { responsibles: { contains: id } }; resto → false`. `canUpdateCampaignDemand = canReadCampaignDemand` (regra única "quem vê gerencia" — aceite: "quem pode atualizar pode gerenciar os responsáveis"). `canCreateCampaignDemand` **inalterado** (staff no município da carteira). `canDeleteCampaignDemand` admin-only inalterado. `canReadCampaignStaffField`/`canManageCampaignStaffField` inalterados. +2. **Criador auto-responsável** — no branch `create` do hook `enforceDemandWorkflow` (`CampaignDemand.ts:129-143`), quando `actor` é campaignUser: `data.responsibles = union(data.responsibles, [actor.id])`. Cobre **todos** os caminhos de criação: form `/demandas/nova`, wizard (A5), rascunhos C90 (activity.ts passa `user: currentActor`), admin UI (payload admin → actor null → sem responsáveis → fail-closed: invisível a assessores, o que é aceitável — quem criou via admin vê tudo). Sem auto-add no update. +3. **Backfill dos existentes** — na própria migration (padrão C90 `20260808_184113_remodel_activity_responsible.ts`, `RAISE NOTICE` + SQL no up): `INSERT INTO campaign_demand_rels (order, parent_id, path, campaign_user_id) SELECT 0, id, 'responsibles', created_by_id FROM campaign_demand WHERE created_by_id IS NOT NULL`. Preserva "a criação não some do criador" para as demandas pré-c143. Campo novo → nenhuma linha tem responsável → sem dedup. +4. **Gestão (server)** — `updateCampaignDemandResponsiblesRecord` em `actions/demand.ts`, mesmo shape dos irmãos: schema zod novo (`{ id, responsibles: array de positiveRelationshipId }`), `withPayloadTransaction`, `reloadCampaignActor`, staff check, advisory lock `campaign-demand:{id}`, `findByID` scoped (`overrideAccess: false` — a row access autoriza antes do write), `payload.update` com a lista **completa** (replace). Replace é a forma mais simples e segura: o estado do client já é a lista inteira e o advisory lock serializa editores concorrentes (add/remove atômico seria 2 formas de escrita para o mesmo dado). +5. **Busca de candidatos (UI)** — server action `searchDemandResponsibleOptions(query, municipalityId)` (padrão `searchDemandActivityOptions`): query < 2 chars → sugestões = assessores do município da demanda (read scoped de `municipality.advisors`); senão → busca por nome em `campaignUser` com `eligibleCampaignStaffWhere` (reusa o padrão de `loadEligibleAdvisorOptions`), `overrideAccess: false` (campaign users leem `campaignUser` — `canReadCampaignUsers`). Sugestão NÃO é herança: é atalho de preenchimento, a visibilidade vem só do campo salvo. +6. **Componente de multi-select** — `DemandResponsibleMultiSelect` novo em `src/components/campaign/demand/` (domain, não shared): single collection, chips removíveis, chip do criador rotulado "Você (criador)", sugestões no diálogo quando query curta, hidden inputs repetidos `responsibles` (contrato de `RelationMultiSelect`, não o JSON de `ResponsibleMultiSelect`). **Não reusar `ResponsibleMultiSelect` (C90)**: é polimórfico (typeLabel/relationTo), tem cap hardcoded `MAX_ACTIVITY_RESPONSIBLES=20`, e o contrato de FormData é JSON tipado — forçar modo single-collection seria 4 props novas + branch, um módulo raso; dois componentes com contratos distintos é honesto (mesmo critério que separou `RelationMultiSelect` de `ResponsibleMultiSelect`). +7. **Detalhe** — card `DemandResponsiblesCard` (client) com a lista (nome + avatar via `loadCampaignUserDisplayByIds`, helper existente), add por combobox, remove por chip, e o rótulo estático "Visível para: Responsáveis · Candidato · Coordenador". Layout do rascunho: grade `lg:grid-cols-3` — coluna 1–2 com os blocos atuais (descrição, decisão, workflow, controle interno), coluna 3 com o card. Todos os readers são updaters (update access = read access), então o card de gestão aparece para qualquer staff que abra a demanda — sem flag. +8. **Criação** — `DemandFields` ganha o campo responsáveis (opcional via prop `responsiblesMunicipalityId`/`currentUser`): `/demandas/nova` e wizard compartilham o mesmo parse (`parseCampaignDemandCreateFormData` ganha `responsibles: formData.getAll('responsibles')`), schema de create ganha `responsibles: z.array(positiveRelationshipId)` (sem cap — como `municipality.advisors`; o hook faz o union, dedup implícito). Draft: `/demandas/nova` em grade 2 colunas (form | responsáveis); wizard empilhado (o wizard herda via `DemandFields`). + +### Componentes / mudanças + +- **`CampaignDemand.responsibles`** (`src/collections/CampaignDemand.ts`): `relationship hasMany → campaignUser`, label "Responsáveis", index; sem field access (row access já gateia; staff nativo não abre `/admin`). +- **`enforceDemandWorkflow`** (`CampaignDemand.ts`): union do criador no create (campaignUser only). +- **`canReadCampaignDemand`/`canUpdateCampaignDemand`** (`src/utilities/access/demands.ts`): regra de membership; remove `resolveActorScopedRead`/`getAccessibleMunicipalityIds` deste módulo. +- **Migration:** `add_campaign_demand_responsibles` (drizzle): `ALTER TABLE campaign_demand_rels ADD COLUMN campaign_user_id` + FK (`ON DELETE cascade`, padrão `state_deputy_rels`) + índice; + backfill no up. Gerar com `pnpm migrate:create`. +- **Access / Consent:** sem Consent (dado interno staff). Fail-closed no where da access. +- **Server actions:** `updateCampaignDemandResponsiblesRecord` + `setDemandResponsiblesFormAction` (`[slug]/formActions.ts`, `revalidatePath`); `searchDemandResponsibleOptions` (`demandas/responsibleSearchActions.ts`). +- **UI:** `DemandResponsibleMultiSelect` + `DemandResponsiblesCard` em `src/components/campaign/demand/`; `DemandFields`/`DemandForm`/`WizardRegisterDemandStep` ganham o campo; `[slug]/page.tsx` ganha o card + grade lg. Impeccable B — seguir o rascunho (`demandas-responsaveis-ui-draft-*.png`): chips, placeholder "Buscar assessor…"/"Adicionar responsável…", textos informativos do rascunho. +- **View-model:** `DemandDetailViewModel.responsibles: Array<{ id, name, avatarUrl }>` via `loadCampaignUserDisplayByIds`; `nova/page.tsx` carrega o `currentUser` (id/nome) para seed do picker. + +### Dados → forma + +- Sem dados apresentados ao usuário além de nomes/avatares de responsáveis — forma já existente (chips com nome + avatar, padrão do código). Lista da demanda **não** ganha coluna de responsáveis (fora do aceite — apenas visibilidade muda). + +## Fases verificáveis + +1. **Schema + server (tracer)** — migration + campo + hook do criador + access rule; `pnpm generate:types`; int tests de access reescritos/adicionados (`campaignDemandWorkflow.int.spec.ts`, `homeSearchDemands.int.spec.ts`); backfill verificado no local. +2. **Ações + busca** — `updateCampaignDemandResponsiblesRecord` + zod + `searchDemandResponsibleOptions`; int de gestão (add/remove/self-removal, advisor não-responsável não lê). +3. **UI** — multi-select na criação (nova + wizard) e card no detalhe; shape → critique → polish contra o rascunho; e2e: assessor não-responsável não vê (lista vazia + URL → 404); ajustar e2e existentes se o aceite mudou o comportamento (criador auto-responsável preserva os fluxos atuais). +4. **Gates** — `tsc`, `lint`, `format:check`, `knip`, `check:cycles`, unit+int, `build` local; e2e afetado no CI. Aikido nos arquivos tocados. + +## Rabbit holes / Não escopo (engenharia) + +- **Notificação ao ser marcado responsável** (fora de escopo da intenção). +- **Coluna de responsáveis na lista / omnibox** — visibilidade herda a access, sem mudança de UI de lista. +- **C141 "Visão Tudo"** — não existe ainda; a regra nova já falha fechado para assessor em qualquer perfil futuro. +- **Generalizar `ResponsibleMultiSelect`/`RelationMultiSelect`** — rejeitado (contratos distintos, módulo raso). +- **Overflow "+N" dos chips no detalhe** — o padrão do código é wrap; o "+2" do rascunho é affordance de wireframe, não spec. + +## Riscos e mitigação + +- **Regra nova muda o que assessores veem em todos os pontos de demanda** (lista, detalhe, busca home, activity detail, C90). Mitigação: tudo herda por `overrideAccess: false` — um único ponto de corte; int tests cobrem cada superfície; C90 cria com `user: currentActor` → criador auto-responsável → a atividade continua listando as próprias demandas. +- **Self-removal deixa a demanda sem responsável** → visível só a candidato/coordenador (fail-closed, aceitável e coerente com o modelo explícito — gestão é dos responsáveis). +- **Join table já existente** (`campaign_demand_rels` com `path='media'`) — a migration adiciona coluna/FK; conferir o SQL gerado (não regenerar a tabela). +- **`codebaseConventions.unit.spec.ts`** — o guard anti-fragmento municipal não é violado (a regra nova não re-escreve `{ municipality: { in } }`); rodar o guard no gate. + +## Aceite de engenharia + +- [x] Aceite de produto da intenção ainda coberto (visibilidade fail-closed por responsáveis explícitos; criador automático; gestão por quem atualiza; leader lockdown; escalada inalterada) +- [x] Invariantes AGENTS/engineering-standards (Local API com `user`/`overrideAccess: false`; transação + advisory lock nas escritas multi-step; pt-BR em labels, identificadores em inglês) +- [x] Testes de domínio previstos (int: access rule, gestão de responsáveis, backfill; e2e: invisibilidade para não-responsável; adjust: workflow/homeSearch/listData) diff --git a/docs/plans/feed-instagram-nao-aparece-home-impl.md b/docs/plans/feed-instagram-nao-aparece-home-impl.md new file mode 100644 index 00000000..41be3229 --- /dev/null +++ b/docs/plans/feed-instagram-nao-aparece-home-impl.md @@ -0,0 +1,123 @@ +# Impl: Feed do Instagram configurado não aparece na home pública + +Status: aprovado +Atualizado em: 2026-08-19 +Issue: #115 +Intenção: docs/plans/feed-instagram-nao-aparece-home.md +Appetite restante: ~1 dia eng + +## Leitura da intenção + +- **Outcome:** a assessoria vê, na própria global "Feed de redes sociais", o estado da sincronização do Instagram — "Sincronizado · há X min · N posts", "Falha na última sincronização" com o motivo em linguagem de produto e a correção esperada, ou "Instagram ainda não configurado" — e tem um botão "Tentar sincronizar de novo" para feedback imediato. O board público permanece fail-closed como hoje (artigos + YouTube quando o IG falha); a home pública não muda em nada. +- **O que NÃO negociar:** painel de status por plataforma na própria global, só estado atual + motivo (sem histórico, sem alertas, sem dashboard); sem wizard OAuth; sem embeds no site público; token nunca exposto fora do admin (`read` admin-only preservado); copy em pt; identificadores em inglês; a home pública nunca quebra nem vaza erro. +- **O que reavaliar:** a hipótese da intenção apontava `InstagramPostExclusionPicker` como superfície do status — o picker é a lista de exclusão (S3) e **fica intacto**; o painel de status é um **irmão** (`InstagramSyncStatusPanel`), mesma família de componente `admin.components`, mesmo padrão do rascunho (caixa de status separada da lista). `revalidateRequest.ts` realmente não muda (a tag `social-feed` já cobre). O diagnóstico da ocorrência em produção é impossível deste worktree (sem acesso ao DB de prod — `PROD_DATABASE_URL` não existe localmente): o próprio painel vira o instrumento de diagnóstico (1 clique após o deploy confirma a causa), e o runbook documenta validação/geração de token. + +## Abordagem recomendada + +```mermaid +flowchart LR + A[Global SocialFeedSettings<br/>+ instagramSyncStatus jsonb] --> H[afterChange<br/>revalidate + sync se credencial mudou] + H --> S[syncInstagramFeed<br/>load + snapshot + status] + B[Botão Tentar sincronizar] --> R[POST /api/social-feed/sync<br/>auth admin + same-origin] + R --> S + C[Render home<br/>getInstagramFeed · unstable_cache] -->|sucesso| P[persist snapshot + status<br/>SQL cru, sem hooks] + C -->|falha| Q[persist status erro +<br/>fallback snapshot] + S --> P + S --> Q + P --> G[(social_feed_settings)] + Q --> G + G --> PANEL[InstagramSyncStatusPanel<br/>ui field · 5 estados] +``` + +**Opções consideradas:** A | B | C +**Recomendação:** A — estado da sincronização **persistido** numa coluna `jsonb` da própria global (escrito por render, hook e botão, todos pelo mesmo módulo `instagramSync.ts`); painel admin como campo `ui` irmão do picker; rota `POST /api/social-feed/sync` (auth de sessão admin + `isSameOriginRequest`) para o botão; erro da API mapeado para copy de produto (`InstagramApiError` + `describeInstagramError`). O caminho de render ganha apenas a persistência do status dentro do try/catch existente — zero mudança visual na home, fail-closed intacto. +**Rejeitadas:** B) estado derivado em runtime (o painel admin não renderiza a home — sem estado não há como mostrar nada; e a intenção exige o estado da **última** sincronização, que precisa sobreviver a restart) | C) server action no botão (primeiro uso sem precedente no shell do admin do Payload; rota HTTP é previsível, testável e segue o precedente de `/api/revalidate`). + +### Decisões de engenharia + +1. **Estado persistido como `instagramSyncStatus` (jsonb, campo hidden) — uma coluna, shape `{ lastSyncAt?, postCount?, error?, errorAt? }`.** + Opções: A) uma coluna jsonb (padrão do `instagramFeedSnapshot`) | B) 3 colunas (`lastSyncAt`, `postCount`, `lastError`) | C) estado em memória/cache. + Recomendação: A — o snapshot já é jsonb escrito por SQL cru; uma coluna nova espelha o padrão; o shape é autodescritivo e muda sem migration futura. `lastSyncAt`/`postCount` são da última sincronização **bem-sucedida**; `error`/`errorAt` da última **falha** (falha após sucesso preserva os campos de sucesso? **não** — o painel em falha mostra só o erro, cena 1 do rascunho; overwrite é mais simples e sem leitura-modificação-escrita). + Rejeitadas: B 3 colunas = 3 DDLs para o mesmo dado; C não sobrevive a restart e o painel admin não compartilha o render. + → **Migration:** `pnpm migrate:create add_instagram_sync_status` (ADD COLUMN jsonb nullable, não destrutiva; revisar o DDL gerado — precedente S2/S3 de drift em migrações hand-written). + +2. **`InstagramApiError` (status + apiMessage) no `loadInstagramFeed` + `describeInstagramError` para copy de produto.** + Opções: A) o fetch parseia o body de erro da Graph API (`{ error: { message, type } }`) e lança erro tipado; `describeInstagramError` mapeia para pt-BR com a correção | B) mensagem genérica "falhou com status N" (status quo) | C) mapa estático por status. + Recomendação: A — o produto exige o "porquê" (token recusado vs ID inválido vs rede); o body da Graph API distingue OAuthException (token inválido/expirado/emitido via Facebook Login — copy do draft) de "Invalid user id"; status 5xx = API indisponível; `TypeError` de fetch = rede. O mesmo `describeInstagramError` serve render, hook e rota (uma spelling). + Rejeitadas: B é o bug atual (assessoria cega); C não lê o body e não cobre casos reais. + - Mapeamentos: `400|401|403` + OAuthException/`expired|invalid|revoked` → "O token do Instagram foi recusado pela Graph API — está inválido/expirado ou foi emitido via Facebook Login (a Graph API só aceita tokens gerados pelo Instagram Login). Gere um novo token de longa duração pelo Instagram Login e atualize o campo acima."; `400` + menção a user/id → "O ID do usuário não foi reconhecido pela Graph API. Confira se é o ID numérico da conta Business/Creator."; `5xx`/outros → "A Graph API está indisponível no momento (status N). Tente novamente em alguns minutos."; `TypeError` de fetch → "Não foi possível falar com a API do Instagram (rede indisponível)."; desconhecido → "Erro inesperado ao sincronizar o Instagram." + +3. **Rota `POST /api/social-feed/sync` para o botão — auth de sessão admin + CSRF same-origin.** + Opções: A) route handler em `(frontend)/api/social-feed/sync` com `payload.auth({ headers })` exigindo `collection === 'users'` + `isSameOriginRequest` (helper já existente) | B) server action | C) rota sem auth (confia no botão). + Recomendação: A — o precedente `sameOriginRequest.ts` é literalmente para "cookie-authenticated JSON route handlers"; `payload.auth` lê o cookie `payload-token` da sessão admin (precedente `campaignAuth`); a rota vive ao lado de `/api/revalidate` (grupo `(frontend)` — route handlers não passam por layouts; o URL é `/api/social-feed/sync`). Retorna `{ ok, status }`; status `null`/400 quando IG não configurado (o botão nem renderiza nesse estado — defesa). + Rejeitadas: B sem precedente no shell admin (risco de incompatibilidade com o RSC do Payload, custo de spike); C permite abuso do rate da Graph API. + → Depois do sync, a rota chama `revalidateTag('social-feed')` (fora de cache — permitido). + +4. **Sync no `afterChange` da global quando a credencial IG muda (awaited, bounded, à prova de erro).** + Opções: A) hook `afterChange` [revalidateFeed, syncInstagramAfterChange] — sync **awaited** quando `doc` ≠ `previousDoc` em `instagramAccessToken`/`instagramUserId`/`instagramEnabled` (false→true); `AbortSignal.timeout(10_000)` no fetch; try/catch total — erro de sync nunca quebra o save | B) sync em todo save | C) sem hook — só botão e render. + Recomendação: A — o fluxo da intenção é "preenche token + ID e salva → o admin mostra o estado": com o sync awaited, o reload da global após o salvar já mostra o resultado **com as credenciais novas** (o estado antigo mostraria a falha da credencial antiga — confuso e o problema do status quo). Gate por mudança: save de exclusões/YouTube não gasta chamada de API nem adiciona latência. Timeout: Graph API responde em <1s mesmo em erro; 10s cap em API pendurada. + Rejeitadas: B latência e custo em todo save sem ganho; C deixa o fluxo principal da intenção em dois cliques e o estado inicialmente mentiroso. + → O hook usa o MESMO `syncInstagramFeed` da rota (uma implementação, dois call sites). + +5. **Painel `InstagramSyncStatusPanel` (campo `ui`, irmão do picker) — 5 estados.** + Opções: A) componente client próprio lendo os campos do form via `useAllFormFields` + estado local pós-botão | B) estender o picker | C) campo `json` não-hidden renderizado cru. + Recomendação: A — o picker prova o padrão (campo `ui` + componente client + `useAllFormFields`); o painel lê `instagramSyncStatus` (inicial), `instagramAccessToken`/`instagramUserId`/`instagramEnabled` e mantém estado local só para o pós-clique (o status já está persistido no servidor — o form não precisa ser modificado). Estados: (1) `instagramEnabled === false` → "Instagram desativado — os posts não aparecem no board."; (2) sem token ou sem ID → "Instagram ainda não configurado" + explicação (cena 3); (3) `error` → "Falha na última sincronização" + mensagem + botão "Tentar sincronizar de novo" (cena 1); (4) `lastSyncAt` → "Sincronizado · há X min · N posts" + "Próxima atualização automática em ~5 min. Os N posts mais recentes estão no board 'Acompanhe de perto' da home." (cena 2, sem botão — fiel ao rascunho); (5) configurado sem status → "Aguardando a primeira sincronização" + botão. Pós-clique: `pending` ("Sincronizando…") → sucesso cena 2 / falha cena 1 com o motivo retornado. Tempo relativo com `Intl.RelativeTimeFormat('pt-BR')` local (o painel é client; `formatRelativePostDate` é server-only — `posts.ts` abre com `import 'server-only'`). + Rejeitadas: B mistura duas responsabilidades no picker (o picker é lista de exclusão; o status é estado operacional — o rascunho os desenha como caixas separadas); C expõe JSON cru (anti-produto). + → **UI:** Impeccable C — encaixe na linguagem do admin (caixas zinc/red do draft, `text-sm`, bordas `rounded-lg`), shape → craft → critique → polish no navegador (admin, 3 cenas + estados). + +6. **`loadInstagramFeed` ganha `signal?: AbortSignal` (repasse ao `fetchImpl`).** + O hook precisa do deadline; o `LoadInstagramFeedArgs` aceita `init` no fetchImpl — o signal flui sem mudar a assinatura do fetch. Unit-testado. + +7. **Stub IG ganha estado `invalid-token` (400 com body `{ error: { type: 'OAuthException', ... } }`).** + A cena 1 do draft é literalmente esse erro; o e2e do painel precisa exercitá-lo (o estado `fail` atual responde 500 genérico). O painel de falha com a copy do token é o aceite central do item. + +### Componentes / mudanças + +- **`InstagramApiError` + `parseInstagramErrorBody`** (`src/utilities/socialFeed/instagramFeed.ts`, estender): parseia body de erro em resposta não-2xx, lança `InstagramApiError` (`status` + `apiMessage`); `loadInstagramFeed` aceita `signal` opcional repassado ao `fetchImpl` (fetch do usuário, media e refresh). +- **`src/utilities/socialFeed/instagramSync.ts`** (novo, `server-only`, dentro do subfolder `socialFeed/` — a pin de top-level da `codebaseConventions.unit.spec.ts` não se aplica a subfolders, o comentário já cobre o domínio): + - `InstagramSyncStatus` (`{ lastSyncAt?: string; postCount?: number; error?: string; errorAt?: string }`). + - `describeInstagramError(cause: unknown): string` — mapeamentos da Decisão 2 (puro, exportado p/ unit). + - `successInstagramSyncStatus(postCount)` / `failedInstagramSyncStatus(error)` — builders puros. + - `persistInstagramSyncStatus(payload, status)` — SQL cru UPDATE+INSERT (mesmo padrão/precondição do snapshot; rodado DENTRO de `unstable_cache` no render — sem hooks). + - `syncInstagramFeed(payload, { signal? }): Promise<{ ok: true; status } | { ok: false; status }>` — lê a global, retorna "não configurado" sem tocar na API quando `enabled === false`/`instagramEnabled === false`/sem token/ID; sucesso → `persistInstagramSnapshot` + persist token renovado (reuso dos privados? **não** — os `persist*` atuais são module-private do `instagramFeed.ts`; exportar `persistInstagramSnapshot`/`persistInstagramAccessToken` do `instagramFeed.ts` para o sync module — o dono do persist é o feed, o sync é o orquestrador) + status de sucesso; falha → status de erro via `describeInstagramError`; nunca lança (status é o resultado). +- **`getInstagramFeed`** (`instagramFeed.ts`, estender): no try (sucesso) persiste também o status de sucesso; no catch persiste o status de erro ANTES do fallback snapshot (a ordem não importa para o board; importa para o painel). Nada mais muda no caminho público. +- **`SocialFeedSettings`** (`src/globals/SocialFeedSettings.ts`, estender): campo `instagramSyncStatus` (json, `admin.hidden: true` — o painel renderiza, o dado não aparece cru); campo `ui` `instagramSyncStatusPanel` (após `instagramEnabled`, antes do token — status no topo do bloco IG, fiel ao rascunho; `admin.components.Field` → painel); `afterChange` = `[revalidateFeed, syncInstagramAfterChange]` (gate da Decisão 4, try/catch total). Copy do campo token/`admin.description`: ajuste de 1 linha para apontar o painel de status ("veja o estado da sincronização abaixo/ao lado") — opcional, não bloquear. +- **`src/app/(frontend)/api/social-feed/sync/route.ts`** (novo): `POST`; `isSameOriginRequest` (401 se cross-origin); `payload.auth({ headers })` exigindo `user.collection === 'users'` (401 sem sessão admin); lê a global, IG desconfigurado → `400 { ok: false, error }`; `syncInstagramFeed` com `AbortSignal.timeout(10_000)`; `revalidateTag(REVALIDATE_SOCIAL_FEED_TAG)`; `200 { ok, status }`. Erro interno → `500` (sem stack). +- **`src/components/admin/InstagramSyncStatusPanel.tsx`** (novo, `'use client'`): estados da Decisão 5; fetch para a rota no clique; `aria-busy` no pending; sem escrita no form (status é persistido no servidor). +- **`tests/e2e/instagram-stub.mjs`** (estender): estado `'invalid-token'` → `400 { error: { type: 'OAuthException', message: 'Error validating access token' } }` em `/user` e `/media` (e refresh → 400); `POST /__stub/state` aceita o novo estado. +- **Migration:** `add_instagram_sync_status` (jsonb nullable). **Types/importmap:** `pnpm generate:types` + `pnpm generate:importmap` (com envs `S3_*` dummy setadas — OPS69). **Access/Consent:** sem consent (estado operacional interno, não PII); global segue admin-only; a rota exige sessão admin. **UI:** Impeccable C (encaixe no admin; shape → craft → critique → polish). + +### Dados → forma + +- O painel mostra **estado operacional**, não métrica: contagem de posts sincronizados (cru, da API), tempo desde a última boa, motivo da última falha. Nada de histórico, alertas ou engajamento (corte da intenção). Forma: caixas do rascunho (verde/zinc neutro sucesso, red falha, zinc neutro não-configurado) — contraste e semântica visuais primeiro, sem cor inventada. + +## Fases verificáveis + +1. **Schema + servidor** (~metade do appetite): migration + `generate:types`; `InstagramApiError` + parse do body + `signal` no `loadInstagramFeed`; `instagramSync.ts` (status builders + `describeInstagramError` + persist SQL + `syncInstagramFeed`); persistência de status no `getInstagramFeed`; rota `/api/social-feed/sync`. Unit tests: `describeInstagramError` (OAuthException→token, invalid-user→ID, 5xx→indisponível, TypeError→rede, unknown→inesperado), `loadInstagramFeed` com fetchImpl 400+body de erro lançando `InstagramApiError`, repasse de `signal`, builders de status, gate `instagramCredentialsChanged` (doc/previousDoc: token mudou, ID mudou, enabled false→true, exclusão não dispara). Gates parciais: tsc + `pnpm test`. +2. **Painel + hook + runbook** (restante): `InstagramSyncStatusPanel` + campo `ui` + hook `afterChange` com sync awaited; stub `invalid-token`; runbook `docs/ops/instagram-feed-token-runbook.md` (o que o painel mostra; validação do token via refresh/me com curl; geração pelo Instagram Login vs Facebook Login; a provável causa da ocorrência atual e como o painel confirma em 1 clique pós-deploy); alinhar a copy do vazio do picker com o rascunho ("Nenhum post sincronizado. Após uma sincronização bem-sucedida, os últimos posts aparecem aqui…"); `generate:importmap` + AGENTS.md (1 linha: painel de status + rota de sync na seção do `social-feed`). Polish no navegador (admin, 3 cenas). +3. **e2e + gates finais**: admin smoke — seed com token+ID → painel "Aguardando a primeira sincronização" → clique → "Sincronizado" com contagem + assert do `instagramSyncStatus` via REST; stub `invalid-token` → falha com copy do token + botão; stub `fail` → falha 500; reset do helper `resetSocialFeedSettings` ganha `instagramSyncStatus: null` (determinismo do serial); smoke do picker existente deve continuar verde (o hook passa a sincronizar no seed com credenciais — stub ok, determinístico). Gates: `pnpm gate:fast`, `pnpm test`, `pnpm build` local, e2e no CI; changelog `docs/changelog/2026-08-19-s11.md` + `pnpm changelog:build`. + +## Rabbit holes / Não escopo (engenharia) + +- Não sincronizar no salvar de exclusões/YouTube (gate por credencial — Decisão 4). +- Não fazer histórico/alerta/dashboard de saúde; não tocar no board público nem nos cards. +- Não mexer no fluxo de refresh de token existente (o `loadInstagramFeed` já refresca e persiste; o sync só reporta). +- Não paginar o feed, não buscar stories/métricas. +- Não backfill de status retroativo (a primeira sincronização após o deploy escreve o estado). +- Não acesso ao DB de prod para o diagnóstico (fora do alcance do worktree); o painel + runbook cobrem o aceite. +- Não mudar a pin de `codebaseConventions` (o módulo novo fica no subfolder `socialFeed/` já documentado). + +## Riscos e mitigação + +- **Rota auth no admin:** `payload.auth({ headers })` com cookie `payload-token` — precedente `campaignAuth`; sem sessão → 401 silencioso (o painel mostra o erro como falha de sincronização? **não** — o painel trata 401 como "sessão expirada": recarrega a página para o admin re-autenticar; copy honesta). e2e cobre. +- **Hook sync com latência no save:** cap `AbortSignal.timeout(10_000)`; sync nunca lança (try/catch); save de credenciais é raro (assessoria configura uma vez). +- **`revalidateTag` dentro de cache:** a persistência de status no render usa SQL cru (sem hooks) — mesma restrição documentada do snapshot (Next 15.4 lança `revalidateTag` dentro de `unstable_cache`); a rota/hook revalidam fora do cache (permitido). +- **e2e serial com hook novo:** todo seed de credenciais via REST dispara o sync (stub determinístico, ok/fail por estado); resets incluem `instagramSyncStatus: null`; orçamento de polling do serial mantido. +- **Formato do body de erro da Graph API:** documentado (`{ error: { message, type } }`); parse defensivo — body inesperado → mensagem genérica por status (nunca quebra o fluxo). +- **Painel com estado stale após save:** o sync awaited no hook resolve (status escrito antes do reload); botão cobre o resto; "há X min" é honesto (última boa real). + +## Aceite de engenharia + +- [ ] Aceite de produto da intenção coberto: painel de status na global (sincronizado/falha com motivo+não configurado), botão "Tentar sincronizar de novo", home pública inalterada e fail-closed, runbook curto com validação/geração de token, token nunca fora do admin +- [ ] Invariantes AGENTS/engineering-standards: migration não destrutiva; sem consent novo; identificadores em inglês; copy/admin em pt; access admin-only preservado; rota com auth de sessão + CSRF same-origin; tag `social-feed` já cobre revalidação; SQL cru fora de cache +- [ ] Testes de domínio previstos: unit (mapeamentos de erro, `InstagramApiError`, signal, builders, gate do hook) + e2e (painel ok/falha token/falha 500, status persistido via REST, serial determinístico) diff --git a/docs/plans/feed-instagram-nao-aparece-home-ui-draft-admin-falha-token.png b/docs/plans/feed-instagram-nao-aparece-home-ui-draft-admin-falha-token.png new file mode 100644 index 00000000..7ff0878d Binary files /dev/null and b/docs/plans/feed-instagram-nao-aparece-home-ui-draft-admin-falha-token.png differ diff --git a/docs/plans/feed-instagram-nao-aparece-home-ui-draft-admin-nao-configurado.png b/docs/plans/feed-instagram-nao-aparece-home-ui-draft-admin-nao-configurado.png new file mode 100644 index 00000000..505f5b9f Binary files /dev/null and b/docs/plans/feed-instagram-nao-aparece-home-ui-draft-admin-nao-configurado.png differ diff --git a/docs/plans/feed-instagram-nao-aparece-home-ui-draft-admin-ok.png b/docs/plans/feed-instagram-nao-aparece-home-ui-draft-admin-ok.png new file mode 100644 index 00000000..57063668 Binary files /dev/null and b/docs/plans/feed-instagram-nao-aparece-home-ui-draft-admin-ok.png differ diff --git a/docs/plans/feed-instagram-nao-aparece-home-ui-draft.html b/docs/plans/feed-instagram-nao-aparece-home-ui-draft.html new file mode 100644 index 00000000..8a9607b8 --- /dev/null +++ b/docs/plans/feed-instagram-nao-aparece-home-ui-draft.html @@ -0,0 +1,315 @@ +<!doctype html> +<html lang="pt-BR"> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script> + <title>UI draft — Feed do Instagram configurado não aparece na home + + +
+

+ Assessoria de comunicação configura o feed do Instagram no admin (global "Feed de redes + sociais") e precisa ver, no mesmo lugar, por que os posts não aparecem na home pública. +

+ + +
+

Cena 1 — token recusado pela Graph API

+
+
+
+ + Configurações + + / Feed de redes sociais +
+ + Draft · Salvar · Cancelar + +
+ +
+

Feed de redes sociais

+

+ Fonte automática do board "Acompanhe de perto" da home de campanha. Sem chave + configurada, os cards da plataforma não aparecem. +

+ +
+

Falha na última sincronização

+

+ A Graph API recusou o token configurado (token inválido/expirado ou emitido via + Facebook Login — a Graph API só aceita tokens gerados pelo Instagram Login). + Enquanto isso, nenhum post do Instagram aparece na home. +

+

+ Correção: gere um token de longa duração pelo Instagram Login + (conta Business/Creator vinculada à página) e atualize o campo abaixo. +

+ +
+ +
+
+ +
+ IGAA...abc123 +
+
+
+ +
+ 17841400000000000 +
+
+
+ +
+ 3 +
+
+
+ +
+

Instagram — posts recentes

+

+ Nenhum post sincronizado. Após uma sincronização bem-sucedida, os últimos posts + aparecem aqui para marcar os que não devem ir para a home. +

+
+ +
+ + +
+
+
+
+ + +
+

Cena 2 — sincronizado com sucesso

+
+
+
+ + Configurações + + / Feed de redes sociais +
+ + Draft · Salvar · Cancelar + +
+ +
+

Feed de redes sociais

+ +
+

Sincronizado · há 2 min · 4 posts

+

+ Próxima atualização automática em ~5 min. Os 4 posts mais recentes estão no board + "Acompanhe de perto" da home. +

+
+ +
+
+ +
+ IGAA...abc123 +
+
+
+ +
+ 17841400000000000 +
+
+
+ +
+ 3 +
+
+
+ +
+

Instagram — posts recentes

+

+ Marque "Não exibir" para tirar um post do board (ex.: posts de grade). O board pula + para o próximo elegível. +

+
    +
  • + + + + Caravana pelo interior: chegada em Vitória da Conquista + + há 2 h · ID 17841400000000000_001 + + + Exibir + +
  • +
  • + + + + Foto do plenário: votação da pauta da saúde + + ontem · ID 17841400000000000_002 + + + Exibir + +
  • +
  • + + + + Post de grade (sem legenda) — para marcar "Não exibir" + + ontem · ID 17841400000000000_003 + + + Não exibir + +
  • +
  • + + + + Agenda da semana: agendas abertas em Salvador + + há 2 dias · ID 17841400000000000_004 + + + Exibir + +
  • +
+
+ +
+ + +
+
+
+
+ + +
+

+ Cena 3 — sem token nem ID (primeiro acesso) +

+
+
+
+ + Configurações + + / Feed de redes sociais +
+ + Draft · Salvar · Cancelar + +
+ +
+

Feed de redes sociais

+ +
+

Instagram ainda não configurado

+

+ Informe o token de acesso e o ID da conta para publicar posts do Instagram na home. + Sem configuração, o board funciona com artigos e YouTube. +

+
+ +
+
+ +
+ (vazio) +
+
+
+ +
+ (vazio) +
+
+
+ +
+ + +
+
+
+
+
+ + diff --git a/docs/plans/feed-instagram-nao-aparece-home.md b/docs/plans/feed-instagram-nao-aparece-home.md new file mode 100644 index 00000000..e467f93a --- /dev/null +++ b/docs/plans/feed-instagram-nao-aparece-home.md @@ -0,0 +1,93 @@ +# Feed do Instagram configurado não aparece na home pública + +Status: aprovado +Atualizado em: 2026-08-19 +Issue: #115 +Priority: P1 +Model: cursor-grok-4.5-medium +Impeccable: C — encaixe na global do admin (painel de status de sincronização) +Rascunho UI: docs/plans/feed-instagram-nao-aparece-home-ui-draft.html + PNGs embutidos abaixo +Appetite: ~1 dia eng + diagnóstico da causa em produção +Responsável: — + +## Intenção + +A assessoria preencheu o token e o ID do Instagram na configuração do feed (global "Feed de redes sociais", entregue na S3) e os posts **não aparecem** na seção "Acompanhe de perto" da home pública — em produção, o board mostra YouTube e artigos, nenhum card do Instagram e nenhum link "Seguir no Instagram →" (verificado em 2026-08-19 em jorgesolla1313.com.br). O desenho da S3 é fail-closed **silencioso**: qualquer falha da API (token recusado, ID inválido, rede) degrada para "sem cards" sem deixar rastro — o único indício hoje é o picker do admin vazio ("Nenhum post recente"), que não diz **por quê**. A assessoria fica num ciclo cego de tentativa-e-erro de credenciais. + +## Persona e fluxo + +- **Persona / contexto:** assessoria de comunicação, configurando a global no admin do Payload (grupo "Configurações"), sem acesso ao código nem a logs. +- **Job principal:** saber, no mesmo lugar onde configurou o feed, se a sincronização do Instagram funcionou — e, se não, **por quê** e **o que fazer**. +- **Fluxo desejado:** + 1. A assessoria preenche token + ID e salva a global. + 2. O admin mostra o estado da sincronização: "Sincronizado · há X min · N posts" (e os posts aparecem no picker, prontos para exclusão), ou "Falha na última sincronização" com o motivo e a correção esperada (ex.: token inválido/expirado ou emitido via Facebook Login — gerar pelo Instagram Login). + 3. Com sincronização OK, os posts aparecem na home pública em minutos (cache revalidado no salvar). + 4. A causa da ocorrência atual fica diagnosticada e documentada num runbook curto para a assessoria não repetir o ciclo. +- **Anti-goals de produto:** nada de wizard OAuth "conectar com Facebook" dentro do admin; nada de dashboard de saúde de feeds; nada de embeds/scripts do Instagram no site público; nada de métricas de engajamento. + +### Esboço de fluxo (B/C/D) + +```text +[admin: global Feed de redes sociais] + → salvar token+ID → [status: Sincronizado · N posts] → [home pública: cards IG em minutos] + → [status: Falha + motivo + correção] → [assessoria corrige credencial e salva] → [status OK] +``` + +### Rascunho UI (B/C/D) + +Painel de status de sincronização por plataforma na global do admin, embutido junto aos campos do Instagram (e reaproveitável para YouTube depois, se a paridade for pedida). + +![Rascunho UI — admin, falha de token](feed-instagram-nao-aparece-home-ui-draft-admin-falha-token.png) + +![Rascunho UI — admin, sincronizado com picker](feed-instagram-nao-aparece-home-ui-draft-admin-ok.png) + +![Rascunho UI — admin, não configurado](feed-instagram-nao-aparece-home-ui-draft-admin-nao-configurado.png) + +## Objetivo e aceite + +- Com credenciais válidas (token gerado pelo Instagram Login + ID da conta Business/Creator), os posts do Instagram aparecem na seção "Acompanhe de perto" da home em até alguns minutos após salvar a configuração — badge "Instagram", legenda, data relativa, clique abre na plataforma. Sem ação manual além da config. +- Quando a Graph API recusa as credenciais ou a sincronização falha, o admin mostra o motivo em linguagem de produto e a correção esperada (ex.: "token inválido/expirado ou emitido via Facebook Login — gere pelo Instagram Login"). A assessoria deixa de ficar cega. +- A home pública continua fail-closed: API fora ou credenciais inválidas nunca quebram a página nem vazam erro — o board segue com artigos + YouTube (comportamento atual preservado). +- A causa da ocorrência em produção fica diagnosticada e registrada em runbook curto (passos para gerar/validar o token), para o ciclo de tentativa-e-erro não se repetir. +- Guardrails: token nunca exposto fora do admin (read da global admin-only, como hoje); zero embeds/scripts do Instagram no site público (LCP); identificadores em inglês, copy em pt. + +## Dados (intenção) + +- **Vou apresentar dados?** Não — o painel mostra estado operacional (última sincronização, nº de posts, motivo de erro), não métricas. Nenhuma decisão de análise desbloqueada. +- **Forma:** adiada ao plano de implementação — sem contadores, sem engajamento. + +## Direção no codebase (hipótese) + +- **Áreas prováveis:** `src/utilities/socialFeed/instagramFeed.ts` (onde o erro é engolido no fail-closed — fonte do "porquê"), `src/globals/SocialFeedSettings.ts` (superfície do status), `src/components/admin/InstagramPostExclusionPicker.tsx` (pickers/superfície admin já existente, mesma família de componente), `src/utilities/revalidateRequest.ts` (tag `social-feed` — o salvar já revalida; não espera-se tocar). +- **Precedente a olhar:** plano/impl da S3 (`docs/plans/secao-conteudos-home-instagram*.md`), irmão `youtubeFeed.ts`, e2e do describe serial da seção (stub IG) e smoke do picker no admin. +- **Risco de acoplamento:** o fetch roda dentro de `unstable_cache` durante render — qualquer persistência de status precisa respeitar a restrição de não chamar `revalidateTag` dentro de cache (precedente documentado da S2/S3). A home pública é ISR; revalidação via tag já cobre o salvar do admin. + +## Dependências + +- **S3** (pai, `done`/`in-prod` — imutável; este é item **sucessor**). + +## Fora de escopo + +- Setup OAuth / "conectar com Facebook" dentro do admin (o fluxo Meta fica com a assessoria; runbook documenta). +- Redesenho do board público ou dos cards (superfície aprovada na S3 não muda). +- Paridade de status para o YouTube (pode virar followup se a plataforma pedir; o desenho do painel é por plataforma). +- Backfill retroativo de snapshot. + +## Rabbit holes de produto + +- **Dashboard de saúde de feeds.** Se alguém "só completar" a ideia de status, vira painel de monitoramento com histórico e alertas. **Corte neste item:** um painel de status por plataforma na própria global, só estado atual + motivo; sem histórico, sem alertas. +- **Wizard OAuth no admin.** Completar o desejo com o fluxo completo da Meta explode o escopo (app Meta, permissões, redirects). **Corte neste item:** credenciais continuam preenchidas manualmente; o valor é diagnóstico e correção, não aquisição de token. +- **Investigação infinita da causa em produção.** A causa pode ser credencial da assessoria (troca fora do nosso controle). **Corte neste item:** diagnóstico com prazo — o que não for descoberto num período curto vira instrução de validação no runbook, não bloqueio do item. + +## Questões em aberto (produto) + +- **Onde a assessoria vê o "porquê"?** **Opções:** A) painel de status na própria global do feed (ao lado dos campos do Instagram) — o lugar onde a assessoria já está quando configura | B) só logs/exceção no servidor (exige acesso técnico — não resolve para a persona) | C) nada além do picker vazio (status quo). **Recomendação:** A — o fracasso acontece onde a configuração acontece; o sinal pertence a esse mesmo lugar. _(assumido — validar com produto)_ +- **O item cobre a ocorrência atual em produção?** **Opções:** A) diagnóstico da causa atual + painel de status (a correção de credencial fica com a assessoria, documentada) | B) só painel de status; a causa atual fica para followup | C) só runbook de credenciais. **Recomendação:** A — sem o diagnóstico, não sabemos se há também um defeito de código por trás do sintoma; sem o painel, o sintoma se repete cegamente. _(assumido — validar com produto)_ +- **Paridade de status para YouTube agora?** **Opções:** A) só Instagram nesta fatia (o YouTube funciona em produção; o painel nasce desenhado por plataforma) | B) YouTube e Instagram juntos. **Recomendação:** A — fatia mínima útil; paridade vira followup barato se pedida. _(assumido — validar com produto)_ + +## Referências + +- Issue pai S3 (#20, `done`/`in-prod`) + `docs/plans/secao-conteudos-home-instagram.md` e `-impl.md` +- Rascunho UI (gate): `docs/plans/feed-instagram-nao-aparece-home-ui-draft.html` + PNGs acima +- Primeiras pistas para o executor: `src/utilities/socialFeed/instagramFeed.ts` (caminho de falha silenciosa), `src/globals/SocialFeedSettings.ts`, `src/components/admin/InstagramPostExclusionPicker.tsx`, `tests/e2e/frontend.e2e.spec.ts` (describe serial da seção) +- `docs/AGENT-OPS.md` (serializes: nenhum recurso compartilhado — `serializes: []`) diff --git a/docs/plans/ops71-ci-github-actions-tracker-forgejo-impl.md b/docs/plans/ops71-ci-github-actions-tracker-forgejo-impl.md new file mode 100644 index 00000000..82d5a013 --- /dev/null +++ b/docs/plans/ops71-ci-github-actions-tracker-forgejo-impl.md @@ -0,0 +1,130 @@ +# Impl: CI/PR do Forgejo Actions de volta para o GitHub Actions — tracker de Issues permanece no Forgejo + +Status: aprovado +Atualizado em: 2026-08-19 +Issue: #97 +Intenção: docs/plans/ops71-ci-github-actions-tracker-forgejo.md +Appetite restante: herdado (~2–3 dias eng; este impl é o item inteiro — OPS72 roda depois com `depends`) + +## Leitura da intenção + +- **Outcome:** código/PR/CI no GitHub Actions (mesma cascata de verificações, + job único `checks`), tracker de Issues no Forgejo por API, deploy **manual** + (`workflow_dispatch`) com dois jobs (`verify` hosted full → `deploy` + self-hosted no homeserver), rollup `CI (PR) / checks` como required check do + GitHub, workstation livre da suíte. +- **O que NÃO negociar:** tracker (Issues/labels/claims) permanece no Forgejo; + estrutura de job único (OPS62) preservada — nada de matrix; deploy NÃO volta + a ser automático; sem migrar Issues; sem segundo deploy (nada de Vercel); + nunca merge com CI vermelho; `pnpm push` continua sendo o caminho canônico. +- **O que reavaliar:** a intenção sugere "auto-merge nativo do GitHub ou + safety net equivalente" e "`gh workflow run` (ou API GitHub)". Reavaliado: + `gh` **não existe** em nenhuma máquina (OPS50) e a filosofia do repo é + plain-Node stdlib — os helpers novos são scripts Node zero-dep sobre a REST + do GitHub, no mesmo molde de `forgejo-api.mjs`. O auto-merge é o **nativo do + GitHub** (GraphQL `enablePullRequestAutoMerge`, rebase) — o servidor espera o + required check; o OPS64 pin (varrer statuses individuais) era defesa contra o + rollup mentiroso do runner Forgejo, que **não existe** no GitHub (o check-run + `CI (PR) / checks` de um job único é o veredito honesto, e o auto-merge nativo + só mergeia com required checks verdes — garantia do servidor). + +## Abordagem recomendada + +```mermaid +flowchart LR + W[worktree local] -->|pnpm push -u origin HEAD| GH[github.com/fsolla/teqo] + GH -->|PR Ready base main Closes #N| CI["ci-pr.yml — job único checks (mesma cascata OPS62)"] + CI -->|required check CI (PR) / checks| AM["agent-pr-ready-automerge.yml — auto-merge nativo (GraphQL, rebase)"] + AM -->|merge| MAIN[main no GitHub] + MAIN -->|PR closed merged| FLIP["issue-done / plan-issue-ready — flip no Forgejo via FORGEJO_API_TOKEN"] + MAIN -->|dispatch manual| V[deploy.yml verify hosted full suite] + V -->|needs| D["deploy runs-on self-hosted homeserver — deploy-homeserver.sh local"] + D --> P[(teqo-1313 produção)] +``` + +**Opções consideradas:** + +- **A — origin vira GitHub; Forgejo como remote secundário congelado; TEQO_REPO_URL → GitHub (repo público, sem auth).** `origin/main` é o base dos gates locais (ci-scope/gate-ci) — precisa apontar para o repo que recebe os merges, senão o diff local compara contra um main defasado. O repo GitHub é público (verificado: `private: false`) → o homeserver clona sem credencial. +- **B — auto-merge via GraphQL nativo.** GitHub espera o required check no servidor; nenhum poll; sem semáforo portado. Draft `cursor/*` → Ready via PATCH; draft não-`cursor/*` = veto (OPS57, preservado). +- **C — deploy.yml com `verify` (hosted, suíte full sem skips) → `deploy` (needs, self-hosted homeserver) executando `deploy-homeserver.sh ` localmente (sem SSH).** +- **D — `forgejo-issue-transition.mjs` / `agent-promote-related-on-merge.mjs` ganham `PR_BODY` env** (mecanismo que `agent-promote-related` já usa): o body do PR vem do evento GitHub; o script flipa no Forgejo por API sem ler o PR do Forgejo (PR lá não existe mais). +- **E — `configure-branch-protection.mjs` reescrito para a REST do GitHub** com lib pura `github-branch-protection.mjs` (regra desejada + drift), no mesmo molde de `branch-protection.mjs`; `enforce_admins: true` preserva o invariante "nunca merge com CI vermelho" inclusive para admin (o rule-based do Forgejo valia para todos). + +**Recomendação:** A+B+C+D+E — porque cada peça reusa o molde existente (plain Node stdlib, libs puras unit-testadas, drift idempotente) e o aceite da intenção exige exatamente esse desenho; nada disso exige binário novo (`gh`) nem semáforo reimplementado. + +**Rejeitadas:** + +- **Instalar `gh` e usar `gh pr create/merge/run`** — binário novo em todas as máquinas + workflows, contra a convenção zero-dep do repo; a REST cobre tudo. +- **Poll-and-merge portado (`waitForChecks` do Forgejo) como mecanismo único** — reimplementa o semáforo que a intenção manda cortar; o OPS64 pin era contra o bug do rollup do Forgejo, inexistente no GitHub com job único + auto-merge nativo. +- **Mirror ativo do Forgejo (`pnpm push` para dois remotes / sync pós-merge)** — nada lê o git do Forgejo em runtime (deploy usa TEQO_REPO_URL=GitHub; flips são API); o repo Forgejo congela em main no ponto do cutover (tracker intacto por API, anti-goal "nunca fecha o tracker" preservado). +- **Deletar os arquivos do Forgejo nesta entrega** — decisão do gate (2026-08-19): o sistema GitHub Actions nasce PRIMEIRO e é validado ao vivo (PR real com CI verde → auto-merge → flips → deploy manual); a remoção de `.forgejo/workflows/` + scripts mortos é **entrega sucessora**, só depois de tudo funcionando. O repo Forgejo congelado no main pré-cutover mantém os workflows como rollback (reversível: religar runner + PRs lá); apagar na primeira entrega queimaria a via de volta. +- **Manter `ci-classify-production.mjs` como check informativo no dispatch** — o hosted não alcança o homeserver (sem SSH) e o `verify` roda a suíte full sem skips; a classificação não muda nada → removido só na entrega de remoção. +- **`TEQO_REPO_URL` → Forgejo local com mirror** — dependência extra de sync; o repo GitHub é público, clone direto sem auth. +- **Fork-PR no CI** — repo público: fork PR rodaria a suíte full em minutes hosted de graça; `if:` same-repo no job `checks` (mesma guarda de RCE dos outros workflows). + +## Cutover em duas fases (gate 2026-08-19) + +O GitHub é **só** o host do Actions; o Forgejo permanece o **tracker principal** (Issues, labels, claims, flips — tudo via API, intacto). A remoção dos arquivos do Forgejo só acontece após validação ao vivo do fluxo GitHub: + +1. **Fase 1 (esta entrega — OPS71):** cria todo o sistema GitHub Actions (workflows, scripts, libs, docs). **Nada do Forgejo é apagado** — `.forgejo/workflows/`, `forgejo-pr-automerge.mjs`, `forgejo-dispatch.mjs`, `ci-classify-production.mjs` + specs permanecem no repo (dormentes: o repo Forgejo não recebe mais push). +2. **Cutover manual (humano, nesta ordem — (a)–(c) ANTES do push do OPS71, senão o PR mergearia sem required check):** (a) remotes locais (`origin` → GitHub, `forgejo` secundário); (b) PAT `GITHUB_TOKEN` local + secrets no GitHub (`FORGEJO_API_TOKEN`, `CURSOR_API_KEY`) + `allow_auto_merge: true`; (c) `pnpm configure:branch-protection` (GitHub); (d) **desligar o runner do Forgejo** (reversível — é o que para o schedule do `ci.yml` antigo, que não tem mais sentido e disputaria a workstation; pode ser logo após o primeiro PR GitHub validado); (e) runner self-hosted no homeserver (deploy; não bloqueia PRs — pode ficar para depois); (f) push do OPS71 → PR no GitHub → CI verde → auto-merge → flip da #97 (valida os flips no merge do próprio OPS71) → deploy manual dispatch quando o runner do homeserver estiver pronto. +3. **Fase 2 (entrega sucessora, registrada no fechamento desta Issue):** remove `.forgejo/workflows/*`, scripts mortos e docs residuais do fluxo antigo. Só depois do fluxo GitHub comprovado de ponta a ponta. + +### Componentes / mudanças + +- **`.github/workflows/ci-pr.yml`** (novo): port do `.forgejo/workflows/ci-pr.yml` — job único `checks`, mesmos steps/guards/services (`postgres-int`/`postgres-build` por nome na rede do job), skips via `ci-scope.mjs` (env `GITHUB_BASE_REF` idêntico). Deltas GitHub: `concurrency: { group: ci-pr-${{ github.head_ref }}, cancel-in-progress: true }` (fail-fast nativo em push novo — o OPS62 não cancelava só porque o Forgejo não tem API; aqui é grátis), `if:` same-repo no job (fork não queima minutes), `permissions: contents: read` (default), e2e selected preservado (OPS72 ajusta a política depois — `depends`). +- **`.github/workflows/deploy.yml`** (novo): `workflow_dispatch` (sem inputs — ref default main). `verify` (ubuntu-latest, timeout 50): `check-test-locations` → lint → format → typecheck → knip → cycles → unit → int (migrate+seed nos services) → build → e2e **full** (4 workers; guardas de contrato de PR ficam de fora — não são verificação de produção). `deploy` (`needs: [verify]`, `runs-on: [self-hosted, homeserver]`, timeout 60, `permissions: contents: read`): checkout → `bash scripts/deploy-homeserver.sh "$GITHUB_SHA"`. +- **`scripts/deploy-homeserver.sh`**: `TEQO_REPO_URL` default → `https://github.com/fsolla/teqo.git`; no workspace existente, `git remote set-url origin "$TEQO_REPO_URL"` idempotente antes do fetch (o clone velho aponta para o Forgejo local); doc de invocação: execução local no runner (não mais pipe via ssh) — o ` --title --body-file ` (Ready, base `main` — sem opção draft, regra do repo). Substitui o `forgejo_create_pull_request` do MCP no fluxo de entrega. +- **`scripts/lib/github-branch-protection.mjs`** (novo, puro): `DESIRED_RULE` GitHub (`required_status_checks.checks = [{ context: 'CI (PR) / checks' }]`, strict=false, 0 reviews, `enforce_admins: true`, sem restrictions) + `ruleMatches`/`planBranchProtectionRule` (mesmo contrato do atual). +- **`scripts/configure-branch-protection.mjs`**: reescrito para a REST do GitHub (`PUT /repos/{owner}/{repo}/branches/main/protection`), idempotente (read → plan → apply → verify), `--dry-run` preservado. +- **`scripts/forgejo-issue-transition.mjs`**: aceita `PR_BODY` env (body do PR vem do evento GitHub); sem PR_BODY mantém a leitura via API do Forgejo (fallback — os workflows do Forgejo ainda estão vivos até a Fase 2); comentário do flip atualizado ("deploy manual via dispatch, ver AGENT-OPS"). +- **`.github/workflows/issue-done-on-main-merge.yml` / `plan-issue-ready-on-main-merge.yml`** (novos): `pull_request: closed` + `if:` merged && base main && **same-repo** (guarda RCE: checkout de fork + PAT = RCE — preservada); `PR_BODY: ${{ github.event.pull_request.body }}`; `FORGEJO_API_TOKEN` secret; sem `GITHUB_TOKEN` de Issues do GitHub (flip é no Forgejo). +- **`.github/workflows/archive-cursor-agent.yml`** (novo): port direto do `.forgejo/workflows/` (CURSOR_API_KEY). +- **`docs/AGENT-OPS.md`, `.agents/rules/agent-pr-workflow.mdc`, `.agents/skills/work-issue/` (SKILL + execution-pipeline), `.agents/skills/agent-work-issue/SKILL.md`, `.agents/skills/capture-review-debts/SKILL.md` (linha do fechamento), `AGENTS.md`, `docs/ops/teqo-1313-deploy.md`**: fluxo de PR/CI/merge/deploy reescrito para GitHub (canônico); seção de transição nomeando a Fase 2; tabela de secrets (GitHub agora: `FORGEJO_API_TOKEN`, `CURSOR_API_KEY`, local `GITHUB_TOKEN` PAT); passos manuais do cutover na ordem (incl. desligar o runner do Forgejo e por quê — o schedule do `ci.yml` antigo pararia de ter sentido). +- **Preservados nesta entrega (Fase 2 remove):** `.forgejo/workflows/*` (6 arquivos), `scripts/forgejo-pr-automerge.mjs`, `scripts/forgejo-dispatch.mjs`, `scripts/ci-classify-production.mjs`, `scripts/lib/dockerignore.mjs`, `scripts/lib/branch-protection.mjs` + specs (`ciClassifyProduction`, `dockerignoreMatch`, `branchProtection`) — dormentes, documentados como rollback e alvo da entrega de remoção. +- **Testes novos:** `tests/unit/githubApi.unit.spec.ts` (normalização de shapes + endpoints REST/GraphQL, molde do `forgejoApi.unit.spec.ts`), `tests/unit/githubBranchProtection.unit.spec.ts` (regra desejada + drift, molde do `branchProtection.unit.spec.ts`), e pure-functions da política de draft/skip do automerger (`scripts/lib/github-pr-flow.mjs` + spec — o pin OPS57/OPS64 do jeito GitHub). +- **Migration:** sem migration (nenhum schema). +- **Access / Consent:** nenhum. +- **UI:** Impeccable N/A. + +## Fases verificáveis + +1. **Libs + scripts base** — `github-api.mjs`, `github-pr-flow.mjs` (puro), `github-branch-protection.mjs` (puro), `github-pr.mjs`, `github-pr-automerge.mjs`, `forgejo-issue-transition.mjs` (PR_BODY), `configure-branch-protection.mjs` (GitHub) + specs unit. Gate: `pnpm test:unit`. +2. **Workflows** — `.github/workflows/*` (6). Nada do `.forgejo/workflows/` é tocado nesta fase (Fase 2 do cutover). Gate: `pnpm gate:fast` + `pnpm check:cycles` + knip. +3. **Docs/skills + changelog** — AGENTS.md, AGENT-OPS, agent-pr-workflow.mdc, skills, runbook (com seção de transição), `docs/changelog/2026-08-19-ops71.md` + `pnpm changelog:build`. +4. **Gates finais + entrega** — `pnpm gate:ci` completo (unit+int+build), format, knip, cycles; `pnpm push -u origin HEAD` → PR no GitHub via `node scripts/github-pr.mjs` → CI verde no GitHub → auto-merge → flip da #97. **Passos manuais do cutover listados para o humano** (Fase 1 do cutover: remotes, secrets, branch protection, runner homeserver, shutdown runner Forgejo) — o PR em si não depende deles (CI roda no GitHub com secrets de teste), mas a ordem recomendada é: remotes → secrets → configure → push → PR. + +## Fase 2 — entrega sucessora (pós-validação) + +Registrada no fechamento desta Issue (capture-review-debts): remove `.forgejo/workflows/*`, `forgejo-pr-automerge.mjs`, `forgejo-dispatch.mjs`, `ci-classify-production.mjs`, `dockerignore.mjs` (+ specs órfãs) e qualquer doc residual do fluxo Forgejo. Critério de entrada: um ciclo completo validado no GitHub (PR → CI → auto-merge → flip → deploy manual). + +## Rabbit holes / Não escopo (engenharia) + +- Portar `waitForChecks`/statuses para o GitHub — não há; auto-merge nativo. +- Re-engenharia do `deploy-homeserver.sh` (fluxo docker/compose/migrate) — só a origem do clone e o modo de invocação mudam. +- Sincronizar o repo Forgejo com main — congelado (decidido, anti-goal do tracker preservado). +- Migrar Issues/labels/claims — anti-goal explícito. +- OPS72 (e2e local afetado) — item sucessor com `depends: [OPS71]`. +- Pool/`agent-pool*` — dormente (OPS65); o texto `gh pr` em `agent-pool-prompt.mjs` é de feature morta — deixo como está (fora do serializes). +- Secrets de GitHub (`FORGEJO_API_TOKEN`, `CURSOR_API_KEY`, `GITHUB_TOKEN` PAT local) — passo manual documentado, não código. + +## Riscos e mitigação + +- **Nome do check-run `CI (PR) / checks`:** o workflow `name: CI (PR)` + job `checks` geram exatamente esse check-run no GitHub; o required check usa o mesmo literal — pin no spec da branch protection e verificação ao vivo no primeiro PR (se o nome divergir, o configure aponta o drift e o PR não mergea — fail-closed, nunca merge sem verificação). +- **Auto-merge GraphQL com `GITHUB_TOKEN`:** permissões declaradas (`contents`+`pull-requests: write`) cobrem PATCH draft e a mutation; se o GraphQL recusar (ex.: auto-merge desabilitado no repo), o job fica vermelho com a mensagem — mitigação: passo manual `allow_auto_merge: true` no repo (Settings ou API) listado no cutover. +- **PR com conflito:** auto-merge fica armado e o PR espera — igual ao hoje (waitForChecks lançava "não mergeável"); nenhum merge errado é possível (servidor). +- **Repo público → fork PR:** `if:` same-repo no job `checks` + setting de fork workflows no repo (passo manual) — fork não queima minutes nem acessa secrets. +- **Runner do homeserver ainda não instalado:** o `deploy` fica indisponível até o passo manual; PRs seguem mergeando (CI hosted não depende do runner). O PR deste item não publica nada sozinho — o deploy é dispatch manual. +- **Schedule do `ci.yml` antigo rodando na workstation durante a transição:** com o repo Forgejo congelado, o gate do Forgejo lerá um deployed SHA inexistente no clone → fail-open vermelho → suíte full a cada 30 min na workstation. Mitigado: o cutover desliga o runner do Forgejo logo após o merge (passo (d), reversível) — antes do primeiro deploy via GitHub. +- **`gate:ci` local vs `origin/main`:** o remote muda para GitHub; sem o passo manual dos remotes, o gate local compara contra o Forgejo defasado → risco de diff falso. Mitigado: o passo de remotes é o PRIMEIRO do cutover e o runbook avisa; o `*-impl.md` documenta o comando exato. +- **TEQO_REPO_URL no homeserver:** clone velho com origin Forgejo → `set-url` idempotente no script cobre; repo público → sem credencial. +- **Knip com os scripts do Forgejo preservados:** nada órfão é criado nesta entrega (todos os scripts seguem referenciados pelos YAMLs do `.forgejo/workflows/`, que permanecem) — sem risco de knip flagrar a deleção adiada. + +## Aceite de engenharia + +- [x] Aceite de produto da intenção ainda coberto (CI/PR GitHub, tracker Forgejo, deploy manual verify→self-hosted, rollup required check) +- [x] Cutover em duas fases (gate 2026-08-19): sistema GitHub criado e validado primeiro; arquivos do Forgejo preservados nesta entrega, remoção em entrega sucessora pós-validação +- [x] Invariantes AGENTS/engineering-standards (sem migration, sem schema; plain Node stdlib nos helpers de CI; zero-dep nos workflows) +- [x] Testes de domínio previstos: github-api (normalização/endpoints), github-branch-protection (drift), github-pr-flow (draft policy/skips — pin OPS57/OPS64 no jeito GitHub) diff --git a/docs/plans/ops71-fase2-remover-arquivos-forgejo-impl.md b/docs/plans/ops71-fase2-remover-arquivos-forgejo-impl.md new file mode 100644 index 00000000..b10ca02a --- /dev/null +++ b/docs/plans/ops71-fase2-remover-arquivos-forgejo-impl.md @@ -0,0 +1,83 @@ +# Impl: OPS71 Fase 2 — remover arquivos do Forgejo (workflows/scripts) após validação do GitHub Actions + +Status: aprovado +Atualizado em: 2026-08-19 +Issue: #111 +Intenção: docs/plans/ops71-ci-github-actions-tracker-forgejo.md (§"Fase 2 — entrega sucessora", linha 99-101 do impl da Fase 1) +Appetite restante: pequeno — remoção + fixups de consumidores + docs (1 sessão) + +## Leitura da intenção + +- **Outcome:** o fluxo GitHub Actions (OPS71 Fase 1, validado ao vivo: PRs #742/#743 — CI verde, auto-merge nativo, flips via API) é a única via de CI/PR/deploy no repo; os arquivos do fluxo Forgejo saem do tree; docs residuais atualizados; nada do tracker é tocado. +- **O que NÃO negociar:** tracker no Forgejo (Issues/labels/claims/flips via API) — `forgejo-api.mjs`, `agent-*.mjs`, `issue.mjs`, `forgejo-issue-transition.mjs`, `agent-promote-related-on-merge.mjs` **ficam**; `.forgejo/worktree.env` fica (fora do escopo `workflows/*`, consumido pelo provisionamento de worktree); desligar o runner do Forgejo é passo manual do runbook (fora do código); `ci.yml` de main não volta (deploy = dispatch manual). +- **O que reavaliar:** a hipótese da intenção dizia "grep `forgejo-pr-automerge|forgejo-dispatch|ci-classify|dockerignore` como saída" — a varredura achou **dois consumidores funcionais** além de docs, não listados no escopo literal: `scripts/lib/test-affected-core.mjs` (HIGH_RISK_EXACT com os 2 paths mortos) e `tests/unit/ciSkipInvariants.unit.spec.ts` (lê `.forgejo/workflows/ci.yml`+`ci-pr.yml` e pina o gate OPS65). Ambos **precisam ser editados** — são o contrato que quebraria se os arquivos sumissem. Também achado: `README.md` ainda descreve o fluxo Forgejo pré-Fase 1 (janela de 30 min, deploy automático, linha de CI apontando `.forgejo/workflows/`). + +## Abordagem recomendada + +```mermaid +flowchart LR + D[14 arquivos deletados] --> C1[test-affected-core: -2 entries HIGH_RISK] + D --> C2[ciSkipInvariants: -2 paths .next-e2e, -2 high-risk, teste OPS65 reescrito para deploy.yml dispatch-only] + D --> C3[gate-ci: comentário .github/workflows/ci-pr.yml] + D --> C4[github-pr-automerge + agent-pr-ready-automerge: comentário sem referência morta] + D --> DOC[AGENTS.md + runbook + README — era GitHub só] + DOC --> CL[changelog ops71-fase2 + changelog:build] +``` + +**Opções consideradas:** + +- **A — Remoção seca + fixups mínimos dos consumidores** (o escopo do body + os 2 consumidores funcionais + 3 docs). Nada de novo é criado; invariantes de CI re-ancorados no `.github/workflows/`. +- **B — Reescrever `ciSkipInvariants` para pinar "`.forgejo/` não existe"** — redundante: a deleção é o próprio diff do PR; e o teste que importa é o invariante _vivo_ (deploy nunca automático), que re-pino no `deploy.yml`. +- **C — Deletar o teste OPS65 inteiro** (remover o teste da janela de 30 min e pronto) — perderia o pin "nunca deploy automático" sem reposição; o GitHub manteve o espírito (dispatch manual), o teste deve continuar existindo apontando para o arquivo certo. + +**Recomendação:** A — porque os consumidores são pequenos e bem delimitados, a substituição do pin OPS65 pelo pin equivalente da era GitHub (`deploy.yml` sem trigger `push`/`schedule`; `ci-pr.yml` sem o gate morto) preserva a invariante de deploy manual, e nada além do escopo declarado é tocado. + +**Rejeitadas:** + +- **Manter os arquivos como rollback inerte** — o propósito da Issue é a remoção; rollback passa a ser git history (o main congelado do Forgejo é uma cópia íntegra da era Forgejo; o PR é um commit reversível). +- **Deletar `forgejo-issue-transition.mjs`/`agent-promote-related-on-merge.mjs`/`forgejo-api.mjs`/`agent-*.mjs`** — anti-goal explícito (tracker vivo). +- **Deletar `.forgejo/worktree.env`** — fora do escopo (`workflows/*`); consumido pelo provisionamento de worktree. +- **Editar `docs/plans/*`, `docs/changelog/*`, `docs/CHANGELOG-AGENTS.md`** — história congelada/insert-only; a narrativa de "preservados até Fase 2" vive nos changelogs antigos e permanece. +- **Tocar `docs/AGENT-OPS.md`** — já é era-GitHub (grep não acha referência a nenhum arquivo removido). + +### Componentes / mudanças + +- **Deletados (14):** `.forgejo/workflows/` (6: ci-pr.yml, ci.yml, agent-pr-ready-automerge.yml, issue-done-on-main-merge.yml, plan-issue-ready-on-main-merge.yml, archive-cursor-agent.yml); `scripts/forgejo-pr-automerge.mjs`, `scripts/forgejo-dispatch.mjs`, `scripts/ci-classify-production.mjs`, `scripts/lib/dockerignore.mjs`, `scripts/lib/branch-protection.mjs`; specs `tests/unit/ciClassifyProduction.unit.spec.ts`, `tests/unit/dockerignoreMatch.unit.spec.ts`, `tests/unit/branchProtection.unit.spec.ts`. Nenhuma importação viva aponta para eles (grep: só `test-affected-core` + `ciSkipInvariants` + docs). +- **`scripts/lib/test-affected-core.mjs`**: remove `scripts/ci-classify-production.mjs` e `scripts/lib/dockerignore.mjs` de `HIGH_RISK_EXACT` (paths mortos nunca aparecem em diff; listas devem espelhar o tree). +- **`tests/unit/ciSkipInvariants.unit.spec.ts`**: (1) tira `ci.yml`+`ci-pr.yml` do loop `.next-e2e`; (2) tira os 2 paths do pin de high-risk; (3) reescreve o teste "main CI 30-min window (OPS65)" para a era GitHub: lê `.github/workflows/deploy.yml` e pina `workflow_dispatch:` presente, `push:`/`schedule:` ausentes (deploy nunca automático) e `.github/workflows/ci-pr.yml` sem `ci-classify-production.mjs` (gate morto). +- **`scripts/gate-ci.mjs`** (comentário do header): "Local mirror of `.forgejo/workflows/ci-pr.yml`" → `.github/workflows/ci-pr.yml`. +- **`scripts/github-pr-automerge.mjs` + `.github/workflows/agent-pr-ready-automerge.yml`**: comentários com o parêntese "(forgejo-pr-automerge.mjs)" → referência genérica à era Forgejo (arquivo não existe mais). +- **`AGENTS.md`**: frase "The Forgejo-era files (...) are preserved as rollback until the OPS71 Fase 2 removal" → removidos na Fase 2; rollback via git history (main congelado do Forgejo / revert do PR). +- **`docs/ops/teqo-1313-deploy.md`** (§"Rollback do cutover"): "os workflows de `.forgejo/workflows/` seguem no repo até a Fase 2" → arquivos removidos; restauração via git history do main congelado do Forgejo. +- **`README.md`**: bloco de fluxo stale (janela 30 min, deploy automático, linha `CI: .forgejo/workflows/ci-pr.yml + ci.yml`) → fluxo era-GitHub (PR → `CI (PR) / checks` → auto-merge nativo → deploy manual dispatch). +- **Changelog:** `docs/changelog/2026-08-19-ops71-fase2.md` + `pnpm changelog:build`. +- **Migration:** sem migration (nenhum schema). +- **Access / Consent / UI:** N/A. + +## Fases verificáveis + +1. **Deleção + fixups de código** — `git rm` dos 14 arquivos; edita `test-affected-core.mjs`, `ciSkipInvariants.unit.spec.ts`, comentários de `gate-ci.mjs`/`github-pr-automerge.mjs`/`agent-pr-ready-automerge.yml`. Gate: `pnpm test:unit` (ciSkipInvariants/testAffected/github\*). +2. **Docs + changelog** — AGENTS.md, runbook, README, `docs/changelog/2026-08-19-ops71-fase2.md` + `pnpm changelog:build`. +3. **Gates finais** — `pnpm exec tsc --noEmit`, `pnpm lint`, `pnpm format:check`, `pnpm exec knip`, `pnpm check:cycles`, `pnpm test` (unit+int), `pnpm build`; grep de saneamento (`forgejo-pr-automerge|forgejo-dispatch|ci-classify|dockerignore.mjs|branch-protection.mjs` → só histórico/docs/changelogs); push → PR GitHub → CI (PR) / checks → auto-merge. + +## Rabbit holes / Não escopo (engenharia) + +- Re-engenharia do `ciSkipInvariants` além do re-pino — não; só o que o arquivo lê muda. +- `pnpm configure:branch-protection`/github libs — intocados (era GitHub). +- Pool/`agent-pool*` dormente (OPS65) — intocado. +- Desligar o runner do Forgejo — passo manual no runbook, não código. +- `.dockerignore` (o arquivo) — **permanece**: o `docker build` do `deploy-homeserver.sh` o consome; só o lib `dockerignore.mjs` (parse de regras, gate OPS65) morre. + +## Riscos e mitigação + +- **`ciSkipInvariants` quebrar por leitura de arquivo removido** — os 3 pontos são editados na mesma fase da deleção; gate local `pnpm test:unit` antes de qualquer push. +- **Pin perdido do "deploy manual"** — mitigado pelo teste reescrito (dispatch-only, sem `push:`/`schedule:`); se um dia o `ci.yml` voltar com trigger, o teste acende. +- **Referência morta residual fora do grep** — saneamento final com o grep do escopo + knip (dead exports/deps é CI-blocking); docs históricos (plans/changelogs antigos) são allowlist consciente. +- **Knip acusar `branch-protection.mjs`/`dockerignore.mjs` como órfãos na Fase 1→Fase 2** — a deleção é atômica com a remoção das referências (mesma fase); nada fica órfão no diff. +- **PR de plano vs `check-plans-only-pr-closes`** — este PR tem código (não é plans-only); guard não se aplica. + +## Aceite de engenharia + +- [x] Aceite de produto da intenção ainda coberto (remoção completa + docs; tracker intacto) +- [x] Invariantes AGENTS/engineering-standards (sem migration/schema; nada do tracker; docs históricos intocados; changelog insert-only) +- [x] Testes de domínio previstos: ciSkipInvariants re-ancorado na era GitHub (deploy dispatch-only); unit suíte verde diff --git a/docs/plans/ops71-flip-pos-merge-falhou-silencioso-impl.md b/docs/plans/ops71-flip-pos-merge-falhou-silencioso-impl.md new file mode 100644 index 00000000..b4a628fc --- /dev/null +++ b/docs/plans/ops71-flip-pos-merge-falhou-silencioso-impl.md @@ -0,0 +1,97 @@ +# Impl: OPS71-FLIP — Flip pós-merge falhou/silencioso no fluxo GitHub — validar e endurecer issue-done-on-main-merge + +Status: aprovado +Atualizado em: 2026-08-20 +Issue: #119 +Intenção: body da Issue (sem plano linkado — o body é a spec) +Appetite restante: herdado (chore P2, sem appetite declarado — escopo = as 4 ações sugeridas + fix de raiz) + +## Leitura da intenção + +- **Outcome:** todo PR mergeado em `main` com `Closes #N` deve flipar a Issue no Forgejo para `done`+`in-prod` + comentário + **close** — de forma **não silenciosa** (falha visível) e com **caminho de recuperação manual** quando o evento do GitHub não criar run. +- **O que NÃO negociar:** tracker continua no Forgejo (flips via `FORGEJO_API_TOKEN`); fork guard (RCE) preservado; flips nunca mentem (OPS61: job vermelho se qualquer flip falhar); CI green é o único gate de merge. +- **O que reavaliar:** a hipótese da Issue ("tipos de evento / entrega do GitHub") estava certa só pela metade — a causa real é a **anti-recursão do GITHUB_TOKEN** (merge atribuído a `github-actions[bot]` não cria runs); a hipótese "410 é transitório, só pin/retry" é insuficiente — o 410 quebrou o run do #742 **depois** do evento ter criado o run; os dois problemas são independentes. + +## Investigação (root cause — evidência ao vivo, GitHub API pública) + +Comparação dos dois merges (timeline `issues/{n}/timeline` + `actions/runs`): + +| | PR #742 (Fase 1) | PR #746 (Fase 2) | +| ------------------------ | ----------------------------------------------------------------- | ----------------------------------------------------------------------- | +| `auto_rebase_enabled` | `github-actions[bot]` 21:20:38Z | `github-actions[bot]` 00:02:32Z | +| `merged` / `closed` | **`fsolla`** 22:17:20Z | **`github-actions[bot]`** 00:20:11Z | +| Runs criados no `closed` | 32308114529 (falhou 410) + 32308114530 (skipped), 3s após o merge | **0 runs** (nenhum evento, nenhum head — janela 00:00–00:45 verificada) | + +**Causa raiz:** o auto-merge nativo armado via `enablePullRequestAutoMerge` (GraphQL) com o **GITHUB_TOKEN** do workflow executa o merge como `github-actions[bot]`. O GitHub **documenta** ("Automatic token authentication", anti-recursão): eventos acionados por ações feitas com o `GITHUB_TOKEN` **não criam workflow runs** (exceção: `workflow_dispatch`, `repository_dispatch`). O `closed` fica no timeline do PR, mas o Actions não gera run nenhum — nem `pull_request`, nem `push` (0 runs na janela do merge). Toda auto-merge por bot vai pular os dois flips pós-merge **em silêncio** (run nem é criado — nunca vai aparecer vermelho). + +O 410 é falha **separada**: o run do #742 foi criado normalmente (merge por humano) e o `GET /issues/97/labels` respondeu 410 Gone (nginx) — transitório, endpoint ok às 00:3x. O retry do OPS67 cobre fetch-rejection + 5xx-no-GET; 410 (4xx) nunca retenta; o corpo nginx HTML entra no erro só como slice cru de 400 chars (imprestável para log). + +## Abordagem recomendada + +```mermaid +flowchart LR + M["PR mergea em main"] -->|"auto-merge armado com PAT real (AUTOMERGE_PAT)"| E["closed event → runs criados
(merge atribuído ao dono do PAT)"] + M -. "legado: bot merge (GITHUB_TOKEN)" .-> X["anti-recursão: 0 runs
(causa raiz #746)"] + E --> F["issue-done / plan-issue-ready flipam no Forgejo"] + F --> C["labels done+in-prod + comentário + close
(forgejo-issue-transition.mjs)"] + H["humano: dispatch manual (recovery)"] --> G["gh pr view → PR_BODY → mesmo script"] + C -->|"410/5xx transitório"| R["retry 410 no forgejo-api
(proxy respondeu — origem intocada)"] +``` + +**Opções consideradas (fix de raiz):** A | B | C +**Recomendação:** **A + recuperação manual + close + retry 410** — a trinca atende os 4 pontos da Issue e conserta a causa que a torna _silenciosa_. +**Rejeitadas:** + +- **B — manter arming com GITHUB_TOKEN e só adicionar dispatch manual:** o flip continua quebrado em todo merge de bot (o "silencioso" do título); o humano vira o flip de todo PR. Rejeitada porque a Issue é exatamente sobre o silêncio. +- **C — poller (schedule) que varre PRs mergeados:** disparo a cada N minutos com API lendo PRs fechados = latência, races, e um "verificador" novo que o OPS71 eliminou de propósito; PAT + evento nativo é mais simples e imediato. + +**Opções consideradas (recuperação manual):** A | B +**Recomendação:** **A — `workflow_dispatch` com input `pr`** (o script já aceita `PR_BODY`; o step de dispatch busca body+merged via `gh` e repassa). B (rodar o script local com `PR_BODY`) é o fallback final do runbook, sem YAML novo. +**Rejeitadas:** C — GitHub App só para isso (peso de registrar app; o repo já usa PATs). + +**Opções consideradas (410):** A | B +**Recomendação:** **A — 410 entra no retry do `forgejo-api.mjs` para QUALQUER método** (não só GET): 410 vem do proxy (nginx/Cloudflare) — a origem **nunca viu** o request, então retentar escrita não duplica efeito colateral (diferente de 5xx, onde a origem pode ter aplicado o write — esse continua fail-closed). Corpo não-JSON vira marker compacto no erro. +**Rejeitadas:** B — 410 só no GET (o flip faz `removeLabels` GET → `addLabels` POST → `addComment` POST; um 410 no POST continuaria quebrando o flip apesar da classificação "transitório conhecido"). + +### Componentes / mudanças + +- **`.github/workflows/agent-pr-ready-automerge.yml`**: passa `AUTOMERGE_PAT: ${{ secrets.AUTOMERGE_PAT }}` ao step; comentário do porquê (anti-recursão). +- **`scripts/github-pr-automerge.mjs`**: **fail-closed** — se `AUTOMERGE_PAT` ausente, `exit 1` com mensagem nomeando o secret e o motivo (nunca fallback silencioso para GITHUB_TOKEN); `enableAutoMerge` passa a usar `createApi({ token: AUTOMERGE_PAT })` (o `markPullRequestReady` continua com o token do run — PATCH draft:false não precisa gerar evento: nada consome `ready_for_review` além do próprio workflow, e o CI corre no `synchronize` do push do agente). +- **`.github/workflows/issue-done-on-main-merge.yml`** + **`plan-issue-ready-on-main-merge.yml`**: `on: workflow_dispatch` com input `pr` (required); job `if:` = evento (`closed` + merged + main + same-repo) **ou** dispatch com input; permission `pull-requests: read` (gh); step de dispatch: `gh pr view` → valida `merged == true` + `baseRefName == main` + `headRepository.nameWithOwner == github.repository` (mesmo contrato do evento — um dispatch num PR de fork/aberto é skip) → `PR_BODY` via `GITHUB_OUTPUT`; step do node usa `steps.pr.outputs.* || github.event.pull_request.*` (um caminho só, sem override vazio). O script já no-ops sem `Closes`/`Related`. +- **`scripts/forgejo-issue-transition.mjs`**: no loop do flip, após labels+comentário → **`api.closeIssue(number)`** (era-Forgejo o close era nativo do merge do PR; era-GitHub o PR não toca o tracker — sem o PATCH toda Issue mergeada fica `OPEN` com `done`+`in-prod`). Falha de close = `flipFailed` (OPS61: vermelho, nunca mentir). Comentário ganha menção ao close. +- **`scripts/lib/forgejo-api.mjs`**: `RETRYABLE_BEFORE_ORIGIN = new Set([410])` retentado em qualquer método (comentário: proxy respondeu — origem intocada, sem risco de efeito duplicado; 5xx segue GET-only); erro `!response.ok` com corpo HTML → marker compacto `corpo não-JSON (proxy/nginx — HTTP ): <120 chars>` em vez do slice cru. +- **`scripts/lib/github-api.mjs`**: **intocado** (o 410 observado é do proxy do Forgejo; github-api não recebe o mesmo tratamento por escopo — política documentada nos headers de ambos). +- **`docs/AGENT-OPS.md`**: tabela de workflows (rows dos dois flips ganham "dispatch manual de recuperação"), tabela de secrets (`AUTOMERGE_PAT`), parágrafo do flip com o porquê bot-merge não cria run + runbook de recuperação (dispatch com número do PR; fallback local `PR_BODY`). +- **`AGENTS.md`**: frase curta no parágrafo do OPS71 (auto-merge armado com `AUTOMERGE_PAT` — merge por bot não cria runs). +- **Migration:** sem migration. **Access/Consent:** n/a. **UI:** n/a. +- **Changelog:** `docs/changelog/2026-08-20-ops71-flip.md` + `pnpm changelog:build`. + +## Fases verificáveis + +1. **Lib + script do flip** — `forgejo-api.mjs` (410 + closeIssue + marker HTML), `forgejo-issue-transition.mjs` (close). Unit specs novas em `tests/unit/forgejoApi.unit.spec.ts` (410 GET retry, 410 write retry, 410 esgotado → marker HTML, closeIssue PATCH pin). Verificação: `pnpm test` (spec), `pnpm exec tsc --noEmit`, lint/format. +2. **Auto-merge com PAT** — `github-pr-automerge.mjs` fail-closed + workflow env. Verificação: tsc/lint; comportamento real no próprio PR desta entrega (CI verde → auto-merge armado com o PAT). +3. **Workflows de recuperação** — `workflow_dispatch` nos dois flips. Verificação: YAML válido (CI do PR), gate do humano. +4. **Gates + docs + changelog** — `pnpm gate:fast`; `pnpm changelog:build`; AGENT-OPS/AGENTS. +5. **Pós-merge (validação ao vivo, humano):** criar o secret `AUTOMERGE_PAT` **antes** do merge deste PR (senão o auto-merge do próprio PR falha fail-closed); conferir o flip da #119 (run criado pelo `closed` do merge — agora atribuído ao humano) e, como recovery proof, disparar o dispatch manual do `issue-done` uma vez. + +## Rabbit holes / Não escopo (engenharia) + +- Retry 410 no `github-api.mjs` (simetria): sem evidência de 410 no GitHub API; política documentada, gatilho = ocorrência real. +- GitHub App dedicado para o merge (B do fix de raiz): rejeitado no gate — PAT fine-grained basta. +- `agent-promote-related-on-merge.mjs` (`setLabels` do `agent-forgejo.mjs`): intocado — só o workflow dele ganha o dispatch. +- Poller/`schedule` de verificação de merges (C): rejeitado — o evento nativo + dispatch cobre. +- Drift do `ci-pr.yml` / branch protection: fora de escopo. + +## Riscos e mitigação + +- **Secret `AUTOMERGE_PAT` ainda não existe no merge deste PR** → o fail-closed derruba o auto-merge de todo PR até o humano criar. Mitigação: passo 5 da fase pós-merge declara criar o secret **primeiro**; sem o secret, merges manuais continuam funcionando (o bot não arma, o humano mergea — `closed` por humano gera runs, comportamento #742). +- **Fine-grained PAT sem permissão de merge** → `enablePullRequestAutoMerge` GraphQL falha → job vermelho (visível, não silencioso). Mitigação: runbook documenta escopos (`pull-requests: write` + `contents: write` no repo fsolla/teqo). +- **Dispatch manual num PR não mergeado / fork** → skip (validação `merged` + `baseRefName` + `headRepository` no step), mesma semântica do evento. +- **`gh` no runner hosted**: pré-instalado no `ubuntu-latest`; `GITHUB_TOKEN` injetado automaticamente; sem dependência nova. + +## Aceite de engenharia + +- [x] Aceite de produto da intenção ainda coberto (flip sempre acontece; falha é visível; recovery existe) +- [x] Invariantes AGENTS/engineering-standards (fork guard nos dois workflows, OPS61 exit-1, zero-dep plain Node, DRY <3 call sites) +- [x] Testes de domínio previstos (unit: retry 410 GET/write, marker HTML, closeIssue, fail-closed PAT) +- [x] Self-score decision-quality: 5/5 (decisões caras com rejeitadas; abordagem no appetite; rabbit holes nomeados; depth check ok — reusa createApi/PR_BODY; intenção preservada) diff --git a/docs/plans/ops71-runner-self-hosted-github-homeserver-impl.md b/docs/plans/ops71-runner-self-hosted-github-homeserver-impl.md new file mode 100644 index 00000000..22005327 --- /dev/null +++ b/docs/plans/ops71-runner-self-hosted-github-homeserver-impl.md @@ -0,0 +1,135 @@ +# Impl: Runner self-hosted do GitHub no homeserver (deploy do dispatch manual) + CURSOR_API_KEY + +Status: aguardando aprovação +Atualizado em: 2026-08-19 +Issue: #113 +Intenção: o body da Issue #113 (OPS71-INFRA) é a spec — sem plano de intenção separado +Appetite: horas de operação (não é Issue de código — nada de schema/UI/access) + +## Leitura da intenção + +- **Outcome:** o job `deploy` do `.github/workflows/deploy.yml` (dispatch manual) + roda no **runner self-hosted do GitHub instalado no homeserver** (labels + `self-hosted` + `homeserver`), executando `scripts/deploy-homeserver.sh ` + localmente (sem SSH). O hosted (`verify`) nunca toca o homeserver. +- **O que NÃO negociar:** runner só no `deploy.yml` (nunca no `ci-pr.yml`); + usuário com docker group + leitura de `~/stack/`; outbound-only (funciona + atrás do Cloudflare tunnel); registro do runner em infra-solla `STATE.md`. +- **Já validado na sessão (pre-flight):** + - SSH ao homeserver OK (LAN `192.168.15.142`, user `fsolla`). + - `fsolla` tem `docker` no groups + `sudo`; lê `~/stack/teqo-1313.env` e + `~/stack/.env` (modo 600, dono = fsolla). Arch `x86_64` → tarball linux-x64. + - **Runner ainda NÃO existe** no homeserver (sem `~/actions-runner`, sem unit + systemd do runner; o `runner-watch.*` é outra coisa — WoL/shutdown da + workstation). + - **PAT do OPS71 funciona** (local, `~/.bashrc`): `GET /repos/fsolla/teqo` 200 + e `POST …/actions/runners/registration-token` 201 → admin no repo OK. + - Versão estável atual do runner: **v2.336.0** linux-x64, 226 MB, + sha256 `04cf0be1aff4c3ec3554466c39124ca250e3effd8873bb7e8d68535aa9505d5d` + (digest da API de releases do GitHub). + - **Linger do homeserver já ativo** (`loginctl show-user fsolla` → + `Linger=yes`) → systemd **user** funciona sem sudo (o `runner-watch` já é + user service persistente lá — prova do padrão). + - Workspace `~/teqo-deploy` existe mas ainda aponta origin → Forgejo + (`http://localhost:3000/…`): o `set-url` idempotente do + `deploy-homeserver.sh` re-aponta para `TEQO_REPO_URL` (GitHub) no primeiro + deploy — nada a fazer, mas é o que o log do primeiro deploy deve mostrar. + +## Abordagem recomendada + +Sequência **operacional no homeserver** (nenhum commit no repo teqo — o código +do OPS71 já foi mergeado; o único artefato commitado desta Issue é o registro no +`STATE.md` do infra-solla, repo separado): + +1. **Download** (homeserver, `~/actions-runner`): tarball v2.336.0 linux-x64 da + release oficial, **sha256 conferido** contra o digest da API de releases. +2. **Registration token** (workstation, PAT local): `POST +/repos/fsolla/teqo/actions/runners/registration-token` (TTL 1 h — gerar e + configar na sequência, um único passo encadeado). +3. **config.sh** (homeserver, user fsolla): + `./config.sh --url https://github.com/fsolla/teqo --token --labels +self-hosted,homeserver --name teqo-1313-runner --work _work`. +4. **svc.sh como systemd USER** (`./svc.sh install` sem root → unit + `~/.config/systemd/user/actions.runner.fsolla-teqo-1313-runner.service`) + + `systemctl --user enable --now …` (start). Linger já ativo → persiste sem + login; roda como fsolla (docker + `~/stack` envs). +5. **Verificação do runner**: `systemctl --user status …` ativo; runner listado + em `GET /repos/fsolla/teqo/actions/runners` como online. +6. **Registro em infra-solla** (`~/Code/infra-solla/STATE.md`): entrada na + seção do Teqo + nota na tabela de topologia (runner GitHub no homeserver, + labels, dir, systemd user) — e no runbook `docs/ops/teqo-1313-deploy.md` do + teqo (a tabela "Onde roda cada coisa" já o menciona prospectivamente; só + confirmar o texto com o estado real). Changelog `docs/changelog/2026-08-20-ops71-infra.md` + `pnpm changelog:build`. +7. **Aceite (verificação ao vivo)**: dispatch manual do `deploy.yml` (ref main) + → `verify` full verde → job `deploy` roda no runner do homeserver → log com + o `set-url` do workspace (Forgejo → GitHub) + smoke ok → **segundo dispatch + do mesmo SHA** → guard "already deployed". + +**Decisões:** + +- **systemd user + linger (já ativo) vs. `svc.sh install` via root:** user — sem + senha sudo interativa, mesmo padrão do `runner-watch` (persistência provada no + próprio homeserver); o unit system-level não adicionaria nada (o unit user + roda com `User=fsolla` de qualquer forma). +- **`~/actions-runner` + workdir `_work`:** convenção oficial; `~/teqo-deploy` + permanece como workspace do deploy (o script seta `TEQO_REPO_URL` nele). +- **Sem mudanças em código/workflows:** `runs-on: [self-hosted, homeserver]` + já está no `deploy.yml`; o `deploy-homeserver.sh` já tem o `set-url` + idempotente. O runner vem do GitHub com o token de registro — sem secrets + novos no repo (o repo é público; as envs de produção ficam no homeserver). + +**Rejeitadas:** + +- **`svc.sh install` como root** — exigiria sudo interativo; nada ganho sobre o + unit user (mesmo usuário efetivo, linger já garante auto-start no boot). +- **Rodar o runner em container/docker** — complexidade extra; o padrão oficial + do GitHub é o binário com svc; o runner-watch local prova o user service. +- **Instalar agora o helper `archive-cursor-agent` / criar o secret + `CURSOR_API_KEY` no GitHub** — item secundário da Issue, dormente (pool morto, + OPS65): a key não existe no ambiente; fica **registrado como pendência** no + fechamento (pedir a key ao humano quando quiser reativar o helper), sem ação + agora. + +## Fases verificáveis + +1. Runner baixado + sha256 conferido. +2. Runner configurado (`.runner` + `.credentials` no `~/actions-runner`), + labels corretas, nome `teqo-1313-runner`. +3. Unit systemd user ativo e **persistente** (`systemctl --user is-enabled …`), + runner online na API do GitHub. +4. `STATE.md` (infra-solla) + runbook + changelog registrados. +5. Dispatch manual de validação (verify → deploy → already-deployed no segundo + dispatch) — **aceite final, exige o humano** (dispara no GitHub UI). + +## Rabbit holes / Não escopo (engenharia) + +- Não re-engenharia do `deploy-homeserver.sh` (fluxo compose/migrate intacto). +- Não mexer no stack/registry/env do homeserver. +- Não criar secret `CURSOR_API_KEY` (dormente — só com a key do usuário). +- Não editar o `ci-pr.yml` nem a branch protection. + +## Riscos e mitigação + +- **Token de registro expira em 1 h** → o `config.sh` roda na mesma sequência + do POST (script encadeado da workstation → homeserver). +- **`svc.sh install` gerar unit system em vez de user** (se detectar root): + rodar sem sudo garante o user unit; se algo criar system unit, desfazer com + `svc.sh uninstall` e repetir como user. +- **Runner offline no dispatch** → falha conhecida já documentada no runbook + ("job deploy nunca roda / runner offline"): checar `systemctl --user status`, + o runner-watch local não cuida deste runner (não é o forgejo-runner). +- **verify full ~50 min hosted antes do deploy** → aceite da Issue é um ciclo + completo; nada do homeserver é tocado até `verify` verde. +- **Rollback**: `~/actions-runner/./svc.sh uninstall` + `rm -rf ~/actions-runner` + (desregistra no GitHub); o deploy volta a ficar indisponível até o runner ser + religado — comportamento conhecido, documentado no runbook. + +## Aceite de engenharia + +- [ ] Aceite de produto da intenção coberto (deploy self-hosted no homeserver, + verify hosted nunca toca o homeserver) +- [ ] Runner instalado como fsolla (docker + `~/stack`), systemd user com + linger, outbound-only +- [ ] Registrado em infra-solla `STATE.md` + runbook + changelog +- [ ] Validação ao vivo: dispatch manual → deploy no runner → "already + deployed" no segundo dispatch diff --git a/docs/plans/ops72-e2e-local-afetado-ci-sem-full-impl.md b/docs/plans/ops72-e2e-local-afetado-ci-sem-full-impl.md new file mode 100644 index 00000000..b34dc3c3 --- /dev/null +++ b/docs/plans/ops72-e2e-local-afetado-ci-sem-full-impl.md @@ -0,0 +1,76 @@ +# Impl: E2E local apenas para os testes afetados — CI de PR sem e2e full + +Status: aprovado +Atualizado em: 2026-08-19 +Issue: #98 +Intenção: docs/plans/ops72-e2e-local-afetado-ci-sem-full.md +Appetite restante: ~0,5–1 dia eng (herdado) + +## Leitura da intenção + +- **Outcome:** (1) skill `work-issue` (e irmãs do fluxo) exige que o agente rode localmente **os e2e que criou + os da mesma superfície afetada** — **discricionário**: o agente decide quais; `pnpm test:e2e:affected` é a ferramenta, não um gate mecânico; (2) CI de PR roda o **blast-radius** (e2e que o classifier detecta como tocados — modo `selected`, nunca `full`); (3) e2e **full** roda na **main, antes do deploy** (`verify` do deploy manual — rede de segurança pré-publicação); (4) limitação da #72 documentada na skill. +- **O que NÃO negociar:** full no deploy/verify de main; gate local (`gate:push`) sem e2e (passo explícito da skill, não hook); mecânica do classifier/manifest intocada; CI selected (blast radius) mantido como rede — o local é discricionário por design, o CI não. +- **O que reavaliar:** hipótese de "`.github/workflows/ci.yml` (verificador de main)" — **não existe mais**: OPS71 eliminou o `ci.yml`; a suíte full vive só no job `verify` do `deploy.yml` (confirmado). A hipótese do ci-pr.yml já ter os dois steps e2e (`full` + `selected`) dirigidos por `ci-scope.mjs` se confirma — o trabalho é **remover o full** e reorientar o texto. + +## Abordagem recomendada + +```mermaid +flowchart LR + A[Diff do PR] --> B[ci-scope.mjs / e2e-affected.mjs] + B -->|selected| C[CI PR: E2E blast-radius manifest-selected specs] + B -->|full| D[CI PR: sem e2e - full vive no verify do deploy] + B -->|none| E[CI PR: sem e2e - sem blast radius] + F[Agente antes do push] --> G[e2e local dos specs criados + mesma superfície - discricionário] + G -->|decisão do agente| H[pnpm test:e2e:affected ou specs diretas] + I[main antes do deploy] --> J[verify: e2e full] +``` + +**Opções consideradas:** A | B | C +**Recomendação:** A — remover o step e2e `full` do `ci-pr.yml`; quando o classifier emitir `full` (diff high-risk: schema/lockfile/harness), o CI de PR simplesmente não roda e2e (o step `selected` tem `if: e2e_mode == 'selected'` e é pulado; build-for-e2e segue rodando). O espelho local (`run-e2e-affected.mjs`) roda **full localmente** nesse mesmo caso — mesma mecânica, sem gap: o único caminho para "full" é diff high-risk, e aí o agente viu o full na máquina dele e o `verify` do deploy roda full antes de publicar. Ciência: um diff high-risk sem e2e no CI é um risco deliberado (rabbit hole "full só no deploy cria gap de regressão" já aceito no gate). +**Rejeitadas:** B — manter o step `full` no CI (status quo): viola o gate "nunca full por PR" e devolve o custo 40+ min por PR de infra. C — mapear `full` → falha/aviso no CI: criaria bloqueio artificial em PRs legítimos de infra/schema (o diff high-risk é exatamente o que o local roda full); o verify do deploy já é o gate honesto. + +### Componentes / mudanças + +- **`.github/workflows/ci-pr.yml`**: remover o step `E2E tests (full suite, single job, 4 workers)`; atualizar o comentário do header ("e2e policy: selected (affected) specs on PRs — never full; full lives in deploy verify — OPS72"); o step `E2E tests (manifest-selected specs)` fica funcionalmente intacto. +- **`scripts/gate-ci.mjs`**: mensagens informativas do escopo e2e (texto-only, ~linhas 153–162) — "e2e verified in CI" vira "e2e selected no CI (PR); full só no `verify` do deploy; local: `pnpm test:e2e:affected` (passo da skill)". Comportamento intocado (e2e fora do gate — aceite). Mensagens não são pinadas por unit tests (verificado). +- **`.agents/skills/work-issue/execution-pipeline.md`** (corpo compartilhado das duas skills): seção "Executar" item 3 (Gates) ganha o passo **e2e local (discricionário)** antes do `pnpm push`; nova seção curta "E2E local afetado" documentando: o agente roda **os specs e2e que criou + os da mesma superfície afetada**, decidindo quais (discricionário); a ferramenta (`pnpm test:e2e:affected` — espelho do classifier; ou specs diretas via `--no-deps`), e a **limitação da #72** (`--no-deps` + projetos paralelos colidem no `seedTestUser` → `--workers=1` ou a cadeia padrão de projetos; só conveniência local, CI é sequencial por construção). +- **`.agents/skills/work-issue/SKILL.md`**: checklist item 4 e Passo 4 ganham o passo (humano): "rode localmente os e2e que você criou + os da mesma superfície afetada — você decide quais (`pnpm test:e2e:affected`)". +- **`.agents/skills/agent-work-issue/SKILL.md`**: delta pool — mesmo passo **quando o worktree tiver browsers Playwright**; Cursor Cloud sem browser → registra justificativa e segue (CI selected é a rede) — opção B da intenção com registro, não silêncio. +- **`AGENTS.md`**: reescrever o parágrafo "O e2e não roda mais no gate local (OPS59)" → política OPS72: e2e afetado local = passo obrigatório da skill; CI de PR = selected; full = `verify` do deploy manual. +- **`docs/AGENT-OPS.md`**: item "Agente faz sozinho" ganha o passo e2e local afetado; linha do `ci-pr.yml` na tabela CI passa a "e2e selected (nunca full — OPS72)". +- **`.agents/rules/agent-pr-workflow.mdc`**: linha 41 (`pnpm gate:push` descrição) reflete a política nova. +- **Migration:** sem migration. **Access / Consent:** n/a. **UI:** Impeccable A — sem UI. +- **Changelog (OPS44):** `docs/changelog/2026-08-19-ops72.md` + `pnpm changelog:build` + `changelog:check`. + +### Dados → forma (se aplicável) + +- N/a — não apresenta dados; apenas política/CI/skills. + +## Fases verificáveis + +1. **CI** — `.github/workflows/ci-pr.yml`: step full removido + comentários; `scripts/gate-ci.mjs`: mensagens (texto-only). Verificação: `pnpm exec prettier --check` nos arquivos; diff limpo. +2. **Skills** — `execution-pipeline.md` (passo + seção #72), `work-issue/SKILL.md`, `agent-work-issue/SKILL.md`. +3. **Docs** — `AGENTS.md`, `AGENT-OPS.md`, `agent-pr-workflow.mdc`. +4. **Changelog** — entrada OPS72 + build + check. +5. **Gates** — `pnpm gate:fast`; `pnpm format:check`; `pnpm test:unit --changed` (nenhum pin quebra — mensagens de console não são pinadas); e2e local afetado: o diff toca `scripts/gate-ci.mjs` (HIGH_RISK) → modo `full` → suíte full rodada (154 passed em dev; 8 flakes de ambiente dev triados — passam em `E2E_PROD=1`, o modo do CI); CI do PR não roda e2e (full por design desta política); `pnpm push`; PR Ready `Closes #98`. + +## Rabbit holes / Não escopo (engenharia) + +- Não tocar `ci-scope.mjs`, `e2e-affected.mjs`, `e2e-affected-manifest.mjs`, `test-affected-core.mjs`, `run-e2e-affected.mjs` — mecânica funciona; só muda onde/em que modo roda (fora de escopo da intenção). +- Não implementar o fix da #72 (fica na fila; só documentação). +- Não adicionar e2e ao `gate:push`/`gate:ci` (aceite explícito: passo da skill, não hook). +- Não mexer no `deploy.yml` (já roda full no `verify`). +- Não criar script novo — `pnpm test:e2e:affected` já existe. + +## Riscos e mitigação + +- **Diff high-risk no CI sem e2e** (ex.: PR de schema): o PR não roda e2e no CI. Mitigação: espelho local roda full no mesmo diff; `verify` do deploy roda full antes de publicar; documentado no workflow e no gate-ci. +- **Agente pula o e2e local**: discricionário por design (decisão do usuário) — o agente decide quais specs rodar e pode decidir não rodar. Mitigação: CI selected (blast radius) pega a superfície quando o diff é selecionável; full no `verify` do deploy fecha o ciclo antes de publicar. +- **Mensagem do gate-ci desatualizada vs CI**: texto ajustado na mesma entrega; nada pinado. +- **Serialização**: a intenção serializa `ci-pr.yml` e `.agents/skills/work-issue/`; OPS71 já mergeou — sem concorrência pendente (OPS72 é o primeiro a tocar no ci-pr.yml novo pós-OPS71). + +## Aceite de engenharia + +- [x] Aceite de produto da intenção ainda coberto (e2e local discricionário documentado na skill; CI/PR = blast-radius selected, nunca full; full na main antes do deploy; #72 documentada; gate local intocado) +- [x] Invariantes AGENTS/engineering-standards (edita o dono — execution-pipeline é o corpo compartilhado; nada duplicado) +- [x] Testes de domínio previstos: nenhum pin quebra (mensagens de console não pinadas); `test:e2e:affected` com modo `none` valida o fluxo novo diff --git a/docs/plans/ops73-deploy-admin-prod-evidence.png b/docs/plans/ops73-deploy-admin-prod-evidence.png new file mode 100644 index 00000000..116eb10c Binary files /dev/null and b/docs/plans/ops73-deploy-admin-prod-evidence.png differ diff --git a/docs/plans/ops73-deploy-admin-prod-fixed-evidence.png b/docs/plans/ops73-deploy-admin-prod-fixed-evidence.png new file mode 100644 index 00000000..c23d92c0 Binary files /dev/null and b/docs/plans/ops73-deploy-admin-prod-fixed-evidence.png differ diff --git a/docs/plans/ops73-deploy-admin-prod-impl.md b/docs/plans/ops73-deploy-admin-prod-impl.md new file mode 100644 index 00000000..083d249e --- /dev/null +++ b/docs/plans/ops73-deploy-admin-prod-impl.md @@ -0,0 +1,77 @@ +# Impl: Admin do Payload segue em branco em produção — re-home S8/S9 no GitHub + fix importMap + deploy + verificação + +Status: entregue (deploy `dba75ff8` em prod + verificação pós-deploy completa) +Atualizado em: 2026-08-20 +Issue: #114 +Intenção: docs/plans/ops73-deploy-admin-prod.md +Appetite restante: herdado (~0,5 dia); ajuste: +1 PR de re-home (premissa da intenção se provou falsa) + +## Leitura da intenção + +- **Outcome:** `/admin` de produção renderiza o formulário de login (tela preta some); editor loga e abre uma coleção; log do container sem `getFromImportMap: PayloadComponent not found in importMap` para `@payloadcms/storage-s3/client#S3ClientUploadHandler`. +- **O que NÃO negociar:** nada de schema/migration/Consent/UI nova nesta entrega; não reabrir o diagnóstico OPS69 (#77); não introduzir features novas; o deploy é do que já está aprovado. +- **O que reavaliar (premissa FALSA):** a intenção afirma que "o fix OPS69 está em main desde 7162c65e e produção nunca o recebeu — basta deployar o main". **Verificado ao vivo (2026-08-19 23:56–2026-08-20 00:40), a premissa está errada em dois pontos:** + 1. **Produção JÁ recebeu um deploy às 23:09 UTC** (revision `8c734ba`, "docs(S8)", build 23:09:47, container up 23:09:48) — mas essa revision **não tem o handler no importMap**: o commit S9 (`8d2352c0`, captura de novidades na home) regenerou o importMap **sem** as envs `S3_*` e removeu as 2 linhas do fix OPS69. O erro `getFromImportMap` persiste no log desde o start do container e `/admin` continua em branco (0 inputs/botões/links; shell HTML ok, título "Painel de Controle - Payload"). A revision ANTERIOR (`fbfbfb1`) **tinha** o handler (2 matches) — confirma o relato "funcionou e no deploy seguinte parou de funcionar de novo". + 2. **O main do GitHub diverge do main do Forgejo (pós-cutover OPS71):** GitHub main = `3ea16243` (OPS69 fix com handler ✓, OPS71 infra ✓, plans S11/OPS73 ✓) mas **NÃO contém as features S8 (seção "Nossa história") e S9 (captura de novidades/newsletter)** que produção roda desde o deploy das 23:09 (árvore de 8c734ba tem `CampaignNewsletterForm.tsx`, `CampaignStorySection.tsx`, migration `20260819_192605_add_campaign_newsletter_capture`, e o GitHub main não — 404 via API; `git diff 8c734ba 3ea16243` mostra os arquivos como `D`). O Forgejo main congelou em `e339ceec` (S8/S9/S10); o workspace do homeserver ainda aponta `origin = http://localhost:3000/fsolla/teqo.git` (o Forgejo local) — por isso sua visão de "main" era o e339ceec. **Deployar o main do GitHub como está REGREDIRIA as features S8/S9 em produção** (some a seção "Nossa história" e o form de newsletter). + +## Abordagem recomendada + +```mermaid +flowchart LR + A[cherry-pick S9 8d2352c0 + S8 488dcf7d sobre main 3ea16243] --> B[resolver conflito importMap: manter handler] + B --> C[regen importMap com S3_* dummy: diff vazio = prova de consistencia] + C --> D[gates locais: tsc/lint/format/knip/cycles/unit/int/build] + D --> E[PR OPS73 em main com auto-merge] + E --> F[confirmar no main: handler + S8/S9 presentes] + F --> G[deploy via ssh homeserver: deploy-homeserver.sh sha] + G --> H[verificar: revision label + log + /admin renderiza + home S8/S9 + editor loga] +``` + +**Opções consideradas:** A | B | C +**Recomendação:** **A — PR único OPS73** que re-homa no GitHub main as features S8+S9 já aprovadas e mergeadas no Forgejo (cherry-pick dos commits de feature `8d2352c0` + `488dcf7d`), resolve o conflito do importMap **mantendo o handler** e prova consistência rodando `generate:importmap` com envs `S3_*` dummy (diff deve ser vazio) — porque: (1) o aceite "admin no ar" exige o handler no build; (2) o aceite "não regredir prod" exige S8/S9 no main antes do deploy; (3) o main do GitHub é a fonte da verdade do deploy (guard de HEAD + `TEQO_REPO_URL`); (4) cherry-pick do código já aprovado ≠ feature nova (anti-goal preservado: nada de design/schema novo). +**Rejeitadas:** + +- **B — PRs separados (re-home S8, re-home S9, depois fix/deploy):** ownership nominalmente mais limpo, mas 3 PRs em fila, admin fora do ar por mais tempo e o deploy continua dependendo do último; sem ganho de risco. +- **C — deploy do main atual aceitando a regressão S8/S9:** regressão pública (seção "Nossa história" + form de novidades somem) — viola o aceite "o time editorial e de campanha volta a trabalhar" e reverteria features `in-prod` no tracker. + +### Componentes / mudanças + +- **`src/app/(payload)/admin/importMap.js`** (gerado, commitado): estado final = com `@payloadcms/storage-s3/client#S3ClientUploadHandler` (idêntico ao de 3ea16243 — o conflito do cherry-pick do S9 resolve mantendo o handler; a regen com envs `S3_*` dummy fecha a prova de consistência com diff vazio). +- **S9 (cherry-pick `8d2352c0`):** `CampaignNewsletterSection.tsx`, `submitCampaignNewsletter.ts`, `CampaignNewsletterForm.tsx`, `campaignConsentKeys.ts`, `schemas/campaignNewsletter.ts`, mudanças em `Contact.ts`/`Subscription.ts`/`CampaignHero.tsx`/`CitySelect.tsx`/`StateSelect.tsx`/`page.tsx`/`styles.css` + migration `20260819_192605_add_campaign_newsletter_capture` + docs/changelog. **ImportMap do commit NÃO entra (remover as -2 linhas é o que quebrou; manter o handler).** +- **S8 (cherry-pick `488dcf7d`):** `CampaignStorySection.tsx`, `page.tsx` (+2), e2e `frontend.e2e.spec.ts`/`fixtures/e2eTest.ts`, `.gitignore`, plans/changelog. +- **Migration:** o cherry-pick traz a do S9; o deploy aplica **zero migrations novas** vs prod (8c734ba já a tem aplicada — `20260819_192605` rodou no deploy das 23:09). Sem migration nova neste PR. +- **Access / Consent:** não se aplica (S8/S9 já aprovados; `campaignConsentKeys.ts` é parte do S9 já mergeado no Forgejo). +- **UI:** Impeccable A — nenhuma UI nova; features S8/S9 já aprovadas (copy aprovado nos respectivos PRs). + +## Fases verificáveis + +1. **Cherry-picks + conflito** — `git cherry-pick 8d2352c0` (resolver importMap: manter o handler) e `488dcf7d` sobre `origin/main` (3ea16243); conferir que o diff final vs 3ea16243 contém S8/S9 completos (sem truncamentos) e que o importMap fica com handler. (Quota: ~30 min) +2. **Prova de consistência do importMap** — `S3_BUCKET=teqo-media S3_ENDPOINT=http://localhost:3900 S3_ACCESS_KEY_ID=test S3_SECRET_ACCESS_KEY=test pnpm generate:importmap` → `git diff` do importMap deve ficar **vazio** (estado já consistente). Se aparecer drift não compreendido: parar e investigar (guard do OPS69). +3. **Gates** — `pnpm exec tsc --noEmit`, `pnpm lint`, `pnpm format:check`, `pnpm exec knip`, `pnpm check:cycles`, `pnpm test` (unit+int), `pnpm build` (DB local). E2e: no CI do PR (frontend S8/S9 specs + admin). +4. **PR → merge** — push da branch, PR para main, auto-merge (required check `CI (PR) / checks`); conferir no main pós-merge: `importMap.js` com handler, `CampaignNewsletterForm.tsx`/`CampaignStorySection.tsx` presentes, migration do S9 presente. +5. **Deploy (caminho B — runner #113 ainda não instalado)** — `ssh homeserver 'bash ~/teqo-deploy/scripts/deploy-homeserver.sh '` (o script re-pointa o origin do workspace para o GitHub, guard de HEAD, flock, build migrator → migrate (no-op) → build runner → up → healthcheck → smoke). Executado pelo agente via SSH (acesso validado: `ssh homeserver` ok, docker ok, envs em `~/stack/teqo-1313.env`), com o humano acompanhando; alternativa: humano roda o mesmo comando. ~15–20 min. +6. **Verificação pós-deploy** — (a) `docker inspect` revision label == sha deployado; (b) `docker logs teqo-1313 --since ` sem `getFromImportMap` para o handler (aceite #3 da intenção); (c) `/admin` de prod renderiza o formulário de login no browser (Playwright + screenshot → `docs/plans/ops73-deploy-admin-prod-fixed-evidence.png`); (d) home pública mantém seção "Nossa história" (S8) e form de novidades (S9); (e) **editor loga e abre uma coleção — passo manual do humano** (credenciais de prod não ficam com o agente). +7. **Fechamento** — changelog `docs/changelog/2026-08-20-ops73.md` + `pnpm changelog:build`; comentários na Issue #114 com evidência; reforçar no AGENTS.md a lição do S9 (regen sem envs re-órfão o handler) e a necessidade do guard #87; registrar débito da divergência de plataformas (workspace homeserver aponta para Forgejo local; S10 re-home pendente — S10 NÃO está em prod nem no GitHub main, e sua Issue está in-prod no tracker). + +## Rabbit holes / Não escopo (engenharia) + +- **Arqueologia do rewrite do main (S10 force-push/rebase sobre o Forgejo):** a divergência das plataformas está documentada acima; reconstituir quem-mergeou-onde não muda o plano. Corte: verificação por conteúdo (API + árvore), não por linhagem. +- **Re-home do S10 (pixel):** S10 não está em prod (8c734ba só tem o `facebookPixel.ts` antigo do petition pages, `be1106f8`) nem no GitHub main — é PR próprio, fora deste escopo; registrar como débito. +- **Instalar o runner (#113)/deploy automático:** fora de escopo (a intenção já corta; caminho B é o que desbloqueia hoje). +- **Guard de CI do importMap (#87):** segue pendente; este incidente (regen do S9) é a segunda ocorrência da mesma classe — reforça a prioridade, não muda o escopo. + +## Riscos e mitigação + +- **Main do GitHub anda durante o trabalho (agentes paralelos S10/S11):** re-verificar `git ls-remote origin main` antes do cherry-pick, antes do push e antes do deploy (o guard do script re-checa). Se o main mudar, rebase e re-valida. +- **Conflito no cherry-pick além do importMap (page.tsx, Contact.ts, etc.):** base do S8/S9 = 693d2e48 (comum aos dois mains); o delta GitHub (OPS69/OPS71/plans) não toca os arquivos do S9 exceto importMap — conflitos esperados só no importMap; resolver mantendo o handler. +- **Deploy falhar no meio:** rollback automático do script (restore do compose + up) + runbook `docs/ops/teqo-1313-deploy.md`; zero migrations novas → janela de rollback limpa. +- **Admin continuar branco pós-deploy (fix insuficiente):** verificação por browser na fase 6 é o gate; se persistir, o erro no log apontará o próximo passo (o importMap está correto no main — cenário improvável, mas o log decide). +- **Screenshot de evidência sem ferramenta de visão:** usar Playwright (DOM: inputs/buttons > 0) + `design-vision` para descrever a imagem. + +## Aceite de engenharia + +- [ ] Aceite de produto da intenção coberto: `/admin` renderiza login em prod; log sem o erro do handler; editor loga e abre coleção (humano) +- [ ] Sem regressão: S8/S9 presentes no main do GitHub e servidos em prod (história + newsletter na home) +- [ ] Invariantes AGENTS/engineering-standards: nada de schema/Consent/UI novo; importMap é arquivo gerado e commitado; gates verdes +- [ ] Prova de consistência do importMap: regen com envs `S3_*` dummy → diff vazio +- [ ] Changelog + Issue #114 documentada com evidência (antes/depois); débitos registrados (S10 re-home, guard #87 reforçado, workspace homeserver origin) diff --git a/docs/plans/ops73-deploy-admin-prod.md b/docs/plans/ops73-deploy-admin-prod.md new file mode 100644 index 00000000..6a2d19b3 --- /dev/null +++ b/docs/plans/ops73-deploy-admin-prod.md @@ -0,0 +1,74 @@ +# Admin do Payload segue em branco em produção — deploy do fix OPS69 + verificação pós-deploy + +Status: rascunho +Atualizado em: 2026-08-19 +Issue: #114 +Priority: P1 +Model: composer-2.5 +Impeccable: A — sem mudança de UI (deploy do que já está em main) +Rascunho UI: N/A +Appetite: ~0,5 dia — deploy + verificação funcional; um outcome verificável +Responsável: — + +## Intenção + +O time editorial e de campanha está **sem CMS**: `/admin` em produção (jorgesolla1313.com.br) abre em branco (tela preta) — a página carrega, o shell hidrata, mas o formulário de login nunca renderiza. O bug raiz já foi diagnosticado e corrigido em `main` pelo **OPS69** (Issue #77, fechada): o `importMap` commitado ficou sem a entrada `@payloadcms/storage-s3/client#S3ClientUploadHandler` e o admin não monta as views quando o plugin `s3Storage` está ativo (envs `S3_*` de produção). O fix está em main desde `7162c65e` (2026-08-19). + +**Mas produção nunca recebeu o fix.** O container roda um build de 2026-08-18 21:49 (`e6bd1d26`), anterior ao merge: o deploy automático antigo morreu (#85 OPS70 — runs agendados skipped) e o caminho novo (dispatch manual do `deploy.yml` no GitHub Actions) depende do runner self-hosted no homeserver, que ainda não está instalado (#113 OPS71-INFRA). A verificação pós-deploy do OPS69 (aceite do próprio plano) nunca rodou. + +Verificado ao vivo em 2026-08-19: `/admin` de produção renderiza o shell vazio — **0 inputs, 0 botões, body sem texto** — mesmo sintoma do diagnóstico OPS69. `main` no GitHub hoje (`f5862c9b`) contém o fix. + +## Persona e fluxo + +- **Persona / contexto:** editor de conteúdo do mandato/campanha (staff que alimenta notícias e campanhas pelo Painel de Controle), bloqueado há ~1 dia. +- **Job principal:** abrir o admin, logar e voltar a editar publicações — sem depender de infra nova. +- **Fluxo desejado:** abre `jorgesolla1313.com.br/admin` → formulário de login aparece (não mais tela preta) → credenciais → dashboard com as coleções → consegue abrir uma coleção. +- **Anti-goals de produto:** não rediagnosticar o bug (OPS69 já fechou o diagnóstico com evidência); não reabrir a Issue #77; não entregar nenhuma mudança de código nova — só publicar o que já está aprovado em main. + +## Objetivo e aceite + +- `/admin` em produção renderiza o formulário de login para quem tem credencial (tela preta some). +- O container de produção roda uma revisão que contém o fix do importMap (`7162c65e` ou posterior — HEAD atual `f5862c9b`). +- O log do container de produção não re-emite `getFromImportMap: PayloadComponent not found in importMap` para o handler do storage. +- Um editor loga e abre uma coleção sem editar nada (smoke mínimo de leitura). +- Guardrail: nada de schema/migration/Consent/UI nesta entrega — é o deploy do que já está em `main`. + +## Dados (intenção) + +- **Vou apresentar dados?** Não — a verificação é funcional (render do login + log do container), nenhum número novo. + +## Direção no codebase (hipótese) + +- **Áreas prováveis:** `scripts/deploy-homeserver.sh` (o script de deploy — fonte da verdade do fluxo, roda no homeserver), `.github/workflows/deploy.yml` (dispatch manual `verify` → `deploy`), runbook `docs/ops/teqo-1313-deploy.md`, `src/app/(payload)/admin/importMap.js` (o fix já commitado — conferir a revision do container, não o arquivo). +- **Precedente a olhar:** OPS69 (`docs/plans/ops69-admin-branco-importmap-s3*.md`, #77) — aceite de verificação pós-deploy pendente; OPS71 (cutover do deploy) e #113 (runner); OPS66 (ordem migrate→build já resolvida no script). +- **Risco de acoplamento:** o deploy de `main` leva junto outras mudanças já aprovadas (ex.: S7, cards da home) — esperado e desejado (deploy do HEAD); o smoke do script cobre `/`, `/campanha/login`, `/admin`; rollback = restore do compose (runbook). + +## Dependências + +- Nenhuma dura. #113 (runner self-hosted) é o habilitador do caminho A, não pré-requisito — o caminho B desbloqueia sem ele. + +## Fora de escopo + +- Rediagnóstico do bug e guard de CI do importMap — #87 OPS70 já na fila (P2). +- Instalação do runner self-hosted do GitHub — #113 (Issue própria). +- Deploy contínuo automático — decisão OPS71 é deploy manual; #85 OPS70 (pipeline antigo) parece obsoleto pós-cutover — sugestão de fechamento à parte, não escopo daqui. + +## Rabbit holes de produto + +- **"A tela preta voltou → reabrir o diagnóstico."** Se alguém só completar o desejo: regressar ao OPS69. **Corte neste item:** conferir a revision do container antes de qualquer diagnóstico — a evidência (build de 2026-08-18 sem o fix, main com o fix desde 2026-08-19) já explica o sintoma. +- **"Já que vou mexer, instalo o runner/automatizo tudo."** **Corte:** #113 (runner) e #87 (guard) são Issues separadas; este item entrega admin no ar. + +## Questões em aberto (produto) + +- **Caminho do deploy?** **Opções:** A) dispatch manual do `deploy.yml` no GitHub Actions (verify full ~50 min → deploy no runner do homeserver — exige instalar o runner, #113, antes); B) executar `scripts/deploy-homeserver.sh ` direto no homeserver (mesmo script idempotente que o runner rodaria; smoke + rollback documentados; não espera #113); C) instalar o runner primeiro e depois A. **Recomendação:** **B agora** — admin caído = CMS indisponível; o script é o mesmo e o runbook cobre smoke e rollback; depois, o primeiro dispatch via A valida o pipeline OPS71. _(assumido — validar com produto)_ +- **Prioridade?** **Opções:** P0 (mesma dor do incidente original) | P1 (o fix já existe em main; resta entrega + verificação). **Recomendação:** **P1** — claimável já, sem precisar de re-P0 da fila. _(assumido — validar com produto)_ +- **Quem toca o homeserver?** O agente não tem acesso ao homeserver nem ao GitHub para dispatch sem ação do operador. **Opções:** o executor prepara os comandos do runbook e o humano roda (2 passos, ~5 min) | o executor dispara via GitHub API (`GITHUB_TOKEN` disponível) quando o runner estiver em pé (#113). **Recomendação:** humana para o caminho B imediato; o agente executa o restante (conferir SHA, verificar `/admin` no browser, checar log) e registra a evidência. _(assumido — validar com produto)_ + +## Referências + +- OPS69: `docs/plans/ops69-admin-branco-importmap-s3.md` + `-impl.md`, Issue #77 (fechada — fix `7162c65e` em main) +- Runbook de deploy: `docs/ops/teqo-1313-deploy.md` (smoke, rollback, falhas conhecidas) +- OPS71 cutover: `docs/plans/ops71-ci-github-actions-tracker-forgejo.md`; runner: #113; guard de importMap: #87; pipeline antigo morto: #85 +- Evidência (gate): `docs/plans/ops73-deploy-admin-prod-evidence.png` — `/admin` de produção em 2026-08-19 (shell vazio, sem login) + +![Admin em branco em produção — 2026-08-19](ops73-deploy-admin-prod-evidence.png) diff --git a/docs/plans/ops73-followup-rehome-s10-infra.md b/docs/plans/ops73-followup-rehome-s10-infra.md new file mode 100644 index 00000000..d0059a93 --- /dev/null +++ b/docs/plans/ops73-followup-rehome-s10-infra.md @@ -0,0 +1,42 @@ +# Follow-up pós-OPS73 — re-home S10 no GitHub, origin do workspace homeserver + fix da race de seedUser do e2e + +Status: rascunho +Atualizado em: 2026-08-20 +Issue: OPS73-FOLLOWUP (depends #114) +Intenção: Origem — débitos colhidos no fechamento do OPS73 (#114) +Appetite: ~0,4 dia (fase 1 dominante) + +## Contexto + +O OPS73 revelou que o cutover OPS71 **divergiu as plataformas**: o Forgejo main congelou em `e339ceec` (com S8/S9/S10) e o GitHub main recebeu OPS69/OPS71/OPS74 — as features S8/S9 foram re-homadas no OPS73 (PR #751), mas o **S10 (pixel Meta) não**. Além disso, duas pendências de infra/teste apareceram no ciclo. + +## Já resolvido (não reabrir) + +- Re-home do S8 (488dcf7d) e S9 (8d2352c0) no GitHub main → **PR #751** (merged `624126da`/rebase `dba75ff8`), admin fix + deploy verificado. +- ImportMap com handler S3 no main (regen com envs dummy — diff vazio). + +## Fases (mesma família: infra/deploy pós-cutover) + +### F1 — Re-home do S10 (pixel Meta) no GitHub main + deploy + +O S10 está `done`+`in-prod` no tracker (#94, PR #109 merged Forgejo `e339ceec`), mas o código **não está no GitHub main nem em prod** (a revision deployada `dba75ff8` não tem o pixel; `8c734ba` só tem o `facebookPixel.ts` antigo do petition, `be1106f8`). Cherry-pick do commit S10 (`e339ceec`) sobre o main do GitHub (mesma mecânica do OPS73), revisar a migration `20260819_213947_add_site_settings_facebook_pixel_id` (aditiva), rodar gates + e2e e deployar. Verificar `/campanha` com `SiteSettings.facebookPixelId` configurado → `#meta-pixel-*` presente. + +### F2 — Apontar o origin do workspace do homeserver para o GitHub + +`~/teqo-deploy` ainda tem `origin = http://localhost:3000/fsolla/teqo.git` (Forgejo local). O `deploy-homeserver.sh` já re-aponta para `TEQO_REPO_URL` (https://github.com/fsolla/teqo.git) a cada deploy (idempotente), mas o estado depois de um deploy devia ficar GitHub. Ação: `git -C ~/teqo-deploy remote set-url origin https://github.com/fsolla/teqo.git`. Sem code change. + +### F3 — Race de `seedTestUser` no e2e full do GitHub CI + +`tests/helpers/seedUser.ts` faz delete-then-create de `dev@payloadcms.com`. Com a suíte full no CI do GitHub (4 workers paralelos; `frontend`, `admin` e `campaignNewsletter` specs chamam `seedTestUser`), dois workers podem intercalar `delete`/`create` → `duplicate key value violates unique constraint "users_email_idx"` reportado como `ValidationError: email`. Mitigar: `on conflict` na criação ou serializar o seed (blocking lock), ou uma chamada com retry. Impacto: é o 2º sinal do e2e full (novo no pipeline GitHub); o deploy `verify` roda full e pode flakear por isso. + +## Rabit holes / Explicitamente fora + +- **Não** reabrir o diagnóstico do admin (OPS73 fechou). +- **Não** "re-homar" o re-home do S8/S9 de novo (já em main). +- Guard #87 (importMap sem envs S3\_) é Issue própria (já absorve a lição do S9); não duplicar aqui. + +## Aceite + +- S10 rodando em prod (pixel presente) e no GitHub main. +- `~/teqo-deploy` com origin = GitHub após a mudança. +- e2e full do GitHub CI estável no `seedTestUser` (sem flake de duplicate email). diff --git a/opencode.json b/opencode.json index 4653e50f..73e2a530 100644 --- a/opencode.json +++ b/opencode.json @@ -6,5 +6,30 @@ "url": "https://design.penpot.app/mcp/stream?userToken={file:.opencode/secrets/penpot-token}", "enabled": true } + }, + "provider": { + "cheapest-inference": { + "npm": "@ai-sdk/openai-compatible", + "name": "Cheapest Inference", + "options": { + "baseURL": "https://api.cheapestinference.com/v1" + }, + "models": { + "deepseek-v4-flash": { + "name": "DeepSeek V4 Flash", + "limit": { + "context": 1000000, + "output": 384000 + } + }, + "mimo-v2.5": { + "name": "MiMo v2.5", + "limit": { + "context": 1000000, + "output": 128000 + } + } + } + } } } diff --git a/scripts/ci-classify-production.mjs b/scripts/ci-classify-production.mjs deleted file mode 100644 index b7ab0935..00000000 --- a/scripts/ci-classify-production.mjs +++ /dev/null @@ -1,155 +0,0 @@ -#!/usr/bin/env node -/** - * ci-classify-production — OPS65 production-change gate for the main CI - * window. Classifies whether the merge head differs from the deployed - * artifact in anything that would enter the docker build context. - * - * The `.dockerignore` is the source of truth (the Dockerfile does `COPY . .`): - * a changed path is production when it is NOT excluded by the ignore - * patterns. The gate is FAIL-OPEN toward production: dispatch (explicit - * operator intent) and an unknown deployed SHA always run the full suite; - * a classification ERROR (git/.dockerignore unreadable, no input) prints - * production=true AND exits 1, so the workflow run turns red — the suite - * still runs (fail-open) but the deploy never happens silently. A green run - * only ever means "the gate could classify and there is nothing to do". - * - * Plain Node, stdlib only — the gate must stay cheap (no pnpm install). - * - * node scripts/ci-classify-production.mjs --deployed [--value] - * node scripts/ci-classify-production.mjs --files-from [--value] - * - * Output: JSON { production, reason, changed, kept, skipped } on stdout - * (`--value` prints only `true`/`false`). Exit 0 = classified; exit 1 = - * could not classify (production=true printed, run should go red). - */ -import { execFileSync } from 'node:child_process' -import { readFileSync } from 'node:fs' -import { pathToFileURL } from 'node:url' - -import { isDockerignored, parseDockerignore } from './lib/dockerignore.mjs' - -const parseArgs = (argv) => { - const flags = {} - for (let index = 2; index < argv.length; index += 1) { - const arg = argv[index] - if (arg.startsWith('--')) { - const name = arg.slice(2).replace(/-([a-z])/g, (_, char) => char.toUpperCase()) - const next = argv[index + 1] - flags[name] = typeof next === 'string' && !next.startsWith('--') ? next : true - if (typeof next === 'string' && !next.startsWith('--')) index += 1 - } - } - return flags -} - -const flags = parseArgs(process.argv) -const deployed = flags.deployed -const filesFrom = flags.filesFrom -const valueOnly = flags.value === true - -/** - * @param {string[]} changed - repo-relative changed paths - * @param {import('./lib/dockerignore.mjs').DockerignoreRule[]} patterns - - * .dockerignore rules - * @returns {{ production: boolean, kept: string[], skipped: string[] }} - */ -export function classifyProduction(changed, patterns) { - const kept = [] - const skipped = [] - for (const path of changed) { - if (path.length === 0) continue - if (isDockerignored(path, patterns)) skipped.push(path) - else kept.push(path) - } - return { production: kept.length > 0, kept, skipped } -} - -const report = (production, reason, extra = {}) => { - const result = { production, reason, ...extra } - if (valueOnly) { - console.log(production) - return - } - console.log(JSON.stringify(result)) -} - -/** - * Could not classify at all (git/.dockerignore unreadable, no input): run - * the full pipeline AND make the run red — a gate that cannot classify must - * never look like "nothing to do". - */ -const failOpenRed = (reason) => { - report(true, reason, { changed: [], kept: [], skipped: [] }) - process.exitCode = 1 -} - -// Import-safe: the module is unit-tested (classifyProduction), so the CLI -// body only runs when executed as a script. -const isCli = - typeof process.argv[1] === 'string' && import.meta.url === pathToFileURL(process.argv[1]).href -if (isCli) { - main() -} - -function main() { - if (process.env.GITHUB_EVENT_NAME === 'workflow_dispatch') { - report(true, 'workflow_dispatch (intenção explícita)') - return - } - - let changed - try { - if (filesFrom) { - changed = readFileSync(filesFrom, 'utf8').split('\n').filter(Boolean) - } else if (deployed === '') { - // The workflow always passes --deployed (possibly empty: no container - // revision on the homeserver). An empty deployed SHA is a legitimate - // "unknown deployed state" — run the full pipeline, keep the run green. - report(true, 'sem SHA deployado (estado desconhecido)', { - changed: [], - kept: [], - skipped: [], - }) - return - } else if (deployed === true) { - failOpenRed('--deployed sem valor') - return - } else if (deployed) { - changed = execFileSync('git', ['diff', '--name-only', deployed, 'HEAD', '--'], { - encoding: 'utf8', - }) - .split('\n') - .filter(Boolean) - } else { - failOpenRed('sem SHA deployado nem files-from') - return - } - } catch (error) { - failOpenRed(`erro ao obter o diff (${error instanceof Error ? error.message : error})`) - return - } - - if (changed.length === 0) { - report(false, 'diff vazio (nada a fazer)', { - changed: [], - kept: [], - skipped: [], - }) - return - } - - let rules - try { - rules = parseDockerignore(readFileSync('.dockerignore', 'utf8')) - } catch (error) { - failOpenRed(`.dockerignore ilegível (${error instanceof Error ? error.message : error})`) - return - } - - const classified = classifyProduction(changed, rules) - report( - classified.production, - classified.production ? 'mudança de produção desde o último deploy' : 'sem mudança de produção', - { changed, kept: classified.kept, skipped: classified.skipped }, - ) -} diff --git a/scripts/configure-branch-protection.mjs b/scripts/configure-branch-protection.mjs index 34c824d5..f737ab0a 100644 --- a/scripts/configure-branch-protection.mjs +++ b/scripts/configure-branch-protection.mjs @@ -1,38 +1,45 @@ /** - * Drift repair for branch protection (main-only) — Forgejo. Idempotent: reads - * the existing rules and creates / updates / no-ops (never duplicates). + * Drift repair for `main` branch protection — GitHub (OPS71). Idempotent: + * reads the existing rule and creates / updates / no-ops (never duplicates). * - * pnpm configure:branch-protection # apply - * pnpm configure:branch-protection -- --dry-run + * GITHUB_TOKEN= pnpm configure:branch-protection # apply + * GITHUB_TOKEN= pnpm configure:branch-protection -- --dry-run * - * main: required status check `CI (PR) / checks*` (the ci-pr cascade rollup — - * OPS61: Forgejo matches the contexts as globs against the real status - * contexts like `CI (PR) / checks (pull_request)`, so the old literal `checks` - * would match nothing), strict=false, 0 reviews. Feature PRs auto-merge when - * that context is green; the server rule is the final defense against merges - * before the cascade settles (the waitForChecks rollup gate is the client - * half). - * (migration-lock removed 2026-08-12 — see ci-pr.yml header.) + * Desired rule: required check-run `CI (PR) / checks` (the ci-pr cascade — + * OPS62), strict=false, 0 reviews, `enforce_admins: true` (nobody, admin + * included, merges with a red required check). Feature PRs auto-merge when + * that check is green; the server rule is the final defense against every + * merge path, manual API merges included. */ -import { parseArgs } from './lib/agent-forgejo.mjs' -import { DESIRED_RULE, planBranchProtectionRule } from './lib/branch-protection.mjs' -import { createApi } from './lib/forgejo-api.mjs' +import { createApi } from './lib/github-api.mjs' +import { DESIRED_RULE, planBranchProtectionRule } from './lib/github-branch-protection.mjs' -const { flags } = parseArgs(process.argv.slice(2), new Set()) -const dryRun = flags['dry-run'] === true +const parseArgs = (argv) => { + const flags = {} + for (let index = 2; index < argv.length; index += 1) { + const arg = argv[index] + if (arg.startsWith('--')) { + const name = arg.slice(2).replace(/-([a-z])/g, (_, char) => char.toUpperCase()) + const next = argv[index + 1] + flags[name] = typeof next === 'string' && !next.startsWith('--') ? next : true + if (typeof next === 'string' && !next.startsWith('--')) index += 1 + } + } + return flags +} + +const flags = parseArgs(process.argv) +const dryRun = flags.dryRun === true +const branch = 'main' const api = createApi({}) -const repo = process.env.FORGEJO_REPOSITORY ?? process.env.GITHUB_REPOSITORY ?? 'fsolla/teqo' -const ruleName = DESIRED_RULE.rule_name - -const existingRules = await api.listBranchProtections() -const existing = - existingRules.find((rule) => rule.rule_name === ruleName || rule.branch_name === ruleName) ?? null +const repo = process.env.GITHUB_REPOSITORY ?? 'fsolla/teqo' +const existing = await api.getBranchProtection(branch) const { action } = planBranchProtectionRule(existing) console.log( - `[configure:branch-protection] repo ${repo}${dryRun ? ' (dry-run)' : ''} — regra "${ruleName}": ${ + `[configure:branch-protection] repo ${repo}${dryRun ? ' (dry-run)' : ''} — regra "${branch}": ${ action === 'noop' ? 'já conforme — nada a fazer' : action === 'update' @@ -42,34 +49,26 @@ console.log( ) if (action === 'noop') { - console.log(`[configure:branch-protection] no-op: ${JSON.stringify(DESIRED_RULE)}`) + console.log(`[configure:branch-protection] no-op: ${JSON.stringify(existing)}`) process.exit(0) } -const payload = { ...DESIRED_RULE, branch_name: ruleName } -const target = action === 'create' ? '/branch_protections' : `/branch_protections/${ruleName}` -const method = action === 'create' ? 'POST' : 'PATCH' - if (dryRun) { - console.log(`[dry-run] ${method} /repos/${repo}${target} ${JSON.stringify(payload)}`) + console.log( + `[dry-run] PUT /repos/${repo}/branches/${branch}/protection ${JSON.stringify(DESIRED_RULE)}`, + ) process.exit(0) } -if (action === 'create') { - await api.updateBranchProtection(ruleName, payload) -} else { - await api.editBranchProtection(ruleName, payload) -} +await api.updateBranchProtection(DESIRED_RULE, branch) -const after = await api.listBranchProtections() -const applied = - after.find((rule) => rule.rule_name === ruleName || rule.branch_name === ruleName) ?? null -if (!applied) { +const after = await api.getBranchProtection(branch) +if (!after) { console.error( - `[configure:branch-protection] FALHOU: ${method} ${target} respondeu mas a regra "${ruleName}" não aparece no servidor.`, + `[configure:branch-protection] FALHOU: PUT respondeu mas a regra "${branch}" não aparece no servidor.`, ) process.exit(1) } console.log( - `[configure:branch-protection] done — regra verificada no servidor: ${JSON.stringify(applied)}`, + `[configure:branch-protection] done — regra verificada no servidor: ${JSON.stringify(after)}`, ) diff --git a/scripts/deploy-homeserver.sh b/scripts/deploy-homeserver.sh index 42ceaee9..420ccfa2 100644 --- a/scripts/deploy-homeserver.sh +++ b/scripts/deploy-homeserver.sh @@ -2,8 +2,11 @@ # # Deploy teqo-1313 to the homeserver (OPS53). # -# Runs ON the homeserver, piped by the CI deploy job: -# ssh homeserver "bash -s -- " < scripts/deploy-homeserver.sh +# OPS71: runs ON the homeserver via the GitHub self-hosted runner (deploy job +# of .github/workflows/deploy.yml): +# bash scripts/deploy-homeserver.sh +# (The Forgejo-era invocation `ssh homeserver "bash -s -- " < script` +# is gone — no SSH hop, no workstation involvement.) # # Flow: HEAD guard (only the current main HEAD deploys) -> flock # serialization -> workspace fetch at -> docker login (local registry) @@ -17,14 +20,16 @@ # the running site untouched and the job red. # # Environment defaults assume the homeserver layout: stack under -# $HOME/stack, repo cloned from the local Forgejo. Secrets are sourced from -# the chmod-600 env files and never echoed (no `set -x`, passwords only via -# --password-stdin / build secrets). +# $HOME/stack, repo cloned from github.com/fsolla/teqo (public — no +# credentials; the pre-OPS71 clone pointed at the local Forgejo and is +# re-pointed idempotently below). Secrets are sourced from the chmod-600 env +# files and never echoed (no `set -x`, passwords only via --password-stdin / +# build secrets). set -euo pipefail SHA="${1:?usage: deploy-homeserver.sh }" -TEQO_REPO_URL="${TEQO_REPO_URL:-http://localhost:3000/fsolla/teqo.git}" +TEQO_REPO_URL="${TEQO_REPO_URL:-https://github.com/fsolla/teqo.git}" STACK_DIR="${STACK_DIR:-$HOME/stack}" WORKSPACE_DIR="${WORKSPACE_DIR:-$HOME/teqo-deploy}" DEPLOY_LOCK="${DEPLOY_LOCK:-/tmp/teqo-1313-deploy.lock}" @@ -88,6 +93,14 @@ if [ ! -d "$WORKSPACE_DIR/.git" ]; then say "cloning $TEQO_REPO_URL into $WORKSPACE_DIR" git clone "$TEQO_REPO_URL" "$WORKSPACE_DIR" else + # OPS71: the pre-cutover clone pointed at the local Forgejo — re-point + # idempotently so the HEAD guard and the fetch compare against the repo + # that actually received the merge. + current_url="$(git -C "$WORKSPACE_DIR" remote get-url origin 2>/dev/null || true)" + if [ -n "$current_url" ] && [ "$current_url" != "$TEQO_REPO_URL" ]; then + say "updating workspace origin: $current_url → $TEQO_REPO_URL" + git -C "$WORKSPACE_DIR" remote set-url origin "$TEQO_REPO_URL" + fi git -C "$WORKSPACE_DIR" fetch origin main fi git -C "$WORKSPACE_DIR" checkout --detach "$SHA" || fatal "checkout of $SHA failed" diff --git a/scripts/forgejo-dispatch.mjs b/scripts/forgejo-dispatch.mjs deleted file mode 100644 index 6972dc38..00000000 --- a/scripts/forgejo-dispatch.mjs +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Plain-Node CLI (no pnpm) — dispatch a Forgejo workflow via workflow_dispatch. - * Used by CI jobs (requeue) and humans. - * - * node scripts/forgejo-dispatch.mjs [key=value …] - */ - -import { createApi } from './lib/forgejo-api.mjs' - -const [, , workflowFile, ref = 'main', ...inputPairs] = process.argv -if (!workflowFile) { - console.error('Usage: node scripts/forgejo-dispatch.mjs [ref] [key=value …]') - process.exit(1) -} - -const inputs = {} -for (const pair of inputPairs) { - const eq = pair.indexOf('=') - if (eq <= 0) continue - inputs[pair.slice(0, eq)] = pair.slice(eq + 1) -} - -const api = createApi({}) -try { - await api.workflowDispatch(workflowFile, { ref, inputs }) - console.log( - `[forgejo-dispatch] ${workflowFile}@${ref} enviado${Object.keys(inputs).length > 0 ? ` (${Object.keys(inputs).join(', ')})` : ''}`, - ) -} catch (error) { - console.error(`[forgejo-dispatch] falhou: ${error instanceof Error ? error.message : error}`) - process.exit(1) -} diff --git a/scripts/forgejo-issue-transition.mjs b/scripts/forgejo-issue-transition.mjs index 0dbee9c5..28822b36 100644 --- a/scripts/forgejo-issue-transition.mjs +++ b/scripts/forgejo-issue-transition.mjs @@ -2,8 +2,12 @@ * Plain-Node CLI (no pnpm) — on merge to main, flip Issues cited as * `Closes #N` / `Fixes #N` in the PR body to `done` + `in-prod` and comment. * Replacement for the `gh issue edit/comment` loop of issue-done-on-main-merge. - * The PR body comes from the API (Forgejo's PR webhook payload is not relied - * on), so the workflow only needs the PR number. + * + * OPS71: the PR now lives on GitHub. The workflow passes the PR body via the + * `PR_BODY` env (from the GitHub event payload) and the script flips Issues + * on the Forgejo tracker by API — no Forgejo PR read when PR_BODY is present. + * Without PR_BODY the script falls back to reading the PR from the Forgejo + * API (the Forgejo-era workflows stay alive until the OPS71 Fase 2 removal). * * OPS61: a failed flip exits 1 — the flip is the whole purpose of this * workflow, and the 403 era proved that a swallowed error ends the job @@ -11,6 +15,7 @@ * (the loop does not abort), but the job goes red so the breakage is visible. * * node scripts/forgejo-issue-transition.mjs --pr + * PR_BODY='Closes #97' node scripts/forgejo-issue-transition.mjs --pr */ import { createApi } from './lib/forgejo-api.mjs' @@ -37,7 +42,11 @@ if (!Number.isInteger(prNumber) || prNumber <= 0) { } const api = createApi({}) -const pr = await api.getPullRequest(prNumber) +const bodyFromEnv = process.env.PR_BODY +const pr = + typeof bodyFromEnv === 'string' + ? { merged: true, body: bodyFromEnv } + : await api.getPullRequest(prNumber) if (!pr) { console.error(`[forgejo-issue-transition] PR #${prNumber} não encontrada`) process.exit(1) @@ -62,9 +71,12 @@ for (const number of numbers) { await api.setLabels(number, { add: ['done', 'in-prod'], remove: ['in-progress'] }) await api.addComment( number, - `Merged em main via PR #${prNumber} — \`done\` + \`in-prod\`. Deploy de produção segue o verificador \`ci.yml\` (gated).`, + `Merged em main via PR #${prNumber} — \`done\` + \`in-prod\` + Issue fechada. Deploy de produção é manual (workflow_dispatch no GitHub Actions) — ver docs/AGENT-OPS.md.`, ) - console.log(`[forgejo-issue-transition] #${number}: done + in-prod`) + // OPS71-FLIP: o PR vive no GitHub e não fecha a Issue no Forgejo como o + // merge nativo fazia na era Forgejo — o close é explícito aqui. + await api.closeIssue(number) + console.log(`[forgejo-issue-transition] #${number}: done + in-prod + fechada`) } catch (error) { flipFailed = true console.error( diff --git a/scripts/forgejo-pr-automerge.mjs b/scripts/forgejo-pr-automerge.mjs deleted file mode 100644 index a142769b..00000000 --- a/scripts/forgejo-pr-automerge.mjs +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Plain-Node CLI (no pnpm) — PR safety net: wait for checks and merge by - * rebase (the Forgejo equivalent of `gh pr merge --auto --rebase`, which the - * Forgejo API does not offer as a scheduled operation). Idempotent: already - * merged → no-op success. The verdict comes from re-reading the PR (Forgejo's - * merge endpoint answers 200 + empty body on success, so the POST response - * alone cannot prove the merge) — "já mergeado" is only printed for a PR the - * server actually reports as merged; a failed merge exits 1 loudly. - * - * Draft policy (OPS57): only `cursor/*` heads get marked ready. A draft from - * any other branch is the actor's veto — the CLI skips it (exit 0) so the - * workflow never forces a human/plans PR to ready. - * - * node scripts/forgejo-pr-automerge.mjs --pr - */ - -import { createApi } from './lib/forgejo-api.mjs' - -const parseArgs = (argv) => { - const flags = {} - for (let index = 2; index < argv.length; index += 1) { - const arg = argv[index] - if (arg.startsWith('--')) { - const name = arg.slice(2) - const next = argv[index + 1] - flags[name] = typeof next === 'string' && !next.startsWith('--') ? next : true - if (typeof next === 'string' && !next.startsWith('--')) index += 1 - } - } - return flags -} - -const flags = parseArgs(process.argv) -const prNumber = Number(flags.pr) -if (!Number.isInteger(prNumber) || prNumber <= 0) { - console.error('Usage: node scripts/forgejo-pr-automerge.mjs --pr ') - process.exit(1) -} - -const api = createApi({}) -try { - const pr = await api.getPullRequest(prNumber) - if (!pr || pr.state !== 'OPEN') { - console.log( - `[forgejo-pr-automerge] PR #${prNumber} não está aberto (${pr?.state ?? 'inexistente'}) — skip`, - ) - process.exit(0) - } - if (pr.base.ref !== 'main') { - console.log(`[forgejo-pr-automerge] PR #${prNumber} base é ${pr.base.ref} (não main) — skip`) - process.exit(0) - } - if (pr.isDraft) { - if (!pr.head.ref.startsWith('cursor/')) { - console.log( - `[forgejo-pr-automerge] PR #${prNumber} é draft fora de cursor/* — skip (veto do ator)`, - ) - process.exit(0) - } - console.log(`[forgejo-pr-automerge] PR #${prNumber} é draft — marcando Ready`) - await api.markPullRequestReady(prNumber) - } else { - console.log(`[forgejo-pr-automerge] PR #${prNumber} já Ready`) - } - const { - attempted, - merged, - pr: after, - } = await api.autoMerge(prNumber, { - // OPS61: the wait now covers the whole CI (PR) cascade (the `checks` - // rollup only posts after every ci-pr job), so the 30 min default was too - // tight under load — e2e shards + int + build run before the rollup. - timeoutMs: 45 * 60 * 1000, - log: (line) => console.log(`[forgejo-pr-automerge] ${line}`), - }) - if (merged) { - console.log( - attempted - ? `[forgejo-pr-automerge] PR #${prNumber} mergeado (rebase)` - : `[forgejo-pr-automerge] PR #${prNumber} já mergeado`, - ) - } else { - console.log( - after?.state === 'CLOSED' - ? `[forgejo-pr-automerge] PR #${prNumber} fechado sem merge — skip` - : `[forgejo-pr-automerge] PR #${prNumber} voltou a draft — skip (veto do ator)`, - ) - } -} catch (error) { - console.error(`[forgejo-pr-automerge] falhou: ${error instanceof Error ? error.message : error}`) - process.exit(1) -} diff --git a/scripts/gate-ci.mjs b/scripts/gate-ci.mjs index 8c0268b3..cf709ccd 100644 --- a/scripts/gate-ci.mjs +++ b/scripts/gate-ci.mjs @@ -1,9 +1,11 @@ #!/usr/bin/env node /** - * Local mirror of `.forgejo/workflows/ci-pr.yml` (serial): phase-1 cheap checks, + * Local mirror of `.github/workflows/ci-pr.yml` (serial): phase-1 cheap checks, * then phase-2 expensive (int/build). e2e is NOT part of the local mirror - * (OPS59): it runs once, in CI (PR job `e2e` → `checks`; main `ci.yml` e2e → - * `checks` → `deploy`). Local optional feedback: `pnpm test:e2e:affected`. + * (OPS59 + OPS72): the PR CI runs only the e2e blast radius (mode `selected`, + * never `full`), the deploy `verify` job runs the full suite before + * publishing, and the local affected run is a skill step (discretionary): + * `pnpm test:e2e:affected`. * Docs guards (OPS63): the `docs-guards` checks (changelog append-only, * aggregate sync, conflict markers) also run here — cheap git diffs, same * scripts the CI job runs (the `changelog-rewrite:` escape stays CI-only, @@ -77,7 +79,7 @@ const main = async () => { const scope = readJsonScript('scripts/ci-scope.mjs') console.log( - `[gate:ci] scope base=${scope.base} code=${scope.code.mode} build=${scope.build.mode} test=${scope.test.mode} e2e=${scope.e2e.mode} (e2e runs in CI, not in this gate — OPS59)`, + `[gate:ci] scope base=${scope.base} code=${scope.code.mode} build=${scope.build.mode} test=${scope.test.mode} e2e=${scope.e2e.mode} (e2e not in this gate — OPS72; local affected is a skill step)`, ) // --- Phase 1 (cheap) --- @@ -152,14 +154,18 @@ const main = async () => { if (scope.e2e.mode === 'none') { console.log('\n[gate:ci] ⊘ e2e: CI will skip (no e2e blast radius)') + } else if (scope.e2e.mode === 'selected') { + console.log( + `\n[gate:ci] ▶ e2e: CI runs the blast radius (${scope.e2e.specs.join(', ')}) — not in this gate (OPS72); full lives in deploy verify`, + ) } else { - const specs = - scope.e2e.mode === 'full' ? 'full suite' : `selected: ${scope.e2e.specs.join(', ')}` - console.log(`\n[gate:ci] ▶ e2e: runs in CI (job e2e, ${specs}) — not in this gate (OPS59)`) + console.log( + '\n[gate:ci] ▶ e2e: diff is high-risk (full mode) — no e2e in this PR by design (OPS72); run `pnpm test:e2e:affected` locally; deploy verify runs full before publishing', + ) } console.log( - '\n[gate:ci] ✓ all checks passed (ci-pr mirror: docs guards local — OPS63; e2e verified in CI)', + '\n[gate:ci] ✓ all checks passed (ci-pr mirror: docs guards local — OPS63; e2e: local affected is a skill step — OPS72)', ) } diff --git a/scripts/github-pr-automerge.mjs b/scripts/github-pr-automerge.mjs new file mode 100644 index 00000000..a7828184 --- /dev/null +++ b/scripts/github-pr-automerge.mjs @@ -0,0 +1,86 @@ +/** + * Plain-Node CLI (no pnpm) — GitHub PR safety net: mark `cursor/*` drafts + * ready and arm GitHub's native auto-merge (rebase). Runs in + * .github/workflows/agent-pr-ready-automerge.yml. The auto-merge arming uses + * the AUTOMERGE_PAT (real user PAT) — never the built-in GITHUB_TOKEN, whose + * actor (github-actions[bot]) would merge without creating workflow runs for + * the `closed` event (OPS71-FLIP; anti-recursion — silent broken flips). + * Mark-ready keeps the run token (nothing consumes its event). + * + * Unlike the Forgejo-era automerger (removed in OPS71 Fase 2), there is NO + * poll loop: `enablePullRequestAutoMerge` is server-side — GitHub only merges + * when every required check (incl. `CI (PR) / checks`) is green and the PR is + * mergeable. The OPS64 "never trust the rollup" pin is structural here: a + * single sequential job posts one honest check-run, and the server enforces + * it. Draft policy (OPS57) preserved: only `cursor/*` heads are marked + * ready — any other draft is the actor's veto (skip, exit 0). + * + * Idempotent: already merged / closed / non-main base → no-op success. + * A GraphQL error (e.g. repo auto-merge disabled, PR in conflict) exits 1 so + * the job goes red and the situation is visible. + * + * node scripts/github-pr-automerge.mjs --pr + */ + +import { createApi } from './lib/github-api.mjs' +import { automergeArmingToken, decideAutomergeAction } from './lib/github-pr-flow.mjs' + +const parseArgs = (argv) => { + const flags = {} + for (let index = 2; index < argv.length; index += 1) { + const arg = argv[index] + if (arg.startsWith('--')) { + const name = arg.slice(2) + const next = argv[index + 1] + flags[name] = typeof next === 'string' && !next.startsWith('--') ? next : true + if (typeof next === 'string' && !next.startsWith('--')) index += 1 + } + } + return flags +} + +const flags = parseArgs(process.argv) +const prNumber = Number(flags.pr) +if (!Number.isInteger(prNumber) || prNumber <= 0) { + console.error('Usage: node scripts/github-pr-automerge.mjs --pr ') + process.exit(1) +} + +const api = createApi({}) +// OPS71-FLIP: the native auto-merge MUST be armed with a real user PAT, not +// the built-in GITHUB_TOKEN. A merge performed through the GITHUB_TOKEN actor +// (github-actions[bot]) never creates workflow runs for its events (GitHub +// anti-recursion — the `pull_request` `closed` is recorded in the timeline +// but no run is created), so the post-merge flips would silently never run +// (live finding: PR #746). With a PAT the merge is attributed to its owner (a +// real user) and the `closed` event creates runs again (PR #742 behavior). +// Fail-closed: no AUTOMERGE_PAT → exit 1 — never fall back to GITHUB_TOKEN. +const arming = automergeArmingToken(process.env) +if (!arming.ok) { + console.error(`[github-pr-automerge] ${arming.reason}`) + process.exit(1) +} +const automergeApi = createApi({ token: arming.token }) +try { + const pr = await api.getPullRequest(prNumber) + const verdict = decideAutomergeAction(pr) + + if (verdict.action === 'skip') { + console.log(`[github-pr-automerge] PR #${prNumber}: skip (${verdict.reason})`) + process.exit(0) + } + + if (verdict.action === 'mark-ready') { + console.log(`[github-pr-automerge] PR #${prNumber} é draft cursor/* — marcando Ready`) + await api.markPullRequestReady(prNumber) + } + + console.log(`[github-pr-automerge] PR #${prNumber}: armando auto-merge (rebase)`) + await automergeApi.enableAutoMerge(pr.nodeId) + console.log( + `[github-pr-automerge] PR #${prNumber}: auto-merge armado — o GitHub mergea quando o required check "CI (PR) / checks" fica verde`, + ) +} catch (error) { + console.error(`[github-pr-automerge] falhou: ${error instanceof Error ? error.message : error}`) + process.exit(1) +} diff --git a/scripts/github-pr.mjs b/scripts/github-pr.mjs new file mode 100644 index 00000000..fef6d39f --- /dev/null +++ b/scripts/github-pr.mjs @@ -0,0 +1,64 @@ +/** + * Plain-Node CLI (no pnpm) — create a GitHub PR (Ready, never draft) for + * agent deliveries. Replaces the Forgejo MCP `create_pull_request` in the + * delivery flow (OPS71: PRs live on GitHub; the tracker stays on Forgejo). + * + * GITHUB_TOKEN= node scripts/github-pr.mjs \ + * --head --title "" --body-file + * GITHUB_TOKEN= node scripts/github-pr.mjs --head --title --body "Closes #N" + * + * The repo rule "PRs are Ready, never draft" is structural: there is no + * `--draft` flag. Base defaults to `main`. + */ + +import { createApi } from './lib/github-api.mjs' + +const parseArgs = (argv) => { + const flags = {} + for (let index = 2; index < argv.length; index += 1) { + const arg = argv[index] + if (arg.startsWith('--')) { + const name = arg.slice(2) + const next = argv[index + 1] + flags[name] = typeof next === 'string' && !next.startsWith('--') ? next : true + if (typeof next === 'string' && !next.startsWith('--')) index += 1 + } + } + return flags +} + +const flags = parseArgs(process.argv) +const head = flags.head +const title = flags.title +const base = flags.base ?? 'main' + +if (!head || !title) { + console.error( + 'Usage: node scripts/github-pr.mjs --head --title [--base main] --body "" | --body-file ', + ) + process.exit(1) +} + +let body = flags.body +if (flags['body-file']) { + try { + const { readFileSync } = await import('node:fs') + body = readFileSync(flags['body-file'], 'utf8') + } catch (error) { + console.error(`[github-pr] não consegui ler --body-file: ${error.message}`) + process.exit(1) + } +} +if (typeof body !== 'string' || body.length === 0) { + console.error('[github-pr] body vazio — passe --body ou --body-file') + process.exit(1) +} + +const api = createApi({}) +try { + const pr = await api.createPullRequest({ head, base, title, body }) + console.log(`[github-pr] PR #${pr.number} criado (Ready): ${pr.htmlUrl}`) +} catch (error) { + console.error(`[github-pr] falhou: ${error instanceof Error ? error.message : error}`) + process.exit(1) +} diff --git a/scripts/lib/branch-protection.mjs b/scripts/lib/branch-protection.mjs deleted file mode 100644 index 3cd313c1..00000000 --- a/scripts/lib/branch-protection.mjs +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Pure decision for the main branch protection rule (OPS61). - * - * The desired rule is the repo contract: main requires the CI (PR) cascade - * rollup as the only status check (`strict=false`, 0 reviews). The context is - * a glob on purpose — Forgejo matches `status_check_contexts` with gobwas/glob - * against the real commit-status contexts (`CI (PR) / checks (pull_request)` — - * the ` (event)` suffix is part of the context), so the literal `checks` would - * match nothing and block every merge. - */ - -export const DESIRED_RULE = Object.freeze({ - rule_name: 'main', - enable_status_check: true, - status_check_contexts: ['CI (PR) / checks*'], - enable_push: false, - required_approvals: 0, - dismiss_stale_approvals: false, -}) - -const COMPARED_FIELDS = [ - 'rule_name', - 'enable_status_check', - 'status_check_contexts', - 'enable_push', - 'required_approvals', - 'dismiss_stale_approvals', -] - -const sameStrings = (have, want) => - have.length === want.length && - want.every((value) => have.includes(value)) && - have.every((value) => want.includes(value)) - -/** - * True when the existing rule (from the API, already normalized) matches the - * desired rule on every compared field — array fields as sets. - */ -export const ruleMatches = (existing, desired = DESIRED_RULE) => { - if (!existing) return false - return COMPARED_FIELDS.every((field) => { - const want = desired[field] - const have = existing[field] - if (Array.isArray(want)) return Array.isArray(have) && sameStrings(have, want) - return have === want - }) -} - -/** - * What to do with the rule: `create` (no rule), `update` (rule exists with - * drift), `noop` (already compliant). Idempotent re-runs land on `noop`. - * - * @param {object|null} existing - normalized rule from the API, or null - * @param {object} [desired] - * @returns {{ action: 'create' | 'update' | 'noop' }} - */ -export const planBranchProtectionRule = (existing, desired = DESIRED_RULE) => { - if (ruleMatches(existing, desired)) return { action: 'noop' } - return { action: existing ? 'update' : 'create' } -} diff --git a/scripts/lib/dockerignore.mjs b/scripts/lib/dockerignore.mjs deleted file mode 100644 index c21a5dd3..00000000 --- a/scripts/lib/dockerignore.mjs +++ /dev/null @@ -1,106 +0,0 @@ -/** - * dockerignore match — pure `.dockerignore` semantics (moby patternmatcher - * subset) for the CI production-change gate (OPS65). No git, no fs, no deps. - * - * The gate classifies "production change" by asking: would this path enter the - * docker build context? The `.dockerignore` is the source of truth for the - * artifact (the Dockerfile does `COPY . .`), so a path is production when it - * is NOT excluded by the ignore patterns. - * - * Implemented rules (moby semantics): - * - blank lines and `#` comments are ignored - * - `!` prefixes a negation (re-include); last match wins - * - trailing `/` is a directory pattern (the dir itself and its tree) - * - leading `/` anchors the pattern to the context root - * - a pattern with no other `/` matches the basename at ANY depth - * - any matched path also excludes everything under it (the builder prunes - * at the matched directory, so `docs` excludes `docs/x.md`) - * - `*` matches any run of non-separator chars, `?` a single one - * - `**` matches any number of directories (globstar) - */ - -/** A compiled ignore rule: regex against the cleaned repo-relative path. */ -export class DockerignoreRule { - /** - * @param {RegExp} regex - compiled matcher - * @param {boolean} negate - `!` prefix: re-include when matched - * @param {string} source - original pattern text (diagnostics) - */ - constructor(regex, negate, source) { - this.regex = regex - this.negate = negate - this.source = source - } -} - -/** - * @param {string} pattern - one `.dockerignore` line (no trailing newline) - * @returns {DockerignoreRule} - */ -export function compileDockerignoreRule(pattern) { - const source = pattern - let negate = false - if (pattern.startsWith('!')) { - negate = true - pattern = pattern.slice(1) - } - if (pattern.endsWith('/')) pattern = pattern.slice(0, -1) - const anchored = pattern.startsWith('/') - if (anchored) pattern = pattern.slice(1) - - let body = '' - for (let index = 0; index < pattern.length; index += 1) { - const char = pattern[index] - if (char === '*') { - if (pattern[index + 1] === '*') { - index += 1 - if (pattern[index + 1] === '/') { - body += '(?:.*/)?' - index += 1 - } else { - body += '.*' - } - } else { - body += '[^/]*' - } - } else if (char === '?') { - body += '[^/]' - } else if ('\\^$.|+()[]{}'.includes(char)) { - body += `\\${char}` - } else { - body += char - } - } - - const anyDepth = !anchored && !pattern.includes('/') ? '(?:.*/)?' : '' - const subtree = '(?:/.*)?' - const regexSource = body.length > 0 ? `^${anyDepth}${body}${subtree}$` : '$^' - return new DockerignoreRule(new RegExp(regexSource), negate, source) -} - -/** - * @param {string} text - full `.dockerignore` content - * @returns {DockerignoreRule[]} rules in order (last match wins) - */ -export function parseDockerignore(text) { - return text - .split('\n') - .map((line) => line.trim()) - .filter((line) => line.length > 0 && !line.startsWith('#')) - .map(compileDockerignoreRule) -} - -/** - * Last-match-wins exclusion test for one repo-relative path. - * - * @param {string} path - repo-relative, forward slashes (e.g. "src/app/x.ts") - * @param {DockerignoreRule[]} rules - from parseDockerignore - * @returns {boolean} true when the path is excluded from the build context - */ -export function isDockerignored(path, rules) { - let ignored = false - for (const rule of rules) { - if (rule.regex.test(path)) ignored = !rule.negate - } - return ignored -} diff --git a/scripts/lib/forgejo-api.mjs b/scripts/lib/forgejo-api.mjs index 7381ca49..0fd60762 100644 --- a/scripts/lib/forgejo-api.mjs +++ b/scripts/lib/forgejo-api.mjs @@ -12,8 +12,12 @@ * that rejects (network-level `fetch failed`, DNS/TCP/reset) retries on ANY * method; a 5xx (502/503/504 — Cloudflare blips) retries on GET only, so the * safety net's long `waitForChecks` poll survives, while write endpoints keep - * failing closed instead of risking duplicated side effects. 4xx never - * retries. Defaults: 3 retries, base 300 ms ×2, ±20% jitter. + * failing closed instead of risking duplicated side effects. 410 Gone + * (OPS71-FLIP): served by the reverse proxy (nginx/Cloudflare) BEFORE the + * request reaches the app — the origin never saw it, so retrying is safe on + * ANY method (unlike a 5xx, where the origin may have applied the write). + * 4xx never retries (410 is the one documented proxy-level exception). + * Defaults: 3 retries, base 300 ms ×2, ±20% jitter. * * Shapes are normalized to the GitHub-flavored contract the agent scripts and * their unit specs already use: `issue.state` ∈ OPEN|CLOSED, @@ -24,6 +28,10 @@ const DEFAULT_BASE_URL = 'https://git.solla.dev/api/v1' const DEFAULT_REPOSITORY = 'fsolla/teqo' const USER_AGENT = 'teqo-agent-scripts/1.0 (forgejo)' const RETRYABLE_STATUSES = new Set([502, 503, 504]) +// 410 Gone from the reverse proxy (OPS71-FLIP live finding: nginx answered +// `GET /repos/fsolla/teqo/issues/97/labels` with 410, transient). The origin +// never received the request — retrying is side-effect-free on ANY method. +const RETRYABLE_BEFORE_ORIGIN = new Set([410]) /** * @typedef {object} ForgejoApiOptions @@ -124,6 +132,11 @@ export const createApi = ({ await retryAfter(attempt, `HTTP ${response.status}`) continue } + if (!isLastAttempt && RETRYABLE_BEFORE_ORIGIN.has(response.status)) { + await response.body?.cancel() + await retryAfter(attempt, `HTTP ${response.status} (proxy — origem intocada)`) + continue + } let text try { text = await response.text() @@ -133,7 +146,14 @@ export const createApi = ({ continue } if (!response.ok) { - throw new Error(`Forgejo API ${method} ${path} → ${response.status}: ${text.slice(0, 400)}`) + const isProxyHtml = /^\s* + request(`/repos/${owner}/${name}/issues/${number}`, { + method: 'PATCH', + body: { state: 'closed' }, + }), + listIssueComments: async (number) => { const comments = await request(`/repos/${owner}/${name}/issues/${number}/comments`) return (Array.isArray(comments) ? comments : []).map((comment) => ({ diff --git a/scripts/lib/github-api.mjs b/scripts/lib/github-api.mjs new file mode 100644 index 00000000..ccc1fbd3 --- /dev/null +++ b/scripts/lib/github-api.mjs @@ -0,0 +1,264 @@ +/** + * GitHub REST + GraphQL client for the CI/PR scripts (`github-pr*`, branch + * protection). Zero dependencies — plain Node `fetch` — so it also runs + * inside GitHub Actions jobs without `pnpm install`. + * + * Auth: `GITHUB_TOKEN` (PAT locally; the built-in Actions token in + * workflows). Base URL: `GITHUB_API_URL`, else `https://api.github.com`. + * + * Retry (same policy as forgejo-api OPS67): transient failures retry with + * exponential backoff — a fetch that rejects (network-level, DNS/TCP/reset) + * retries on ANY method; a 5xx retries on GET only, so read polls survive + * while write endpoints keep failing closed instead of risking duplicated + * side effects. 4xx never retries. Defaults: 3 retries, base 300 ms ×2, + * ±20% jitter. + * + * Shapes are normalized to the contract the CLI scripts use: `pr.state` ∈ + * OPEN|CLOSED, `pr.draft`, `pr.nodeId` (GraphQL auto-merge), branch + * protection normalized for drift comparison. + */ + +const DEFAULT_BASE_URL = 'https://api.github.com' +const DEFAULT_GRAPHQL_URL = 'https://api.github.com/graphql' +const DEFAULT_REPOSITORY = 'fsolla/teqo' +const USER_AGENT = 'teqo-agent-scripts/1.0 (github)' +const RETRYABLE_STATUSES = new Set([502, 503, 504]) + +/** + * @typedef {object} GithubApiOptions + * @property {string} [base] + * @property {string} [graphqlBase] + * @property {string} [token] + * @property {string} [repository] + * @property {(input: string | URL, init?: { method?: string; headers?: Record; body?: string }) => Promise} [fetchImpl] + * @property {number} [retries] Additional attempts after the first (total = retries + 1). Default 3. + * @property {number} [backoffMs] Base exponential delay. Default 300. + * @property {boolean} [jitter] ±20% randomization on each delay. Default true. + * @property {(ms: number) => Promise} [sleepImpl] Test seam for the backoff delay. + */ + +/** + * @param {GithubApiOptions} [options] + */ +export const createApi = ({ + base, + graphqlBase, + token, + repository, + fetchImpl, + retries = 3, + backoffMs = 300, + jitter = true, + sleepImpl = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), +} = {}) => { + const baseUrl = (base ?? process.env.GITHUB_API_URL ?? DEFAULT_BASE_URL).replace(/\/+$/, '') + const graphqlUrl = (graphqlBase ?? process.env.GITHUB_GRAPHQL_URL ?? DEFAULT_GRAPHQL_URL).replace( + /\/+$/, + '', + ) + const repo = repository ?? process.env.GITHUB_REPOSITORY ?? DEFAULT_REPOSITORY + const fetcher = fetchImpl ?? fetch + const [owner, name] = repo.split('/') + if (retries < 0 || backoffMs < 0) { + throw new Error( + `Retry inválido: retries=${retries}, backoffMs=${backoffMs} — ambos devem ser >= 0`, + ) + } + const attemptCount = retries + 1 + + const backoffDelay = (attempt) => { + const base = backoffMs * 2 ** (attempt - 1) + return jitter ? Math.round(base * (0.8 + Math.random() * 0.4)) : base + } + + const warnRetry = (path, method, reason, attempt, delay) => + console.warn( + `[github-api] ${method} ${path} falhou (tentativa ${attempt}/${attemptCount}): ${reason} — retry em ${delay}ms`, + ) + + const headers = (withJson = true) => { + const authToken = token ?? process.env.GITHUB_TOKEN + if (!authToken) { + throw new Error('Sem token para a API do GitHub — defina GITHUB_TOKEN.') + } + return { + Authorization: `Bearer ${authToken}`, + 'User-Agent': USER_AGENT, + ...(withJson ? { 'Content-Type': 'application/json' } : {}), + } + } + + const request = async (path, { method = 'GET', body, query } = {}) => { + const qs = query + ? '?' + + Object.entries(query) + .filter(([, value]) => value !== undefined && value !== null) + .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`) + .join('&') + : '' + const url = `${baseUrl}${path}${qs}` + const retryAfter = async (attempt, reason) => { + const delay = backoffDelay(attempt) + warnRetry(path, method, reason, attempt, delay) + await sleepImpl(delay) + } + for (let attempt = 1; attempt <= attemptCount; attempt += 1) { + const isLastAttempt = attempt === attemptCount + let response + try { + response = await fetcher(url, { + method, + headers: headers(), + ...(body !== undefined ? { body: JSON.stringify(body) } : {}), + }) + } catch (error) { + if (isLastAttempt) throw error + await retryAfter(attempt, error.message) + continue + } + if (method === 'GET' && !isLastAttempt && RETRYABLE_STATUSES.has(response.status)) { + await response.body?.cancel() + await retryAfter(attempt, `HTTP ${response.status}`) + continue + } + let text + try { + text = await response.text() + } catch (error) { + if (isLastAttempt || method !== 'GET') throw error + await retryAfter(attempt, error.message) + continue + } + if (response.status === 404 && method === 'GET') return null + if (!response.ok) { + throw new Error(`GitHub API ${method} ${path} → ${response.status}: ${text.slice(0, 400)}`) + } + if (!text) return null + try { + return JSON.parse(text) + } catch { + throw new Error(`GitHub API ${method} ${path}: resposta não-JSON: ${text.slice(0, 80)}`) + } + } + } + + const graphql = async (query, variables) => { + let response + try { + response = await fetcher(graphqlUrl, { + method: 'POST', + headers: headers(), + body: JSON.stringify({ query, variables }), + }) + } catch (error) { + throw new Error(`GitHub GraphQL falhou (rede): ${error.message}`) + } + const text = await response.text() + if (!response.ok) { + throw new Error(`GitHub GraphQL → ${response.status}: ${text.slice(0, 400)}`) + } + const parsed = JSON.parse(text) + const firstError = parsed?.errors?.[0] + if (firstError) { + throw new Error(`GitHub GraphQL: ${firstError.message}`) + } + return parsed.data ?? null + } + + const normalizeState = (state) => (String(state).toLowerCase() === 'closed' ? 'CLOSED' : 'OPEN') + + const normalizePullRequest = (pr) => ({ + number: pr.number, + title: pr.title, + body: pr.body ?? '', + state: normalizeState(pr.state), + merged: Boolean(pr.merged_at || pr.merged), + draft: Boolean(pr.draft), + mergeable: pr.mergeable ?? null, + nodeId: pr.node_id ?? '', + head: { ref: pr.head?.ref ?? '', sha: pr.head?.sha ?? '' }, + base: { ref: pr.base?.ref ?? '' }, + }) + + const api = { + /** + * GET /repos/{owner}/{repo}/pulls/{number} — normalized. + * @param {number} number + */ + getPullRequest: async (number) => { + const pr = await request(`/repos/${owner}/${name}/pulls/${number}`) + return pr ? normalizePullRequest(pr) : null + }, + + /** PATCH draft:false — marks a draft PR ready for review. */ + markPullRequestReady: (number) => + request(`/repos/${owner}/${name}/pulls/${number}`, { + method: 'PATCH', + body: { draft: false }, + }), + + /** POST /pulls — creates a PR (never draft). */ + createPullRequest: async ({ head, base = 'main', title, body }) => { + const pr = await request(`/repos/${owner}/${name}/pulls`, { + method: 'POST', + body: { head, base, title, body }, + }) + return { number: pr.number, title: pr.title, htmlUrl: pr.html_url } + }, + + /** + * Enables GitHub's native auto-merge (GraphQL) with the repo's canonical + * merge style (rebase). The server waits for the required checks — a PR + * with a red `CI (PR) / checks` can never auto-merge. + */ + enableAutoMerge: async (nodeId, mergeMethod = 'REBASE') => { + await graphql( + ` + mutation EnablePullRequestAutoMerge($id: ID!, $mergeMethod: PullRequestMergeMethod!) { + enablePullRequestAutoMerge(input: { pullRequestId: $id, mergeMethod: $mergeMethod }) { + pullRequest { + number + } + } + } + `, + { id: nodeId, mergeMethod }, + ) + }, + + /** + * GET /repos/{owner}/{repo}/branches/{branch}/protection — normalized for + * drift comparison; `null` when the branch has no protection rule (404). + */ + getBranchProtection: async (branch = 'main') => { + const rule = await request(`/repos/${owner}/${name}/branches/${branch}/protection`) + if (!rule) return null + return { + required_status_checks: rule.required_status_checks + ? { + strict: Boolean(rule.required_status_checks.strict), + contexts: (rule.required_status_checks.checks ?? []).map((entry) => entry.context), + } + : null, + enforce_admins: Boolean(rule.enforce_admins?.enabled ?? rule.enforce_admins), + required_pull_request_reviews: rule.required_pull_request_reviews ?? null, + } + }, + + /** + * PUT /repos/{owner}/{repo}/branches/{branch}/protection — replaces the + * whole rule (GitHub replace semantics: omitted protections are removed). + */ + updateBranchProtection: (payload, branch = 'main') => + request(`/repos/${owner}/${name}/branches/${branch}/protection`, { + method: 'PUT', + body: payload, + }), + + /** PATCH /repos/{owner}/{repo} — repo settings (e.g. allow_auto_merge). */ + updateRepository: (payload) => + request(`/repos/${owner}/${name}`, { method: 'PATCH', body: payload }), + } + + return api +} diff --git a/scripts/lib/github-branch-protection.mjs b/scripts/lib/github-branch-protection.mjs new file mode 100644 index 00000000..aba11f21 --- /dev/null +++ b/scripts/lib/github-branch-protection.mjs @@ -0,0 +1,78 @@ +/** + * Pure decision for the GitHub `main` branch protection rule (OPS71). + * + * The desired rule is the repo contract: main requires the ci-pr cascade + * check-run (OPS62) as the only status check (strict=false, 0 reviews) and + * `enforce_admins: true` so even repo admins cannot merge with a red required + * check (the rule-based protection of the Forgejo era applied to everyone — + * preserved). + * + * The auto-merge safety net never merges on red CI by construction (GitHub's + * server-side guarantee), but the branch protection rule is the final defense + * against every other merge path (manual API merges included). + * + * Context literal (verified live on PR #742, 2026-08-19): GitHub's + * required-check matching uses the CHECK-RUN NAME — the bare JOB name + * (`checks`), NOT the UI display ` / ` + * (`CI (PR) / checks`). The latter never matches and leaves the PR + * mergeable_state "blocked" forever. The UI still shows + * `CI (PR) / checks`; the rule literal is `checks`. + */ + +export const REQUIRED_CHECK_CONTEXT = 'checks' + +/** + * GitHub PUT /branches/{branch}/protection replace-semantics payload. Only + * `checks` (modern) is sent — `contexts` is legacy-derived and GitHub's API + * rejects the two together (422 anyOf). The GET normalization reads the + * check-run contexts from `checks`, so drift comparison is unaffected. + */ +export const DESIRED_RULE = Object.freeze({ + required_status_checks: { + strict: false, + checks: [{ context: REQUIRED_CHECK_CONTEXT }], + }, + enforce_admins: true, + required_pull_request_reviews: null, + restrictions: null, + required_linear_history: false, + allow_force_pushes: false, + allow_deletions: false, + block_creations: false, +}) + +const sameStrings = (have, want) => + have.length === want.length && + want.every((value) => have.includes(value)) && + have.every((value) => want.includes(value)) + +/** + * True when the existing rule (from `getBranchProtection`, already + * normalized) matches the desired rule on every compared field — array + * fields as sets. + */ +export const ruleMatches = (existing) => { + if (!existing) return false + const wantStatus = DESIRED_RULE.required_status_checks + if (existing.enforce_admins !== DESIRED_RULE.enforce_admins) return false + if (existing.required_pull_request_reviews !== null) return false + const haveStatus = existing.required_status_checks + if (!haveStatus) return false + if (haveStatus.strict !== wantStatus.strict) return false + return sameStrings( + haveStatus.contexts, + wantStatus.checks.map((entry) => entry.context), + ) +} + +/** + * What to do with the rule: `create` (no rule), `update` (rule exists with + * drift), `noop` (already compliant). Idempotent re-runs land on `noop`. + * + * @param {object|null} existing - normalized rule from the API, or null + * @returns {{ action: 'create' | 'update' | 'noop' }} + */ +export const planBranchProtectionRule = (existing) => { + if (ruleMatches(existing)) return { action: 'noop' } + return { action: existing ? 'update' : 'create' } +} diff --git a/scripts/lib/github-pr-flow.mjs b/scripts/lib/github-pr-flow.mjs new file mode 100644 index 00000000..17823876 --- /dev/null +++ b/scripts/lib/github-pr-flow.mjs @@ -0,0 +1,52 @@ +/** + * Pure decision logic for the GitHub PR auto-merge safety net + * (scripts/github-pr-automerge.mjs). Unit-tested in + * tests/unit/githubPrFlow.unit.spec.ts — the OPS57 (draft veto) / OPS64 + * (never merge on red CI) pins ported to the GitHub mechanism. + * + * GitHub's native auto-merge only fires with the required checks green, so + * the safety net's only real decisions are: skip (non-main base, draft veto, + * already merged) or mark-ready (cursor/* draft) before arming auto-merge. + */ + +export const CURSOR_HEAD_PREFIX = 'cursor/' + +/** OPS57: only `cursor/*` drafts get force-marked ready — any other draft is the actor's veto. */ +export const isCursorHead = (ref) => typeof ref === 'string' && ref.startsWith(CURSOR_HEAD_PREFIX) + +/** + * OPS71-FLIP: the auto-merge MUST be armed with a real user PAT. Arming with + * the built-in GITHUB_TOKEN makes the merge happen as `github-actions[bot]`, + * and GitHub's anti-recursion never creates workflow runs for events + * triggered by GITHUB_TOKEN actions — the post-merge `closed` flips would + * silently never run (live finding PR #746). Fail-closed: no token → ok:false; + * the CLI exits 1 instead of falling back to the run token. + * + * @param {{ AUTOMERGE_PAT?: string }} [env] + * @returns {{ ok: true, token: string } | { ok: false, reason: string }} + */ +export const automergeArmingToken = (env = process.env) => { + const token = env.AUTOMERGE_PAT + if (typeof token === 'string' && token.length > 0) return { ok: true, token } + return { + ok: false, + reason: + 'AUTOMERGE_PAT ausente — sem ele o merge aconteceria como github-actions[bot] e o evento closed não criaria runs (flips pós-merge silenciosos). Defina o secret AUTOMERGE_PAT no GitHub (PAT do fsolla, escopos pull-requests: write + contents: write no repo) — ver docs/AGENT-OPS.md.', + } +} + +/** + * @param {object|null} pr - normalized PR from github-api (or null) + * @returns {{ action: 'skip' | 'mark-ready' | 'enable-auto-merge', reason: string }} + */ +export const decideAutomergeAction = (pr) => { + if (!pr) return { action: 'skip', reason: 'pr-inexistente' } + if (pr.merged) return { action: 'skip', reason: 'ja-mergeada' } + if (pr.state !== 'OPEN') return { action: 'skip', reason: 'pr-nao-aberta' } + if (pr.base?.ref !== 'main') return { action: 'skip', reason: 'base-nao-main' } + if (pr.draft) { + if (isCursorHead(pr.head?.ref)) return { action: 'mark-ready', reason: 'draft-cursor' } + return { action: 'skip', reason: 'draft-veto' } + } + return { action: 'enable-auto-merge', reason: 'ready' } +} diff --git a/scripts/lib/test-affected-core.mjs b/scripts/lib/test-affected-core.mjs index 709c10d7..5225b5ae 100644 --- a/scripts/lib/test-affected-core.mjs +++ b/scripts/lib/test-affected-core.mjs @@ -29,8 +29,6 @@ export const HIGH_RISK_EXACT = new Set([ 'scripts/e2e-affected.mjs', 'scripts/run-e2e-affected.mjs', 'scripts/ci-scope.mjs', - 'scripts/ci-classify-production.mjs', - 'scripts/lib/dockerignore.mjs', 'scripts/check-test-locations.mjs', 'scripts/check-plans-only-pr-closes.mjs', 'scripts/lib/plansOnlyClosesGuard.mjs', diff --git a/src/app/(campaign)/campanha/(app)/acoes/[slug]/page.tsx b/src/app/(campaign)/campanha/(app)/acoes/[slug]/page.tsx index 247dbaf3..71ae3758 100644 --- a/src/app/(campaign)/campanha/(app)/acoes/[slug]/page.tsx +++ b/src/app/(campaign)/campanha/(app)/acoes/[slug]/page.tsx @@ -199,6 +199,7 @@ export default async function CampaignActionWizardPage({ municipalityName={municipality.name} municipalitySlug={municipality.slug} returnPath={returnPath} + currentUser={{ id: user.id, name: user.name }} /> ) } diff --git a/src/app/(campaign)/campanha/(app)/demandas/[slug]/formActions.ts b/src/app/(campaign)/campanha/(app)/demandas/[slug]/formActions.ts index cdbcf193..321661d1 100644 --- a/src/app/(campaign)/campanha/(app)/demandas/[slug]/formActions.ts +++ b/src/app/(campaign)/campanha/(app)/demandas/[slug]/formActions.ts @@ -5,12 +5,14 @@ import { revalidatePath } from 'next/cache' import { attachCampaignDemandReceiptRecord, setCampaignDemandCost, + setCampaignDemandResponsibles, transitionCampaignDemand, updateCampaignDemand, } from '@/app/(campaign)/campanha/actions/demand' import { FormDataBoundaryError, nullableFormText, + repeatedRelationshipFormValues, requiredFormText, requiredRelationshipFormValue, } from '@/lib/formData' @@ -19,6 +21,7 @@ import { CAMPAIGN_DEMAND_EDIT_STAFF_MESSAGE, CAMPAIGN_DEMAND_INVALID_STATUS_MESSAGE, CAMPAIGN_DEMAND_RECEIPT_SAFE_MESSAGES, + CAMPAIGN_DEMAND_RESPONSIBLES_STAFF_MESSAGE, CAMPAIGN_DEMAND_TRANSITION_SAFE_MESSAGES, campaignDemandStatuses, type CampaignDemandStatus, @@ -100,6 +103,23 @@ export const setDemandCostFormAction = async ( genericMessage: 'Não foi possível registrar o custo. Tente novamente.', }) +export const setDemandResponsiblesFormAction = async ( + _state: CampaignFormActionState, + formData: FormData, +): Promise => + runCampaignFormAction({ + execute: async () => { + await setCampaignDemandResponsibles({ + id: requiredRelationshipFormValue(formData, 'demandId'), + responsibles: repeatedRelationshipFormValues(formData, 'responsibles'), + }) + revalidatePath('/campanha/demandas/[slug]', 'page') + return { message: 'Responsáveis atualizados.' } + }, + safeMessages: [CAMPAIGN_DEMAND_RESPONSIBLES_STAFF_MESSAGE], + genericMessage: 'Não foi possível atualizar os responsáveis. Tente novamente.', + }) + export const attachDemandReceiptFormAction = async ( _state: CampaignFormActionState, formData: FormData, diff --git a/src/app/(campaign)/campanha/(app)/demandas/[slug]/page.tsx b/src/app/(campaign)/campanha/(app)/demandas/[slug]/page.tsx index 15a9619e..a1c0ce17 100644 --- a/src/app/(campaign)/campanha/(app)/demandas/[slug]/page.tsx +++ b/src/app/(campaign)/campanha/(app)/demandas/[slug]/page.tsx @@ -5,6 +5,7 @@ import { notFound } from 'next/navigation' import { getPayload } from 'payload' import { DemandDescriptionEditor } from '@/components/campaign/demand/DemandDescriptionEditor' +import { DemandResponsiblesCard } from '@/components/campaign/demand/DemandResponsiblesCard' import { DemandWorkflowCard } from '@/components/campaign/demand/DemandWorkflowCard' import { SetCampaignPageChrome } from '@/components/campaign/shell/CampaignPageChromeContext' import { CampaignPageShell } from '@/components/campaign/shell/CampaignPageShell' @@ -22,6 +23,7 @@ import { requireCampaignPageActor } from '@/utilities/campaignPageActor' import { attachDemandReceiptFormAction, setDemandCostFormAction, + setDemandResponsiblesFormAction, transitionDemandFormAction, updateDemandFormAction, } from './formActions' @@ -118,87 +120,106 @@ export default async function DemandDetailPage({ params }: DemandDetailPageProps {dateTimeFormatter.format(new Date(demand.createdAt))}

- +
+
+ - {demand.decisionNote && (demand.status === 'aprovada' || demand.status === 'rejeitada') ? ( -
-

Nota da decisão

-

- {demand.decisionNote} -

- {demand.decidedAt ? ( -

- Decidida em {dateTimeFormatter.format(new Date(demand.decidedAt))} -

+ {demand.decisionNote && + (demand.status === 'aprovada' || demand.status === 'rejeitada') ? ( +
+

Nota da decisão

+

+ {demand.decisionNote} +

+ {demand.decidedAt ? ( +

+ Decidida em {dateTimeFormatter.format(new Date(demand.decidedAt))} +

+ ) : null} +
) : null} -
- ) : null} - + -
-

- Controle interno -

-

- Custo: {demand.cost != null ? currencyFormatter.format(demand.cost) : 'não registrado'} -

- {demand.receipts.length ? ( - - ) : ( -

Nenhum comprovante anexado.

- )} - {demand.statusHistory.length ? ( -
-

Histórico

-
    - {demand.statusHistory.map((entry, index) => ( -
  • - {campaignDemandStatusLabels[entry.status]} - {entry.authorName ? ` — ${entry.authorName}` : ''} - {entry.createdAt - ? ` · ${dateTimeFormatter.format(new Date(entry.createdAt))}` - : ''} - {entry.note ? ` · ${entry.note}` : ''} -
  • - ))} -
-
- ) : null} -
+
+

+ Controle interno +

+

+ Visível para: Responsáveis ·{' '} + Candidato ·{' '} + Coordenador +

+

+ Custo:{' '} + {demand.cost != null ? currencyFormatter.format(demand.cost) : 'não registrado'} +

+ {demand.receipts.length ? ( + + ) : ( +

Nenhum comprovante anexado.

+ )} + {demand.statusHistory.length ? ( +
+

Histórico

+
    + {demand.statusHistory.map((entry, index) => ( +
  • + {campaignDemandStatusLabels[entry.status]} + {entry.authorName ? ` — ${entry.authorName}` : ''} + {entry.createdAt + ? ` · ${dateTimeFormatter.format(new Date(entry.createdAt))}` + : ''} + {entry.note ? ` · ${entry.note}` : ''} +
  • + ))} +
+
+ ) : null} +
+
+ + 0 ? demand.municipalityId : null} + creatorUserId={demand.createdBy} + initialResponsibles={demand.responsibles} + formAction={setDemandResponsiblesFormAction} + /> +
) } diff --git a/src/app/(campaign)/campanha/(app)/demandas/nova/page.tsx b/src/app/(campaign)/campanha/(app)/demandas/nova/page.tsx index 91b08834..be7565a8 100644 --- a/src/app/(campaign)/campanha/(app)/demandas/nova/page.tsx +++ b/src/app/(campaign)/campanha/(app)/demandas/nova/page.tsx @@ -52,6 +52,7 @@ export default async function NewDemandPage({ searchParams }: NewDemandPageProps municipalityOptions={municipalityOptions} initialActivity={initialActivity} initialMunicipalityId={initialMunicipalityId} + currentUser={{ id: user.id, name: user.name }} searchActivities={searchDemandActivityOptions} formAction={createDemandFormAction} /> diff --git a/src/app/(campaign)/campanha/(app)/demandas/responsibleSearchActions.ts b/src/app/(campaign)/campanha/(app)/demandas/responsibleSearchActions.ts new file mode 100644 index 00000000..42e211db --- /dev/null +++ b/src/app/(campaign)/campanha/(app)/demandas/responsibleSearchActions.ts @@ -0,0 +1,61 @@ +'use server' + +import config from '@payload-config' +import { getPayload } from 'payload' + +import type { RelationOption } from '@/components/campaign/shared/RelationMultiSelect' +import { eligibleCampaignStaffWhere } from '@/utilities/campaignAccess' +import { getCampaignUser } from '@/utilities/campaignAuth' +import { CAMPAIGN_AUTH_REQUIRED_MESSAGE } from '@/utilities/campaignFormActionError' +import { loadCampaignUserSummaries } from '@/utilities/campaignRelationOptions' + +const RESPONSIBLE_SEARCH_LIMIT = 20 + +/** + * C143 — candidate catalog for the demand responsible picker. Short queries + * (or empty) return the municipality's advisors as suggestions — a fill-in + * shortcut, never an automatic responsibility. Longer queries search staff by + * name. Both paths run access-scoped reads (`overrideAccess: false`), so the + * catalog only ever offers what the actor may already read. + */ +export const searchDemandResponsibleOptions = async ( + query: string, + municipalityId: number | null, +): Promise => { + const [payload, user] = await Promise.all([getPayload({ config }), getCampaignUser()]) + if (!user) throw new Error(CAMPAIGN_AUTH_REQUIRED_MESSAGE) + + if (query.trim().length < 2) { + if (!municipalityId) return [] + + const municipality = await payload.find({ + collection: 'municipality', + where: { id: { equals: municipalityId } }, + depth: 0, + limit: 1, + pagination: false, + select: { advisors: true }, + user, + overrideAccess: false, + }) + const advisors = (municipality.docs[0]?.advisors ?? []) + .map((advisor) => (typeof advisor === 'object' && advisor !== null ? advisor.id : advisor)) + .filter((id): id is number => Number.isInteger(id)) + return loadCampaignUserSummaries(payload, user, advisors) + } + + const result = await payload.find({ + collection: 'campaignUser', + where: { + and: [{ name: { contains: query.trim() } }, eligibleCampaignStaffWhere], + }, + depth: 0, + limit: RESPONSIBLE_SEARCH_LIMIT, + pagination: false, + sort: 'name', + select: { name: true }, + user, + overrideAccess: false, + }) + return result.docs.map(({ id, name }) => ({ id, name })) +} diff --git a/src/app/(campaign)/campanha/actions/demand.ts b/src/app/(campaign)/campanha/actions/demand.ts index 5c011378..1916ca84 100644 --- a/src/app/(campaign)/campanha/actions/demand.ts +++ b/src/app/(campaign)/campanha/actions/demand.ts @@ -11,15 +11,18 @@ import { CAMPAIGN_DEMAND_RECEIPT_SIZE_MESSAGE, CAMPAIGN_DEMAND_RECEIPT_STAFF_MESSAGE, CAMPAIGN_DEMAND_RECEIPT_TYPE_MESSAGE, + CAMPAIGN_DEMAND_RESPONSIBLES_STAFF_MESSAGE, CAMPAIGN_DEMAND_TRANSITION_STAFF_MESSAGE, } from '@/lib/schemas/campaignDemand' import { campaignDemandCostSchema, campaignDemandCreateSchema, + campaignDemandResponsiblesSchema, campaignDemandTransitionSchema, campaignDemandUpdateSchema, type CampaignDemandCostInput, type CampaignDemandCreateInput, + type CampaignDemandResponsiblesInput, type CampaignDemandTransitionInput, type CampaignDemandUpdateInput, } from '@/lib/schemas/campaignDemandInput' @@ -84,8 +87,9 @@ export const updateCampaignDemandRecord = async ( throw new Error(CAMPAIGN_DEMAND_EDIT_STAFF_MESSAGE) } - // Access-scoped read (staff see only their municipalities). The AI call - // stays before the advisory lock, so the lock is never held open by it. + // Access-scoped read (C143: responsibles + unrestricted see the demand). + // The AI call stays before the advisory lock, so the lock is never held + // open by it. const demand = await payload.findByID({ collection: 'campaignDemand', id, @@ -256,6 +260,47 @@ export const attachCampaignDemandReceiptRecord = async ( ) } +export const setCampaignDemandResponsiblesRecord = async ( + payload: Payload, + actor: CampaignUser, + input: CampaignDemandResponsiblesInput, +) => { + const { id, responsibles } = campaignDemandResponsiblesSchema.parse(input) + + return withPayloadTransaction( + payload, + async ({ req }) => { + const currentActor = await reloadCampaignActor(payload, actor, req) + if (!isCampaignStaff(currentActor)) { + throw new Error(CAMPAIGN_DEMAND_RESPONSIBLES_STAFF_MESSAGE) + } + await acquireTextAdvisoryLocks(payload, req, [`campaign-demand:${id}`]) + + // Access-scoped read (responsibles + unrestricted see it) before the + // replace, so an advisor cannot manage a demand they cannot read. + await payload.findByID({ + collection: 'campaignDemand', + id, + depth: 0, + select: { title: true }, + user: currentActor, + overrideAccess: false, + req, + }) + return payload.update({ + collection: 'campaignDemand', + id, + data: { responsibles: [...new Set(responsibles)] }, + depth: 0, + user: currentActor, + overrideAccess: false, + req, + }) + }, + { beginFailureMessage: 'Não foi possível iniciar a atualização dos responsáveis.' }, + ) +} + export const createCampaignDemand = async (input: CampaignDemandCreateInput) => { const { payload, actor } = await getCampaignActionContext() return createCampaignDemandRecord(payload, actor, input) @@ -275,3 +320,8 @@ export const setCampaignDemandCost = async (input: CampaignDemandCostInput) => { const { payload, actor } = await getCampaignActionContext() return setCampaignDemandCostRecord(payload, actor, input) } + +export const setCampaignDemandResponsibles = async (input: CampaignDemandResponsiblesInput) => { + const { payload, actor } = await getCampaignActionContext() + return setCampaignDemandResponsiblesRecord(payload, actor, input) +} diff --git a/src/app/(frontend)/api/social-feed/sync/route.ts b/src/app/(frontend)/api/social-feed/sync/route.ts new file mode 100644 index 00000000..a4a22a05 --- /dev/null +++ b/src/app/(frontend)/api/social-feed/sync/route.ts @@ -0,0 +1,41 @@ +import { REVALIDATE_SOCIAL_FEED_TAG } from '@/utilities/revalidateRequest' +import { isSameOriginRequest } from '@/utilities/sameOriginRequest' +import { INSTAGRAM_SYNC_TIMEOUT_MS, syncInstagramFeed } from '@/utilities/socialFeed/instagramSync' +import configPromise from '@payload-config' +import { revalidateTag } from 'next/cache' +import { NextResponse } from 'next/server' +import { getPayload } from 'payload' + +/** + * "Tentar sincronizar de novo" — the admin status panel's retry button + * (`InstagramSyncStatusPanel`). Runs one Instagram sync from the persisted + * global settings, writes the outcome status, busts the `social-feed` cache + * so the public board reflects the new snapshot, and returns the status the + * panel renders immediately. + * + * Auth: admin session (the `payload-token` cookie via `payload.auth`, `users` + * collection only — same gate as the global's `read`) plus the same-origin + * CSRF guard every cookie-authenticated JSON route uses. The token and the + * Graph API are never reachable without an admin session. + */ +export async function POST(request: Request): Promise { + if (!isSameOriginRequest(request)) { + return NextResponse.json({ ok: false, error: 'Forbidden' }, { status: 403 }) + } + + const payload = await getPayload({ config: configPromise }) + const { user } = await payload.auth({ headers: request.headers }) + if (!user || user.collection !== 'users') { + return NextResponse.json({ ok: false, error: 'Unauthorized' }, { status: 401 }) + } + + try { + const outcome = await syncInstagramFeed(payload, { + signal: AbortSignal.timeout(INSTAGRAM_SYNC_TIMEOUT_MS), + }) + revalidateTag(REVALIDATE_SOCIAL_FEED_TAG) + return NextResponse.json({ ok: outcome.ok, status: outcome.status }) + } catch { + return NextResponse.json({ ok: false, error: 'Falha interna ao sincronizar' }, { status: 500 }) + } +} diff --git a/src/app/(payload)/admin/importMap.js b/src/app/(payload)/admin/importMap.js index 2f6f0ae6..097d2c4e 100644 --- a/src/app/(payload)/admin/importMap.js +++ b/src/app/(payload)/admin/importMap.js @@ -34,7 +34,9 @@ import { ExportPreview as ExportPreview_cdf7e044479f899a31f804427d568b36 } from import { ExportSaveButton as ExportSaveButton_cdf7e044479f899a31f804427d568b36 } from '@payloadcms/plugin-import-export/rsc' import { ImportPreview as ImportPreview_cdf7e044479f899a31f804427d568b36 } from '@payloadcms/plugin-import-export/rsc' import { ImportSaveButton as ImportSaveButton_cdf7e044479f899a31f804427d568b36 } from '@payloadcms/plugin-import-export/rsc' +import { InstagramSyncStatusPanel as InstagramSyncStatusPanel_90dd38ffecf41d93e389c788f98f48da } from '../../../components/admin/InstagramSyncStatusPanel' import { InstagramPostExclusionPicker as InstagramPostExclusionPicker_ba03418db242c21470543bbeb06827a0 } from '../../../components/admin/InstagramPostExclusionPicker' +import { S3ClientUploadHandler as S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24 } from '@payloadcms/storage-s3/client' import { ImportExportProvider as ImportExportProvider_cdf7e044479f899a31f804427d568b36 } from '@payloadcms/plugin-import-export/rsc' import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc' @@ -75,7 +77,9 @@ export const importMap = { "@payloadcms/plugin-import-export/rsc#ExportSaveButton": ExportSaveButton_cdf7e044479f899a31f804427d568b36, "@payloadcms/plugin-import-export/rsc#ImportPreview": ImportPreview_cdf7e044479f899a31f804427d568b36, "@payloadcms/plugin-import-export/rsc#ImportSaveButton": ImportSaveButton_cdf7e044479f899a31f804427d568b36, + "./components/admin/InstagramSyncStatusPanel#InstagramSyncStatusPanel": InstagramSyncStatusPanel_90dd38ffecf41d93e389c788f98f48da, "./components/admin/InstagramPostExclusionPicker#InstagramPostExclusionPicker": InstagramPostExclusionPicker_ba03418db242c21470543bbeb06827a0, + "@payloadcms/storage-s3/client#S3ClientUploadHandler": S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24, "@payloadcms/plugin-import-export/rsc#ImportExportProvider": ImportExportProvider_cdf7e044479f899a31f804427d568b36, "@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } diff --git a/src/collections/CampaignDemand.ts b/src/collections/CampaignDemand.ts index 7bd523a3..d834ccb8 100644 --- a/src/collections/CampaignDemand.ts +++ b/src/collections/CampaignDemand.ts @@ -26,6 +26,7 @@ import { canReadCampaignStaffField, canSetCampaignSystemField, canUpdateCampaignDemand, + eligibleCampaignStaffWhere, isCampaignUnrestricted, } from '@/utilities/campaignAccess' import { systemStampedActorField } from '@/utilities/campaignAuditFields' @@ -41,6 +42,40 @@ const DEMAND_STATUS_OPTIONS = campaignDemandStatuses.map((value) => ({ label: campaignDemandStatusLabels[value], })) +/** + * C143 — responsibles are always eligible staff (coordinator/advisor/candidate). + * The picker catalog already filters, but the invariant belongs in the data + * path, fail-closed: forged/non-eligible ids (a leader, a deleted account) are + * dropped from any create/update. Intentional admin bypass — the hook must SEE + * the non-eligible rows to drop them (same precedent as the slug probe). + */ +const restrictResponsiblesToStaff: CollectionBeforeValidateHook = async ({ data, req }) => { + if (!data) return data + const rawResponsibles = data.responsibles + if (!Array.isArray(rawResponsibles) || rawResponsibles.length === 0) return data + + const ids = rawResponsibles + .map((entry) => relationshipId(entry)) + .filter((id): id is number => id !== null) + if (ids.length === 0) return data + + // Bypass (documented in the hook's comment): the eligibility probe must see + // the non-eligible rows (leaders, deleted accounts) to drop them. + const eligible = await req.payload.find({ + collection: 'campaignUser', + where: { and: [{ id: { in: ids } }, eligibleCampaignStaffWhere] }, + depth: 0, + limit: 0, + pagination: false, + select: { name: true }, + overrideAccess: true, + req, + }) + const eligibleIDs = new Set(eligible.docs.map((doc) => doc.id)) + data.responsibles = [...new Set(ids.filter((id) => eligibleIDs.has(id)))] + return data +} + const setCanonicalDemandSlug: CollectionBeforeValidateHook = ({ data, operation, originalDoc }) => { if (!data) return data const title = trimmedText(data.title ?? originalDoc?.title) @@ -127,7 +162,19 @@ const enforceDemandWorkflow: CollectionBeforeChangeHook = async ({ const actor = req.user?.collection === 'campaignUser' ? req.user : null if (operation === 'create') { - if (actor) data.createdBy = actor.id + if (actor) { + data.createdBy = actor.id + + // C143 — the creator is always a responsible (explicit-responsible + // visibility): union with any client-provided list so a create can + // never leave its author outside the demand's own visibility. + const existingResponsibles = Array.isArray(data.responsibles) + ? data.responsibles + .map((entry) => relationshipId(entry)) + .filter((id): id is number => id !== null) + : [] + data.responsibles = [...new Set([...existingResponsibles, actor.id])] + } const initialStatus = isDemandStatus(data.status) ? data.status : 'aberta' data.status = initialStatus @@ -255,6 +302,7 @@ export const CampaignDemand: CollectionConfig = { }, hooks: { beforeValidate: [ + restrictResponsiblesToStaff, setCanonicalDemandSlug, ensureUniqueDemandSlug, validateDemandActivityMunicipality, @@ -322,6 +370,19 @@ export const CampaignDemand: CollectionConfig = { label: 'Liderança solicitante', index: true, }, + { + name: 'responsibles', + type: 'relationship', + relationTo: 'campaignUser', + label: 'Responsáveis', + hasMany: true, + index: true, + filterOptions: eligibleCampaignStaffWhere, + admin: { + description: + 'Só responsáveis, candidato e coordenador veem a demanda. O criador entra automaticamente.', + }, + }, { name: 'status', type: 'select', diff --git a/src/components/CampaignContentSection.tsx b/src/components/CampaignContentSection.tsx index 4fe90bf6..2ffb55ab 100644 --- a/src/components/CampaignContentSection.tsx +++ b/src/components/CampaignContentSection.tsx @@ -7,7 +7,8 @@ import { getPostCanonicalPath, getVisiblePosts, } from '@/utilities/posts' -import { getInstagramFeed, type InstagramPost } from '@/utilities/socialFeed/instagramFeed' +import type { InstagramPost } from '@/utilities/socialFeed/instagramFeed' +import { getInstagramFeed } from '@/utilities/socialFeed/instagramFeedView' import { formatYouTubeViews, getYouTubeFeed, diff --git a/src/components/admin/InstagramPostExclusionPicker.tsx b/src/components/admin/InstagramPostExclusionPicker.tsx index 9d4cb933..62d36d03 100644 --- a/src/components/admin/InstagramPostExclusionPicker.tsx +++ b/src/components/admin/InstagramPostExclusionPicker.tsx @@ -67,8 +67,8 @@ export const InstagramPostExclusionPicker: FieldClientComponent = () => { if (posts.length === 0) { return (

- Nenhum post recente ainda. Com o token e o ID do usuário configurados, os posts aparecem - aqui após a primeira sincronização (a cada 5 minutos). + Nenhum post sincronizado. Após uma sincronização bem-sucedida, os últimos posts aparecem + aqui para marcar os que não devem ir para a home.

) } diff --git a/src/components/admin/InstagramSyncStatusPanel.tsx b/src/components/admin/InstagramSyncStatusPanel.tsx new file mode 100644 index 00000000..00ff9c84 --- /dev/null +++ b/src/components/admin/InstagramSyncStatusPanel.tsx @@ -0,0 +1,186 @@ +'use client' + +import { useAllFormFields } from '@payloadcms/ui' +import type { FieldClientComponent } from 'payload' +import { useEffect, useState } from 'react' + +type InstagramSyncStatusValue = { + lastSyncAt?: string + postCount?: number + error?: string + errorAt?: string +} + +type SyncResponse = { + ok: boolean + status?: InstagramSyncStatusValue + error?: string +} + +const isSyncStatus = (value: unknown): value is InstagramSyncStatusValue => + typeof value === 'object' && value !== null + +const relativeTimeFormatter = new Intl.RelativeTimeFormat('pt-BR', { numeric: 'auto' }) + +const formatSyncTime = (iso?: string): string => { + if (!iso) return '' + const time = new Date(iso).getTime() + if (Number.isNaN(time)) return '' + const seconds = Math.floor((time - Date.now()) / 1000) + if (seconds > -60) return 'agora' + const absSeconds = -seconds + if (absSeconds < 3_600) { + return relativeTimeFormatter.format(-Math.floor(absSeconds / 60), 'minute') + } + if (absSeconds < 86_400) { + return relativeTimeFormatter.format(-Math.floor(absSeconds / 3_600), 'hour') + } + return relativeTimeFormatter.format(-Math.floor(absSeconds / 86_400), 'day') +} + +const postCountLabel = (count?: number): string => + count === undefined ? '' : `${count} ${count === 1 ? 'post' : 'posts'}` + +/** + * Sync-status panel of the Instagram feed (S11) — the S3 fail-closed silence + * made the assessoria blind: a wrong token left no trace beyond an empty + * picker. This panel shows, in the same global where the credentials live, + * the state of the last sync — "Sincronizado · há X min · N posts", + * "Falha na última sincronização" with the product-language reason, or + * "Instagram ainda não configurado" — with a "Tentar sincronizar de novo" + * button that runs `POST /api/social-feed/sync` (admin session) and renders + * the outcome. The status itself is persisted server-side by the render + * path, the global's `afterChange` and the sync route; this component only + * reads it from the form and keeps the in-flight result locally. + */ +export const InstagramSyncStatusPanel: FieldClientComponent = () => { + const [fields] = useAllFormFields() + const [outcome, setOutcome] = useState(null) + const [pending, setPending] = useState(false) + const [requestError, setRequestError] = useState(null) + + const enabled = fields.enabled?.value !== false && fields.instagramEnabled?.value !== false + const accessToken = fields.instagramAccessToken?.value + const userId = fields.instagramUserId?.value + const stored = fields.instagramSyncStatus?.value + const persisted = isSyncStatus(stored) ? stored : {} + + // The panel shows the SERVER-persisted state; the transient retry result in + // `outcome` must not outlive an edit of the credentials it was produced with + // (otherwise it keeps showing the failure of the old token pre-save). + const credentialKey = typeof accessToken === 'string' ? accessToken : '' + const userIdKey = typeof userId === 'string' ? userId : '' + useEffect(() => { + setOutcome(null) + setRequestError(null) + }, [credentialKey, userIdKey]) + + const configured = + typeof accessToken === 'string' && + accessToken !== '' && + typeof userId === 'string' && + userId !== '' + + const status = outcome?.status && isSyncStatus(outcome.status) ? outcome.status : persisted + const failureMessage = status.error ?? null + const lastSyncAt = status.lastSyncAt + const postCount = status.postCount + + const retrySync = async () => { + setPending(true) + setRequestError(null) + try { + const response = await fetch('/api/social-feed/sync', { method: 'POST' }) + const body = (await response.json()) as SyncResponse + if (response.status === 401) { + setRequestError( + 'Sessão do admin expirada. Salve a página e recarregue o admin para reautenticar.', + ) + return + } + if (!response.ok || body.error) { + throw new Error(body.error ?? 'Falha na sincronização') + } + setOutcome(body) + } catch { + setRequestError('Não foi possível sincronizar agora. Tente novamente em instantes.') + } finally { + setPending(false) + } + } + + const retryButton = (label: string = 'Tentar sincronizar de novo') => ( + + ) + + if (!enabled) { + return ( +
+

Instagram desativado

+

+ Os posts do Instagram não aparecem no board enquanto o feed estiver desativado. +

+
+ ) + } + + if (!configured) { + return ( +
+

Instagram ainda não configurado

+

+ Informe o token de acesso e o ID da conta para publicar posts do Instagram na home. Sem + configuração, o board funciona com artigos e YouTube. +

+
+ ) + } + + if (failureMessage) { + return ( +
+

Falha na última sincronização

+

{failureMessage}

+ {requestError ?

{requestError}

: null} + {retryButton()} +
+ ) + } + + if (lastSyncAt) { + return ( +
+

+ Sincronizado · {formatSyncTime(lastSyncAt)} + {postCount !== undefined ? ` · ${postCountLabel(postCount)}` : ''} +

+

+ Próxima atualização automática em ~5 min.{' '} + {postCount !== undefined + ? `Os ${postCountLabel(postCount)} mais recentes estão no board "Acompanhe de perto" da home.` + : 'Os posts mais recentes estão no board "Acompanhe de perto" da home.'} +

+
+ ) + } + + return ( +
+

Aguardando a primeira sincronização

+

+ Clique em "Tentar sincronizar" para buscar os posts do Instagram agora, ou aguarde + a próxima atualização automática. +

+ {requestError ?

{requestError}

: null} + {retryButton('Tentar sincronizar')} +
+ ) +} diff --git a/src/components/campaign/demand/DemandFields.tsx b/src/components/campaign/demand/DemandFields.tsx index 20fe57cc..9a36dce6 100644 --- a/src/components/campaign/demand/DemandFields.tsx +++ b/src/components/campaign/demand/DemandFields.tsx @@ -1,5 +1,9 @@ 'use client' +import { + DemandResponsibleMultiSelect, + type DemandResponsibleOption, +} from '@/components/campaign/demand/DemandResponsibleMultiSelect' import { AsyncSearchCombobox } from '@/components/campaign/shared/AsyncSearchCombobox' import type { RelationOption } from '@/components/campaign/shared/RelationMultiSelect' import { Button } from '@/components/ui/button' @@ -16,6 +20,8 @@ import { CAMPAIGN_DEMAND_BODY_MAX_LENGTH, CAMPAIGN_DEMAND_BODY_PLACEHOLDER, CAMPAIGN_DEMAND_KIND_LABEL, + CAMPAIGN_DEMAND_RESPONSIBLES_DESCRIPTION, + CAMPAIGN_DEMAND_RESPONSIBLES_LABEL, campaignDemandKindLabels, campaignDemandKinds, } from '@/lib/schemas/campaignDemand' @@ -38,6 +44,12 @@ type DemandFieldsProps = { value: string onValueChange: (value: string) => void } + /** C143 — explicit-responsible picker; `aside` splits a right column (nova). */ + responsibles?: { + currentUser: { id: number; name: string } | null + search: (query: string) => Promise + layout: 'aside' | 'stacked' + } } /** @@ -53,6 +65,7 @@ export const DemandFields = ({ onActivityChange, searchActivities, municipality, + responsibles, }: DemandFieldsProps) => { const municipalityReady = municipality ? municipality.value !== '' : true @@ -114,7 +127,7 @@ export const DemandFields = ({ ) - return ( + const coreFields = ( <> {municipality ? (
@@ -172,4 +185,47 @@ export const DemandFields = ({ ) + + if (!responsibles) return coreFields + + const creatorValue: DemandResponsibleOption[] = responsibles.currentUser + ? [ + { + id: responsibles.currentUser.id, + name: responsibles.currentUser.name, + }, + ] + : [] + + const responsiblesField = ( + + {CAMPAIGN_DEMAND_RESPONSIBLES_LABEL} + + {CAMPAIGN_DEMAND_RESPONSIBLES_DESCRIPTION} + + ) + + if (responsibles.layout === 'aside') { + return ( +
+
{coreFields}
+ {responsiblesField} +
+ ) + } + + return ( + <> + {coreFields} + {responsiblesField} + + ) } diff --git a/src/components/campaign/demand/DemandForm.tsx b/src/components/campaign/demand/DemandForm.tsx index a7811038..bd7d10ba 100644 --- a/src/components/campaign/demand/DemandForm.tsx +++ b/src/components/campaign/demand/DemandForm.tsx @@ -2,6 +2,7 @@ import { useActionState, useCallback, useState } from 'react' +import { searchDemandResponsibleOptions } from '@/app/(campaign)/campanha/(app)/demandas/responsibleSearchActions' import { DemandFields, type DemandActivityValue } from '@/components/campaign/demand/DemandFields' import { CampaignFormActionMessage } from '@/components/campaign/shared/CampaignFormActionMessage' import type { RelationOption } from '@/components/campaign/shared/RelationMultiSelect' @@ -15,6 +16,7 @@ type DemandFormProps = { municipalityOptions: RelationOption[] initialActivity?: ActivityRelationOption | null initialMunicipalityId?: number + currentUser?: { id: number; name: string } | null searchActivities: ( query: string, municipalityId: number | null, @@ -29,6 +31,7 @@ export const DemandForm = ({ municipalityOptions, initialActivity = null, initialMunicipalityId, + currentUser = null, searchActivities, formAction, }: DemandFormProps) => { @@ -45,9 +48,13 @@ export const DemandForm = ({ (query: string) => searchActivities(query, parsedMunicipalityId), [parsedMunicipalityId, searchActivities], ) + const searchResponsibles = useCallback( + (query: string) => searchDemandResponsibleOptions(query, parsedMunicipalityId), + [parsedMunicipalityId], + ) return ( -
+ {state.status !== 'success' ? : null} + ) : null} + + + ))} + + ) : null} + + + + + + {loading ? ( +
+
+ ) : failed ? ( +

+ Não foi possível concluir a busca. +

+ ) : visibleOptions.length > 0 ? ( + + {visibleOptions.map((option) => ( + add(option)} + > + {option.name} + + ))} + + ) : ( +

+ {shortQuery + ? 'Sem assessores sugeridos para este município.' + : 'Nenhum resultado encontrado.'} +

+ )} +
+
+
+ + ) +} diff --git a/src/components/campaign/demand/DemandResponsiblesCard.tsx b/src/components/campaign/demand/DemandResponsiblesCard.tsx new file mode 100644 index 00000000..fecef7a6 --- /dev/null +++ b/src/components/campaign/demand/DemandResponsiblesCard.tsx @@ -0,0 +1,90 @@ +'use client' + +import { useActionState, useEffect, useState } from 'react' + +import { searchDemandResponsibleOptions } from '@/app/(campaign)/campanha/(app)/demandas/responsibleSearchActions' +import { CampaignFormActionMessage } from '@/components/campaign/shared/CampaignFormActionMessage' +import { Button } from '@/components/ui/button' +import { Spinner } from '@/components/ui/Spinner' +import { CAMPAIGN_DEMAND_RESPONSIBLES_LABEL } from '@/lib/schemas/campaignDemand' +import type { CampaignFormActionState } from '@/utilities/campaignFormActionError' +import { + DemandResponsibleMultiSelect, + type DemandResponsibleOption, +} from './DemandResponsibleMultiSelect' + +type DemandResponsiblesCardProps = { + demandId: number + municipalityId: number | null + /** The creator id, shown as "(criador)" on the chip (removal stays allowed — hand-off). */ + creatorUserId: number | null + initialResponsibles: DemandResponsibleOption[] + formAction: ( + state: CampaignFormActionState, + formData: FormData, + ) => Promise +} + +/** + * C143 — responsible management on the demand detail. Everyone who can read + * the demand can update it (same row scope), so the card's edit controls are + * available to whoever opens the page; the save submits the full list, the + * action revalidates the page and the local state keeps the saved chips + * (the success message is the confirmation, so no remount key is used). + */ +export const DemandResponsiblesCard = ({ + demandId, + municipalityId, + creatorUserId, + initialResponsibles, + formAction, +}: DemandResponsiblesCardProps) => { + const [state, submitAction, isPending] = useActionState(formAction, {}) + const [dirty, setDirty] = useState(false) + + useEffect(() => { + if (state.status === 'success') setDirty(false) + }, [state.status]) + + const search = (query: string) => searchDemandResponsibleOptions(query, municipalityId) + + return ( + +
+

{CAMPAIGN_DEMAND_RESPONSIBLES_LABEL}

+

Só quem é responsável vê esta demanda.

+
+ + setDirty(true)} + /> + +

+ O criador já entra como responsável automaticamente. Assessores do município aparecem como + sugestão, mas só quem for marcado enxerga. +

+ + + + + + + + ) +} diff --git a/src/components/campaign/shared/WizardRegisterDemandStep.tsx b/src/components/campaign/shared/WizardRegisterDemandStep.tsx index 3ea331f7..c08e5abc 100644 --- a/src/components/campaign/shared/WizardRegisterDemandStep.tsx +++ b/src/components/campaign/shared/WizardRegisterDemandStep.tsx @@ -5,6 +5,7 @@ import { useActionState, useCallback, useState } from 'react' import { createWizardDemandFormAction } from '@/app/(campaign)/campanha/(app)/acoes/formActions' import { searchDemandActivityOptions } from '@/app/(campaign)/campanha/(app)/demandas/activitySearchActions' +import { searchDemandResponsibleOptions } from '@/app/(campaign)/campanha/(app)/demandas/responsibleSearchActions' import { DemandFields, type DemandActivityValue } from '@/components/campaign/demand/DemandFields' import { CampaignFormActionMessage } from '@/components/campaign/shared/CampaignFormActionMessage' import { CampaignWizardShell } from '@/components/campaign/shared/CampaignWizardShell' @@ -21,6 +22,7 @@ type WizardRegisterDemandStepProps = { municipalityName: string municipalitySlug: string returnPath?: string + currentUser?: { id: number; name: string } | null } /** @@ -34,6 +36,7 @@ export const WizardRegisterDemandStep = ({ municipalityName, municipalitySlug, returnPath, + currentUser = null, }: WizardRegisterDemandStepProps) => { const router = useRouter() const [state, submitAction, isPending] = useActionState(createWizardDemandFormAction, {}) @@ -43,6 +46,10 @@ export const WizardRegisterDemandStep = ({ (query: string) => searchDemandActivityOptions(query, municipalityId), [municipalityId], ) + const searchResponsibles = useCallback( + (query: string) => searchDemandResponsibleOptions(query, municipalityId), + [municipalityId], + ) useCampaignFormSuccessToast(state, () => { recordLastActedMunicipality(municipalitySlug) @@ -79,6 +86,11 @@ export const WizardRegisterDemandStep = ({ activity={activity} onActivityChange={setActivity} searchActivities={searchActivities} + responsibles={{ + currentUser, + search: searchResponsibles, + layout: 'stacked', + }} /> {state.status !== 'success' ? : null} diff --git a/src/globals/SocialFeedSettings.ts b/src/globals/SocialFeedSettings.ts index f2326558..663c52fa 100644 --- a/src/globals/SocialFeedSettings.ts +++ b/src/globals/SocialFeedSettings.ts @@ -1,7 +1,12 @@ import { payloadAdminOnly } from '@/utilities/campaignAccess' import { REVALIDATE_SOCIAL_FEED_TAG } from '@/utilities/revalidateRequest' +import { + INSTAGRAM_SYNC_TIMEOUT_MS, + instagramCredentialsChanged, + syncInstagramFeed, +} from '@/utilities/socialFeed/instagramSync' import { revalidateTag } from 'next/cache' -import type { GlobalConfig } from 'payload' +import type { GlobalAfterChangeHook, GlobalConfig } from 'payload' const slug = 'social-feed-settings' @@ -9,6 +14,33 @@ const YOUTUBE_VIDEO_ID_PATTERN = /^[A-Za-z0-9_-]{11}$/ const revalidateFeed = async () => revalidateTag(REVALIDATE_SOCIAL_FEED_TAG) +/** + * Re-syncs Instagram when a save changes the IG credentials (the primary + * flow: the assessoria fills token + ID, saves, and the reload of the global + * already shows the sync state WITH the new credentials — the status panel + * would otherwise keep showing the last failure of the old token). Awaited + * with a bounded deadline so a hanging Graph API never stalls the save; any + * failure is swallowed — the sync's own status IS the result, and a broken + * sync must never break the save. + * + * The sync modules must not import `@payload-config` (they do not), so the + * static import here closes no cycle: `payload.config → this global → + * instagramSync → instagramFeed` ends at `instagramFeed`, which is pure. + */ +const syncInstagramAfterChange: GlobalAfterChangeHook = async ({ doc, previousDoc, req }) => { + if (!instagramCredentialsChanged(doc, previousDoc)) return + try { + // `req` lets the sync write inside this very transaction (same row lock + // held by the save) instead of blocking a second pool connection. + await syncInstagramFeed(req.payload, { + signal: AbortSignal.timeout(INSTAGRAM_SYNC_TIMEOUT_MS), + req, + }) + } catch { + // the status panel reflects the failure; the save itself stays green + } +} + /** * Configuration of the campaign home content board's external feeds (plano-site * §4.2): YouTube now, Instagram in S3. The board's read path is the public home @@ -29,7 +61,7 @@ export const SocialFeedSettings: GlobalConfig = { update: payloadAdminOnly, }, hooks: { - afterChange: [revalidateFeed], + afterChange: [revalidateFeed, syncInstagramAfterChange], }, fields: [ { @@ -88,6 +120,16 @@ export const SocialFeedSettings: GlobalConfig = { description: 'Desliga só os cards de post do Instagram.', }, }, + { + name: 'instagramSyncStatusPanel', + type: 'ui', + label: 'Instagram — estado da sincronização', + admin: { + components: { + Field: './components/admin/InstagramSyncStatusPanel#InstagramSyncStatusPanel', + }, + }, + }, { name: 'instagramAccessToken', type: 'text', @@ -185,5 +227,12 @@ export const SocialFeedSettings: GlobalConfig = { hidden: true, }, }, + { + name: 'instagramSyncStatus', + type: 'json', + admin: { + hidden: true, + }, + }, ], } diff --git a/src/lib/schemas/campaignDemand.ts b/src/lib/schemas/campaignDemand.ts index 2347ed0d..84579703 100644 --- a/src/lib/schemas/campaignDemand.ts +++ b/src/lib/schemas/campaignDemand.ts @@ -93,6 +93,11 @@ export const CAMPAIGN_DEMAND_COST_STAFF_MESSAGE = 'Somente a coordenação, a assessoria e o candidato registram custos.' export const CAMPAIGN_DEMAND_EDIT_STAFF_MESSAGE = 'Somente a coordenação, a assessoria e o candidato editam demandas.' +export const CAMPAIGN_DEMAND_RESPONSIBLES_STAFF_MESSAGE = + 'Somente quem vê a demanda pode gerenciar os responsáveis.' +export const CAMPAIGN_DEMAND_RESPONSIBLES_LABEL = 'Responsáveis pela demanda' +export const CAMPAIGN_DEMAND_RESPONSIBLES_DESCRIPTION = + 'Sugestões: assessores do município. Se ninguém for marcado além de você, só você, candidato e coordenador verão a demanda.' export const CAMPAIGN_DEMAND_RECEIPT_STAFF_MESSAGE = 'Somente a coordenação e a assessoria anexam comprovantes.' export const CAMPAIGN_DEMAND_RECEIPT_TYPE_MESSAGE = 'Envie uma imagem (JPEG, PNG, WebP) ou PDF.' diff --git a/src/lib/schemas/campaignDemandInput.ts b/src/lib/schemas/campaignDemandInput.ts index 9f361b99..a6f50232 100644 --- a/src/lib/schemas/campaignDemandInput.ts +++ b/src/lib/schemas/campaignDemandInput.ts @@ -18,6 +18,8 @@ export const campaignDemandCreateSchema = z.object({ activity: positiveRelationshipId.optional(), /** Staff may record on behalf of a leadership; leaders get their own. */ leadership: positiveRelationshipId.optional(), + /** C143 — explicit responsibles; the creator is unioned in by the collection hook. */ + responsibles: z.array(positiveRelationshipId).default([]), }) export const campaignDemandUpdateSchema = z.object({ @@ -36,7 +38,14 @@ export const campaignDemandCostSchema = z.object({ cost: z.number().min(0).max(100_000_000).nullable(), }) +/** C143 — full-replace of the responsible list (advisory-locked by the action). */ +export const campaignDemandResponsiblesSchema = z.object({ + id: positiveRelationshipId, + responsibles: z.array(positiveRelationshipId).default([]), +}) + export type CampaignDemandCreateInput = z.input export type CampaignDemandUpdateInput = z.input export type CampaignDemandTransitionInput = z.input export type CampaignDemandCostInput = z.input +export type CampaignDemandResponsiblesInput = z.input diff --git a/src/migrations/20260819_233503_add_campaign_demand_responsibles.json b/src/migrations/20260819_233503_add_campaign_demand_responsibles.json new file mode 100644 index 00000000..c2bdfc7f --- /dev/null +++ b/src/migrations/20260819_233503_add_campaign_demand_responsibles.json @@ -0,0 +1,13783 @@ +{ + "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'" + }, + "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": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + }, + "rows": { + "name": "rows", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "supporter_import_batch_batch_id_idx": { + "name": "supporter_import_batch_batch_id_idx", + "columns": [ + { + "expression": "batch_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "supporter_import_batch_actor_idx": { + "name": "supporter_import_batch_actor_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "supporter_import_batch_expires_at_idx": { + "name": "supporter_import_batch_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "supporter_import_batch_updated_at_idx": { + "name": "supporter_import_batch_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "supporter_import_batch_created_at_idx": { + "name": "supporter_import_batch_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "supporter_import_batch_actor_id_campaign_user_id_fk": { + "name": "supporter_import_batch_actor_id_campaign_user_id_fk", + "tableFrom": "supporter_import_batch", + "tableTo": "campaign_user", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.municipality_update": { + "name": "municipality_update", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "municipality_id": { + "name": "municipality_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "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 + }, + "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_updated_at_idx": { + "name": "municipality_update_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "municipality_update_created_at_idx": { + "name": "municipality_update_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "municipality_update_municipality_id_municipality_id_fk": { + "name": "municipality_update_municipality_id_municipality_id_fk", + "tableFrom": "municipality_update", + "tableTo": "municipality", + "columnsFrom": [ + "municipality_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "municipality_update_author_id_campaign_user_id_fk": { + "name": "municipality_update_author_id_campaign_user_id_fk", + "tableFrom": "municipality_update", + "tableTo": "campaign_user", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.activity_tasks": { + "name": "activity_tasks", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "responsible_id": { + "name": "responsible_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "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": true + }, + "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 + }, + "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_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_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.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 + }, + "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": true + }, + "city": { + "name": "city", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "postal_code": { + "name": "postal_code", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "contact_updated_at_idx": { + "name": "contact_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "contact_created_at_idx": { + "name": "contact_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.consent": { + "name": "consent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "text": { + "name": "text", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "consent_key_idx": { + "name": "consent_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "consent_updated_at_idx": { + "name": "consent_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "consent_created_at_idx": { + "name": "consent_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.signature": { + "name": "signature", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "petition_id": { + "name": "petition_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "consent_id": { + "name": "consent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "comment": { + "name": "comment", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "signature_contact_idx": { + "name": "signature_contact_idx", + "columns": [ + { + "expression": "contact_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "signature_petition_idx": { + "name": "signature_petition_idx", + "columns": [ + { + "expression": "petition_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "signature_consent_idx": { + "name": "signature_consent_idx", + "columns": [ + { + "expression": "consent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "signature_updated_at_idx": { + "name": "signature_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "signature_created_at_idx": { + "name": "signature_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "signature_contact_id_contact_id_fk": { + "name": "signature_contact_id_contact_id_fk", + "tableFrom": "signature", + "tableTo": "contact", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "signature_petition_id_petition_id_fk": { + "name": "signature_petition_id_petition_id_fk", + "tableFrom": "signature", + "tableTo": "petition", + "columnsFrom": [ + "petition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "signature_consent_id_consent_id_fk": { + "name": "signature_consent_id_consent_id_fk", + "tableFrom": "signature", + "tableTo": "consent", + "columnsFrom": [ + "consent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "consent_id": { + "name": "consent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "comment": { + "name": "comment", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "subscription_contact_idx": { + "name": "subscription_contact_idx", + "columns": [ + { + "expression": "contact_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "subscription_consent_idx": { + "name": "subscription_consent_idx", + "columns": [ + { + "expression": "consent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "subscription_updated_at_idx": { + "name": "subscription_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "subscription_created_at_idx": { + "name": "subscription_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "subscription_contact_id_contact_id_fk": { + "name": "subscription_contact_id_contact_id_fk", + "tableFrom": "subscription", + "tableTo": "contact", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "subscription_consent_id_consent_id_fk": { + "name": "subscription_consent_id_consent_id_fk", + "tableFrom": "subscription", + "tableTo": "consent", + "columnsFrom": [ + "consent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.post": { + "name": "post", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "enum_post_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "subtitle": { + "name": "subtitle", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "cover_image_id": { + "name": "cover_image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "published_date": { + "name": "published_date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "_status": { + "name": "_status", + "type": "enum_post_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'draft'" + } + }, + "indexes": { + "post_slug_idx": { + "name": "post_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "post_category_idx": { + "name": "post_category_idx", + "columns": [ + { + "expression": "category_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "post_cover_image_idx": { + "name": "post_cover_image_idx", + "columns": [ + { + "expression": "cover_image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "post_updated_at_idx": { + "name": "post_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "post_created_at_idx": { + "name": "post_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "post__status_idx": { + "name": "post__status_idx", + "columns": [ + { + "expression": "_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "post_category_id_tag_id_fk": { + "name": "post_category_id_tag_id_fk", + "tableFrom": "post", + "tableTo": "tag", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "post_cover_image_id_media_id_fk": { + "name": "post_cover_image_id_media_id_fk", + "tableFrom": "post", + "tableTo": "media", + "columnsFrom": [ + "cover_image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.post_rels": { + "name": "post_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "post_rels_order_idx": { + "name": "post_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "post_rels_parent_idx": { + "name": "post_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "post_rels_path_idx": { + "name": "post_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "post_rels_tag_id_idx": { + "name": "post_rels_tag_id_idx", + "columns": [ + { + "expression": "tag_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "post_rels_parent_fk": { + "name": "post_rels_parent_fk", + "tableFrom": "post_rels", + "tableTo": "post", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "post_rels_tag_fk": { + "name": "post_rels_tag_fk", + "tableFrom": "post_rels", + "tableTo": "tag", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._post_v": { + "name": "_post_v", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_title": { + "name": "version_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_slug": { + "name": "version_slug", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_type": { + "name": "version_type", + "type": "enum__post_v_version_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "version_category_id": { + "name": "version_category_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_subtitle": { + "name": "version_subtitle", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_cover_image_id": { + "name": "version_cover_image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_published_date": { + "name": "version_published_date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_body": { + "name": "version_body", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "version_updated_at": { + "name": "version_updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_created_at": { + "name": "version_created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version__status": { + "name": "version__status", + "type": "enum__post_v_version_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'draft'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "latest": { + "name": "latest", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "_post_v_parent_idx": { + "name": "_post_v_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_post_v_version_version_slug_idx": { + "name": "_post_v_version_version_slug_idx", + "columns": [ + { + "expression": "version_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_post_v_version_version_category_idx": { + "name": "_post_v_version_version_category_idx", + "columns": [ + { + "expression": "version_category_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_post_v_version_version_cover_image_idx": { + "name": "_post_v_version_version_cover_image_idx", + "columns": [ + { + "expression": "version_cover_image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_post_v_version_version_updated_at_idx": { + "name": "_post_v_version_version_updated_at_idx", + "columns": [ + { + "expression": "version_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_post_v_version_version_created_at_idx": { + "name": "_post_v_version_version_created_at_idx", + "columns": [ + { + "expression": "version_created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_post_v_version_version__status_idx": { + "name": "_post_v_version_version__status_idx", + "columns": [ + { + "expression": "version__status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_post_v_created_at_idx": { + "name": "_post_v_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_post_v_updated_at_idx": { + "name": "_post_v_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_post_v_latest_idx": { + "name": "_post_v_latest_idx", + "columns": [ + { + "expression": "latest", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_post_v_parent_id_post_id_fk": { + "name": "_post_v_parent_id_post_id_fk", + "tableFrom": "_post_v", + "tableTo": "post", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "_post_v_version_category_id_tag_id_fk": { + "name": "_post_v_version_category_id_tag_id_fk", + "tableFrom": "_post_v", + "tableTo": "tag", + "columnsFrom": [ + "version_category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "_post_v_version_cover_image_id_media_id_fk": { + "name": "_post_v_version_cover_image_id_media_id_fk", + "tableFrom": "_post_v", + "tableTo": "media", + "columnsFrom": [ + "version_cover_image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._post_v_rels": { + "name": "_post_v_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "_post_v_rels_order_idx": { + "name": "_post_v_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_post_v_rels_parent_idx": { + "name": "_post_v_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_post_v_rels_path_idx": { + "name": "_post_v_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_post_v_rels_tag_id_idx": { + "name": "_post_v_rels_tag_id_idx", + "columns": [ + { + "expression": "tag_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_post_v_rels_parent_fk": { + "name": "_post_v_rels_parent_fk", + "tableFrom": "_post_v_rels", + "tableTo": "_post_v", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "_post_v_rels_tag_fk": { + "name": "_post_v_rels_tag_fk", + "tableFrom": "_post_v_rels", + "tableTo": "tag", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tag": { + "name": "tag", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "hidden": { + "name": "hidden", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tag_slug_idx": { + "name": "tag_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tag_updated_at_idx": { + "name": "tag_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tag_created_at_idx": { + "name": "tag_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.exports": { + "name": "exports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "format": { + "name": "format", + "type": "enum_exports_format", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'csv'" + }, + "limit": { + "name": "limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "page": { + "name": "page", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "sort": { + "name": "sort", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "enum_exports_sort_order", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "drafts": { + "name": "drafts", + "type": "enum_exports_drafts", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'yes'" + }, + "collection_slug": { + "name": "collection_slug", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'signature'" + }, + "where": { + "name": "where", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "thumbnail_u_r_l": { + "name": "thumbnail_u_r_l", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "filename": { + "name": "filename", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "filesize": { + "name": "filesize", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "focal_x": { + "name": "focal_x", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "focal_y": { + "name": "focal_y", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "exports_updated_at_idx": { + "name": "exports_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "exports_created_at_idx": { + "name": "exports_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "exports_filename_idx": { + "name": "exports_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.exports_texts": { + "name": "exports_texts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "exports_texts_order_parent": { + "name": "exports_texts_order_parent", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "exports_texts_parent_fk": { + "name": "exports_texts_parent_fk", + "tableFrom": "exports_texts", + "tableTo": "exports", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.imports": { + "name": "imports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "collection_slug": { + "name": "collection_slug", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "import_mode": { + "name": "import_mode", + "type": "enum_imports_import_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "match_field": { + "name": "match_field", + "type": "varchar", + "primaryKey": false, + "notNull": false, + "default": "'id'" + }, + "status": { + "name": "status", + "type": "enum_imports_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'pending'" + }, + "summary_imported": { + "name": "summary_imported", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "summary_updated": { + "name": "summary_updated", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "summary_total": { + "name": "summary_total", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "summary_issues": { + "name": "summary_issues", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "summary_issue_details": { + "name": "summary_issue_details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "thumbnail_u_r_l": { + "name": "thumbnail_u_r_l", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "filename": { + "name": "filename", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "filesize": { + "name": "filesize", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "focal_x": { + "name": "focal_x", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "focal_y": { + "name": "focal_y", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "imports_updated_at_idx": { + "name": "imports_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "imports_created_at_idx": { + "name": "imports_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "imports_filename_idx": { + "name": "imports_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_kv": { + "name": "payload_kv", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "payload_kv_key_idx": { + "name": "payload_kv_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_jobs_log": { + "name": "payload_jobs_log", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "executed_at": { + "name": "executed_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + }, + "task_slug": { + "name": "task_slug", + "type": "enum_payload_jobs_log_task_slug", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "task_i_d": { + "name": "task_i_d", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "input": { + "name": "input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "enum_payload_jobs_log_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "payload_jobs_log_order_idx": { + "name": "payload_jobs_log_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_log_parent_id_idx": { + "name": "payload_jobs_log_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payload_jobs_log_parent_id_fk": { + "name": "payload_jobs_log_parent_id_fk", + "tableFrom": "payload_jobs_log", + "tableTo": "payload_jobs", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_jobs": { + "name": "payload_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "input": { + "name": "input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "total_tried": { + "name": "total_tried", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "has_error": { + "name": "has_error", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "error": { + "name": "error", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "task_slug": { + "name": "task_slug", + "type": "enum_payload_jobs_task_slug", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "queue": { + "name": "queue", + "type": "varchar", + "primaryKey": false, + "notNull": false, + "default": "'default'" + }, + "wait_until": { + "name": "wait_until", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "processing": { + "name": "processing", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payload_jobs_completed_at_idx": { + "name": "payload_jobs_completed_at_idx", + "columns": [ + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_total_tried_idx": { + "name": "payload_jobs_total_tried_idx", + "columns": [ + { + "expression": "total_tried", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_has_error_idx": { + "name": "payload_jobs_has_error_idx", + "columns": [ + { + "expression": "has_error", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_task_slug_idx": { + "name": "payload_jobs_task_slug_idx", + "columns": [ + { + "expression": "task_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_queue_idx": { + "name": "payload_jobs_queue_idx", + "columns": [ + { + "expression": "queue", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_wait_until_idx": { + "name": "payload_jobs_wait_until_idx", + "columns": [ + { + "expression": "wait_until", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_processing_idx": { + "name": "payload_jobs_processing_idx", + "columns": [ + { + "expression": "processing", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_updated_at_idx": { + "name": "payload_jobs_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_created_at_idx": { + "name": "payload_jobs_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_locked_documents": { + "name": "payload_locked_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "global_slug": { + "name": "global_slug", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payload_locked_documents_global_slug_idx": { + "name": "payload_locked_documents_global_slug_idx", + "columns": [ + { + "expression": "global_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_updated_at_idx": { + "name": "payload_locked_documents_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_created_at_idx": { + "name": "payload_locked_documents_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_locked_documents_rels": { + "name": "payload_locked_documents_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "users_id": { + "name": "users_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "campaign_user_id": { + "name": "campaign_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "campaign_web_authn_credential_id": { + "name": "campaign_web_authn_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "notification_id": { + "name": "notification_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "push_subscription_id": { + "name": "push_subscription_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "campaign_invite_id": { + "name": "campaign_invite_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "campaign_vote_summary_snapshot_id": { + "name": "campaign_vote_summary_snapshot_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "municipality_id": { + "name": "municipality_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "leadership_id": { + "name": "leadership_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "state_deputy_id": { + "name": "state_deputy_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vote_pledge_id": { + "name": "vote_pledge_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "allocation_decision_id": { + "name": "allocation_decision_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "campaign_demand_id": { + "name": "campaign_demand_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "supporter_id": { + "name": "supporter_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "supporter_import_batch_id": { + "name": "supporter_import_batch_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "municipality_update_id": { + "name": "municipality_update_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "activity_id": { + "name": "activity_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "calendar_feed_id": { + "name": "calendar_feed_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "google_calendar_sync_id": { + "name": "google_calendar_sync_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "election_tally_id": { + "name": "election_tally_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "election_candidate_vote_id": { + "name": "election_candidate_vote_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "election_candidate_id": { + "name": "election_candidate_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "media_id": { + "name": "media_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "petition_id": { + "name": "petition_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "contact_id": { + "name": "contact_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "consent_id": { + "name": "consent_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "signature_id": { + "name": "signature_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "subscription_id": { + "name": "subscription_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "post_id": { + "name": "post_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tag_id": { + "name": "tag_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "payload_locked_documents_rels_order_idx": { + "name": "payload_locked_documents_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_parent_idx": { + "name": "payload_locked_documents_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_path_idx": { + "name": "payload_locked_documents_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_users_id_idx": { + "name": "payload_locked_documents_rels_users_id_idx", + "columns": [ + { + "expression": "users_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_campaign_user_id_idx": { + "name": "payload_locked_documents_rels_campaign_user_id_idx", + "columns": [ + { + "expression": "campaign_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_campaign_web_authn_credent_idx": { + "name": "payload_locked_documents_rels_campaign_web_authn_credent_idx", + "columns": [ + { + "expression": "campaign_web_authn_credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_notification_id_idx": { + "name": "payload_locked_documents_rels_notification_id_idx", + "columns": [ + { + "expression": "notification_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_push_subscription_id_idx": { + "name": "payload_locked_documents_rels_push_subscription_id_idx", + "columns": [ + { + "expression": "push_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_campaign_invite_id_idx": { + "name": "payload_locked_documents_rels_campaign_invite_id_idx", + "columns": [ + { + "expression": "campaign_invite_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_campaign_vote_summary_snap_idx": { + "name": "payload_locked_documents_rels_campaign_vote_summary_snap_idx", + "columns": [ + { + "expression": "campaign_vote_summary_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_municipality_id_idx": { + "name": "payload_locked_documents_rels_municipality_id_idx", + "columns": [ + { + "expression": "municipality_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_leadership_id_idx": { + "name": "payload_locked_documents_rels_leadership_id_idx", + "columns": [ + { + "expression": "leadership_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_organization_id_idx": { + "name": "payload_locked_documents_rels_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_state_deputy_id_idx": { + "name": "payload_locked_documents_rels_state_deputy_id_idx", + "columns": [ + { + "expression": "state_deputy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_vote_pledge_id_idx": { + "name": "payload_locked_documents_rels_vote_pledge_id_idx", + "columns": [ + { + "expression": "vote_pledge_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_allocation_decision_id_idx": { + "name": "payload_locked_documents_rels_allocation_decision_id_idx", + "columns": [ + { + "expression": "allocation_decision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_campaign_demand_id_idx": { + "name": "payload_locked_documents_rels_campaign_demand_id_idx", + "columns": [ + { + "expression": "campaign_demand_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_supporter_id_idx": { + "name": "payload_locked_documents_rels_supporter_id_idx", + "columns": [ + { + "expression": "supporter_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_supporter_import_batch_id_idx": { + "name": "payload_locked_documents_rels_supporter_import_batch_id_idx", + "columns": [ + { + "expression": "supporter_import_batch_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_municipality_update_id_idx": { + "name": "payload_locked_documents_rels_municipality_update_id_idx", + "columns": [ + { + "expression": "municipality_update_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_activity_id_idx": { + "name": "payload_locked_documents_rels_activity_id_idx", + "columns": [ + { + "expression": "activity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_calendar_feed_id_idx": { + "name": "payload_locked_documents_rels_calendar_feed_id_idx", + "columns": [ + { + "expression": "calendar_feed_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_google_calendar_sync_id_idx": { + "name": "payload_locked_documents_rels_google_calendar_sync_id_idx", + "columns": [ + { + "expression": "google_calendar_sync_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_election_tally_id_idx": { + "name": "payload_locked_documents_rels_election_tally_id_idx", + "columns": [ + { + "expression": "election_tally_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_election_candidate_vote_id_idx": { + "name": "payload_locked_documents_rels_election_candidate_vote_id_idx", + "columns": [ + { + "expression": "election_candidate_vote_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_election_candidate_id_idx": { + "name": "payload_locked_documents_rels_election_candidate_id_idx", + "columns": [ + { + "expression": "election_candidate_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_media_id_idx": { + "name": "payload_locked_documents_rels_media_id_idx", + "columns": [ + { + "expression": "media_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_petition_id_idx": { + "name": "payload_locked_documents_rels_petition_id_idx", + "columns": [ + { + "expression": "petition_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_contact_id_idx": { + "name": "payload_locked_documents_rels_contact_id_idx", + "columns": [ + { + "expression": "contact_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_consent_id_idx": { + "name": "payload_locked_documents_rels_consent_id_idx", + "columns": [ + { + "expression": "consent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_signature_id_idx": { + "name": "payload_locked_documents_rels_signature_id_idx", + "columns": [ + { + "expression": "signature_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_subscription_id_idx": { + "name": "payload_locked_documents_rels_subscription_id_idx", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_post_id_idx": { + "name": "payload_locked_documents_rels_post_id_idx", + "columns": [ + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_tag_id_idx": { + "name": "payload_locked_documents_rels_tag_id_idx", + "columns": [ + { + "expression": "tag_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payload_locked_documents_rels_parent_fk": { + "name": "payload_locked_documents_rels_parent_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "payload_locked_documents", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_users_fk": { + "name": "payload_locked_documents_rels_users_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "users", + "columnsFrom": [ + "users_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_campaign_user_fk": { + "name": "payload_locked_documents_rels_campaign_user_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "campaign_user", + "columnsFrom": [ + "campaign_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_campaign_web_authn_credenti_fk": { + "name": "payload_locked_documents_rels_campaign_web_authn_credenti_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "campaign_web_authn_credential", + "columnsFrom": [ + "campaign_web_authn_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_notification_fk": { + "name": "payload_locked_documents_rels_notification_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "notification", + "columnsFrom": [ + "notification_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_push_subscription_fk": { + "name": "payload_locked_documents_rels_push_subscription_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "push_subscription", + "columnsFrom": [ + "push_subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_campaign_invite_fk": { + "name": "payload_locked_documents_rels_campaign_invite_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "campaign_invite", + "columnsFrom": [ + "campaign_invite_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_campaign_vote_summary_snaps_fk": { + "name": "payload_locked_documents_rels_campaign_vote_summary_snaps_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "campaign_vote_summary_snapshot", + "columnsFrom": [ + "campaign_vote_summary_snapshot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_municipality_fk": { + "name": "payload_locked_documents_rels_municipality_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "municipality", + "columnsFrom": [ + "municipality_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_leadership_fk": { + "name": "payload_locked_documents_rels_leadership_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "leadership", + "columnsFrom": [ + "leadership_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_organization_fk": { + "name": "payload_locked_documents_rels_organization_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_state_deputy_fk": { + "name": "payload_locked_documents_rels_state_deputy_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "state_deputy", + "columnsFrom": [ + "state_deputy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_vote_pledge_fk": { + "name": "payload_locked_documents_rels_vote_pledge_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "vote_pledge", + "columnsFrom": [ + "vote_pledge_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_allocation_decision_fk": { + "name": "payload_locked_documents_rels_allocation_decision_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "allocation_decision", + "columnsFrom": [ + "allocation_decision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_campaign_demand_fk": { + "name": "payload_locked_documents_rels_campaign_demand_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "campaign_demand", + "columnsFrom": [ + "campaign_demand_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_supporter_fk": { + "name": "payload_locked_documents_rels_supporter_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "supporter", + "columnsFrom": [ + "supporter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_supporter_import_batch_fk": { + "name": "payload_locked_documents_rels_supporter_import_batch_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "supporter_import_batch", + "columnsFrom": [ + "supporter_import_batch_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_municipality_update_fk": { + "name": "payload_locked_documents_rels_municipality_update_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "municipality_update", + "columnsFrom": [ + "municipality_update_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_activity_fk": { + "name": "payload_locked_documents_rels_activity_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "activity", + "columnsFrom": [ + "activity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_calendar_feed_fk": { + "name": "payload_locked_documents_rels_calendar_feed_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "calendar_feed", + "columnsFrom": [ + "calendar_feed_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_google_calendar_sync_fk": { + "name": "payload_locked_documents_rels_google_calendar_sync_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "google_calendar_sync", + "columnsFrom": [ + "google_calendar_sync_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_election_tally_fk": { + "name": "payload_locked_documents_rels_election_tally_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "election_tally", + "columnsFrom": [ + "election_tally_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_election_candidate_vote_fk": { + "name": "payload_locked_documents_rels_election_candidate_vote_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "election_candidate_vote", + "columnsFrom": [ + "election_candidate_vote_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_election_candidate_fk": { + "name": "payload_locked_documents_rels_election_candidate_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "election_candidate", + "columnsFrom": [ + "election_candidate_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_media_fk": { + "name": "payload_locked_documents_rels_media_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "media", + "columnsFrom": [ + "media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_petition_fk": { + "name": "payload_locked_documents_rels_petition_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "petition", + "columnsFrom": [ + "petition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_contact_fk": { + "name": "payload_locked_documents_rels_contact_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "contact", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_consent_fk": { + "name": "payload_locked_documents_rels_consent_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "consent", + "columnsFrom": [ + "consent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_signature_fk": { + "name": "payload_locked_documents_rels_signature_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "signature", + "columnsFrom": [ + "signature_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_subscription_fk": { + "name": "payload_locked_documents_rels_subscription_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "subscription", + "columnsFrom": [ + "subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_post_fk": { + "name": "payload_locked_documents_rels_post_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "post", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_tag_fk": { + "name": "payload_locked_documents_rels_tag_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "tag", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_preferences": { + "name": "payload_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payload_preferences_key_idx": { + "name": "payload_preferences_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_updated_at_idx": { + "name": "payload_preferences_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_created_at_idx": { + "name": "payload_preferences_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_preferences_rels": { + "name": "payload_preferences_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "users_id": { + "name": "users_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "campaign_user_id": { + "name": "campaign_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "payload_preferences_rels_order_idx": { + "name": "payload_preferences_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_rels_parent_idx": { + "name": "payload_preferences_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_rels_path_idx": { + "name": "payload_preferences_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_rels_users_id_idx": { + "name": "payload_preferences_rels_users_id_idx", + "columns": [ + { + "expression": "users_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_rels_campaign_user_id_idx": { + "name": "payload_preferences_rels_campaign_user_id_idx", + "columns": [ + { + "expression": "campaign_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payload_preferences_rels_parent_fk": { + "name": "payload_preferences_rels_parent_fk", + "tableFrom": "payload_preferences_rels", + "tableTo": "payload_preferences", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_preferences_rels_users_fk": { + "name": "payload_preferences_rels_users_fk", + "tableFrom": "payload_preferences_rels", + "tableTo": "users", + "columnsFrom": [ + "users_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_preferences_rels_campaign_user_fk": { + "name": "payload_preferences_rels_campaign_user_fk", + "tableFrom": "payload_preferences_rels", + "tableTo": "campaign_user", + "columnsFrom": [ + "campaign_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_migrations": { + "name": "payload_migrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "batch": { + "name": "batch", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payload_migrations_updated_at_idx": { + "name": "payload_migrations_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_migrations_created_at_idx": { + "name": "payload_migrations_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.site_settings_social_links": { + "name": "site_settings_social_links", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "enum_site_settings_social_links_platform", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "site_settings_social_links_order_idx": { + "name": "site_settings_social_links_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "site_settings_social_links_parent_id_idx": { + "name": "site_settings_social_links_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "site_settings_social_links_parent_id_fk": { + "name": "site_settings_social_links_parent_id_fk", + "tableFrom": "site_settings_social_links", + "tableTo": "site_settings", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.site_settings": { + "name": "site_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "header_title": { + "name": "header_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "_status": { + "name": "_status", + "type": "enum_site_settings_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'draft'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "site_settings__status_idx": { + "name": "site_settings__status_idx", + "columns": [ + { + "expression": "_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._site_settings_v_version_social_links": { + "name": "_site_settings_v_version_social_links", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "enum__site_settings_v_version_social_links_platform", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "_uuid": { + "name": "_uuid", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "_site_settings_v_version_social_links_order_idx": { + "name": "_site_settings_v_version_social_links_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_site_settings_v_version_social_links_parent_id_idx": { + "name": "_site_settings_v_version_social_links_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_site_settings_v_version_social_links_parent_id_fk": { + "name": "_site_settings_v_version_social_links_parent_id_fk", + "tableFrom": "_site_settings_v_version_social_links", + "tableTo": "_site_settings_v", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._site_settings_v": { + "name": "_site_settings_v", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "version_header_title": { + "name": "version_header_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version__status": { + "name": "version__status", + "type": "enum__site_settings_v_version_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'draft'" + }, + "version_updated_at": { + "name": "version_updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_created_at": { + "name": "version_created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "latest": { + "name": "latest", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "autosave": { + "name": "autosave", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "_site_settings_v_version_version__status_idx": { + "name": "_site_settings_v_version_version__status_idx", + "columns": [ + { + "expression": "version__status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_site_settings_v_created_at_idx": { + "name": "_site_settings_v_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_site_settings_v_updated_at_idx": { + "name": "_site_settings_v_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_site_settings_v_latest_idx": { + "name": "_site_settings_v_latest_idx", + "columns": [ + { + "expression": "latest", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_site_settings_v_autosave_idx": { + "name": "_site_settings_v_autosave_idx", + "columns": [ + { + "expression": "autosave", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.home": { + "name": "home", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "image_id": { + "name": "image_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "home_image_idx": { + "name": "home_image_idx", + "columns": [ + { + "expression": "image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "home_image_id_media_id_fk": { + "name": "home_image_id_media_id_fk", + "tableFrom": "home", + "tableTo": "media", + "columnsFrom": [ + "image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.metadata_keywords": { + "name": "metadata_keywords", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "keyword": { + "name": "keyword", + "type": "varchar", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "metadata_keywords_order_idx": { + "name": "metadata_keywords_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "metadata_keywords_parent_id_idx": { + "name": "metadata_keywords_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "metadata_keywords_parent_id_fk": { + "name": "metadata_keywords_parent_id_fk", + "tableFrom": "metadata_keywords", + "tableTo": "metadata", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.metadata": { + "name": "metadata", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "open_graph_site_name": { + "name": "open_graph_site_name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "open_graph_description": { + "name": "open_graph_description", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "twitter_creator": { + "name": "twitter_creator", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "twitter_description": { + "name": "twitter_description", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "image_id": { + "name": "image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "metadata_image_idx": { + "name": "metadata_image_idx", + "columns": [ + { + "expression": "image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "metadata_image_id_media_id_fk": { + "name": "metadata_image_id_media_id_fk", + "tableFrom": "metadata", + "tableTo": "media", + "columnsFrom": [ + "image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.privacy_policy": { + "name": "privacy_policy", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "published": { + "name": "published", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "body": { + "name": "body", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.campaign_goals": { + "name": "campaign_goals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "state_goal": { + "name": "state_goal", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 150000 + }, + "margin": { + "name": "margin", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "base_year": { + "name": "base_year", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "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 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.enum_users_roles": { + "name": "enum_users_roles", + "schema": "public", + "values": [ + "admin", + "editor" + ] + }, + "public.enum_campaign_user_role": { + "name": "enum_campaign_user_role", + "schema": "public", + "values": [ + "coordinator", + "advisor", + "candidate", + "leader" + ] + }, + "public.enum_notification_type": { + "name": "enum_notification_type", + "schema": "public", + "values": [ + "municipality_update", + "new_supporter", + "activity_attention", + "invite_accepted" + ] + }, + "public.enum_campaign_invite_kind": { + "name": "enum_campaign_invite_kind", + "schema": "public", + "values": [ + "login", + "autopreenchimento" + ] + }, + "public.enum_municipality_kind": { + "name": "enum_municipality_kind", + "schema": "public", + "values": [ + "municipio", + "zona" + ] + }, + "public.enum_municipality_priority": { + "name": "enum_municipality_priority", + "schema": "public", + "values": [ + "alta", + "normal" + ] + }, + "public.enum_municipality_engagement_level": { + "name": "enum_municipality_engagement_level", + "schema": "public", + "values": [ + "n0", + "n1", + "n2", + "n3", + "n4" + ] + }, + "public.enum_municipality_political_trend_status": { + "name": "enum_municipality_political_trend_status", + "schema": "public", + "values": [ + "favoravel", + "neutra", + "desfavoravel" + ] + }, + "public.enum_leadership_support_status": { + "name": "enum_leadership_support_status", + "schema": "public", + "values": [ + "engajado", + "a_abordar", + "em_disputa", + "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_election_tally_office": { + "name": "enum_election_tally_office", + "schema": "public", + "values": [ + "presidente", + "governador", + "deputado_federal", + "deputado_estadual" + ] + }, + "public.enum_election_tally_turn": { + "name": "enum_election_tally_turn", + "schema": "public", + "values": [ + "1", + "2" + ] + }, + "public.enum_election_candidate_vote_office": { + "name": "enum_election_candidate_vote_office", + "schema": "public", + "values": [ + "presidente", + "governador", + "deputado_federal", + "deputado_estadual" + ] + }, + "public.enum_election_candidate_vote_turn": { + "name": "enum_election_candidate_vote_turn", + "schema": "public", + "values": [ + "1", + "2" + ] + }, + "public.enum_election_candidate_vote_vote_type": { + "name": "enum_election_candidate_vote_vote_type", + "schema": "public", + "values": [ + "nominal", + "legenda" + ] + }, + "public.enum_election_candidate_office": { + "name": "enum_election_candidate_office", + "schema": "public", + "values": [ + "presidente", + "governador", + "deputado_federal", + "deputado_estadual" + ] + }, + "public.enum_election_candidate_turn": { + "name": "enum_election_candidate_turn", + "schema": "public", + "values": [ + "1", + "2" + ] + }, + "public.enum_election_candidate_elected_by": { + "name": "enum_election_candidate_elected_by", + "schema": "public", + "values": [ + "QP", + "média", + "2º turno" + ] + }, + "public.enum_election_candidate_running_again2026": { + "name": "enum_election_candidate_running_again2026", + "schema": "public", + "values": [ + "sim", + "nao", + "desconhecido" + ] + }, + "public.enum_petition_status": { + "name": "enum_petition_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "public.enum__petition_v_version_status": { + "name": "enum__petition_v_version_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "public.enum_contact_gender": { + "name": "enum_contact_gender", + "schema": "public", + "values": [ + "feminino", + "masculino", + "outro", + "nao_informado" + ] + }, + "public.enum_contact_state": { + "name": "enum_contact_state", + "schema": "public", + "values": [ + "AC", + "AL", + "AM", + "AP", + "BA", + "CE", + "DF", + "ES", + "GO", + "MA", + "MG", + "MS", + "MT", + "PA", + "PB", + "PE", + "PI", + "PR", + "RJ", + "RN", + "RO", + "RR", + "RS", + "SC", + "SE", + "SP", + "TO" + ] + }, + "public.enum_post_type": { + "name": "enum_post_type", + "schema": "public", + "values": [ + "noticia", + "campanha", + "artigo", + "evento" + ] + }, + "public.enum_post_status": { + "name": "enum_post_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "public.enum__post_v_version_type": { + "name": "enum__post_v_version_type", + "schema": "public", + "values": [ + "noticia", + "campanha", + "artigo", + "evento" + ] + }, + "public.enum__post_v_version_status": { + "name": "enum__post_v_version_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "public.enum_exports_format": { + "name": "enum_exports_format", + "schema": "public", + "values": [ + "csv", + "json" + ] + }, + "public.enum_exports_sort_order": { + "name": "enum_exports_sort_order", + "schema": "public", + "values": [ + "asc", + "desc" + ] + }, + "public.enum_exports_drafts": { + "name": "enum_exports_drafts", + "schema": "public", + "values": [ + "yes", + "no" + ] + }, + "public.enum_imports_import_mode": { + "name": "enum_imports_import_mode", + "schema": "public", + "values": [ + "create", + "update", + "upsert" + ] + }, + "public.enum_imports_status": { + "name": "enum_imports_status", + "schema": "public", + "values": [ + "pending", + "completed", + "partial", + "failed" + ] + }, + "public.enum_payload_jobs_log_task_slug": { + "name": "enum_payload_jobs_log_task_slug", + "schema": "public", + "values": [ + "inline", + "createCollectionExport", + "createCollectionImport", + "schedulePublish" + ] + }, + "public.enum_payload_jobs_log_state": { + "name": "enum_payload_jobs_log_state", + "schema": "public", + "values": [ + "failed", + "succeeded" + ] + }, + "public.enum_payload_jobs_task_slug": { + "name": "enum_payload_jobs_task_slug", + "schema": "public", + "values": [ + "inline", + "createCollectionExport", + "createCollectionImport", + "schedulePublish" + ] + }, + "public.enum_site_settings_social_links_platform": { + "name": "enum_site_settings_social_links_platform", + "schema": "public", + "values": [ + "instagram", + "facebook", + "youtube", + "whatsapp" + ] + }, + "public.enum_site_settings_status": { + "name": "enum_site_settings_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "public.enum__site_settings_v_version_social_links_platform": { + "name": "enum__site_settings_v_version_social_links_platform", + "schema": "public", + "values": [ + "instagram", + "facebook", + "youtube", + "whatsapp" + ] + }, + "public.enum__site_settings_v_version_status": { + "name": "enum__site_settings_v_version_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "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": "2d7e157e-0346-4f83-972f-36b3ddc8b554", + "prevId": "00000000-0000-0000-0000-000000000000" +} \ No newline at end of file diff --git a/src/migrations/20260819_233503_add_campaign_demand_responsibles.ts b/src/migrations/20260819_233503_add_campaign_demand_responsibles.ts new file mode 100644 index 00000000..2cc7ddc9 --- /dev/null +++ b/src/migrations/20260819_233503_add_campaign_demand_responsibles.ts @@ -0,0 +1,29 @@ +import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres' + +export async function up({ db, payload, req }: MigrateUpArgs): Promise { + await db.execute(sql` + ALTER TABLE "campaign_demand_rels" ADD COLUMN "campaign_user_id" integer; + ALTER TABLE "campaign_demand_rels" ADD CONSTRAINT "campaign_demand_rels_campaign_user_fk" FOREIGN KEY ("campaign_user_id") REFERENCES "public"."campaign_user"("id") ON DELETE cascade ON UPDATE no action; + CREATE INDEX "campaign_demand_rels_campaign_user_id_idx" ON "campaign_demand_rels" USING btree ("campaign_user_id"); + + -- C143 backfill: pre-existing demands keep their creator as responsible, so + -- the explicit-responsible rule does not hide a demand from its own author. + DO $$ + DECLARE backfilled integer; + BEGIN + INSERT INTO "campaign_demand_rels" ("order", "parent_id", "path", "campaign_user_id") + SELECT 0, "id", 'responsibles', "created_by_id" + FROM "campaign_demand" + WHERE "created_by_id" IS NOT NULL; + GET DIAGNOSTICS backfilled = ROW_COUNT; + RAISE NOTICE 'add_campaign_demand_responsibles: backfilling % demanda(s) com o criador como responsável', backfilled; + END $$;`) +} + +export async function down({ db, payload, req }: MigrateDownArgs): Promise { + await db.execute(sql` + ALTER TABLE "campaign_demand_rels" DROP CONSTRAINT "campaign_demand_rels_campaign_user_fk"; + + DROP INDEX "campaign_demand_rels_campaign_user_id_idx"; + ALTER TABLE "campaign_demand_rels" DROP COLUMN "campaign_user_id";`) +} diff --git a/src/migrations/20260820_003426_add_instagram_sync_status.json b/src/migrations/20260820_003426_add_instagram_sync_status.json new file mode 100644 index 00000000..44235a6c --- /dev/null +++ b/src/migrations/20260820_003426_add_instagram_sync_status.json @@ -0,0 +1,13755 @@ +{ + "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'" + }, + "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 + }, + "media_id": { + "name": "media_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "campaign_demand_rels_order_idx": { + "name": "campaign_demand_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaign_demand_rels_parent_idx": { + "name": "campaign_demand_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaign_demand_rels_path_idx": { + "name": "campaign_demand_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaign_demand_rels_media_id_idx": { + "name": "campaign_demand_rels_media_id_idx", + "columns": [ + { + "expression": "media_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "campaign_demand_rels_parent_fk": { + "name": "campaign_demand_rels_parent_fk", + "tableFrom": "campaign_demand_rels", + "tableTo": "campaign_demand", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "campaign_demand_rels_media_fk": { + "name": "campaign_demand_rels_media_fk", + "tableFrom": "campaign_demand_rels", + "tableTo": "media", + "columnsFrom": [ + "media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.supporter": { + "name": "supporter", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "municipality_id": { + "name": "municipality_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vote_intention": { + "name": "vote_intention", + "type": "enum_supporter_vote_intention", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "consent_id": { + "name": "consent_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "consent_content_hash": { + "name": "consent_content_hash", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "consented_at": { + "name": "consented_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "vote_intention_consent_id": { + "name": "vote_intention_consent_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vote_intention_consent_content_hash": { + "name": "vote_intention_consent_content_hash", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "vote_intention_consented_at": { + "name": "vote_intention_consented_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "enum_supporter_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "consent_note": { + "name": "consent_note", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "created_by_id": { + "name": "created_by_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "supporter_contact_idx": { + "name": "supporter_contact_idx", + "columns": [ + { + "expression": "contact_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "supporter_municipality_idx": { + "name": "supporter_municipality_idx", + "columns": [ + { + "expression": "municipality_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "supporter_vote_intention_idx": { + "name": "supporter_vote_intention_idx", + "columns": [ + { + "expression": "vote_intention", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "supporter_consent_idx": { + "name": "supporter_consent_idx", + "columns": [ + { + "expression": "consent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "supporter_vote_intention_consent_idx": { + "name": "supporter_vote_intention_consent_idx", + "columns": [ + { + "expression": "vote_intention_consent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "supporter_source_idx": { + "name": "supporter_source_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "supporter_created_by_idx": { + "name": "supporter_created_by_idx", + "columns": [ + { + "expression": "created_by_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "supporter_updated_at_idx": { + "name": "supporter_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "supporter_created_at_idx": { + "name": "supporter_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "supporter_contact_id_contact_id_fk": { + "name": "supporter_contact_id_contact_id_fk", + "tableFrom": "supporter", + "tableTo": "contact", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "supporter_municipality_id_municipality_id_fk": { + "name": "supporter_municipality_id_municipality_id_fk", + "tableFrom": "supporter", + "tableTo": "municipality", + "columnsFrom": [ + "municipality_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "supporter_consent_id_consent_id_fk": { + "name": "supporter_consent_id_consent_id_fk", + "tableFrom": "supporter", + "tableTo": "consent", + "columnsFrom": [ + "consent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "supporter_vote_intention_consent_id_consent_id_fk": { + "name": "supporter_vote_intention_consent_id_consent_id_fk", + "tableFrom": "supporter", + "tableTo": "consent", + "columnsFrom": [ + "vote_intention_consent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "supporter_created_by_id_campaign_user_id_fk": { + "name": "supporter_created_by_id_campaign_user_id_fk", + "tableFrom": "supporter", + "tableTo": "campaign_user", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.supporter_import_batch": { + "name": "supporter_import_batch", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "batch_id": { + "name": "batch_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + }, + "rows": { + "name": "rows", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "supporter_import_batch_batch_id_idx": { + "name": "supporter_import_batch_batch_id_idx", + "columns": [ + { + "expression": "batch_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "supporter_import_batch_actor_idx": { + "name": "supporter_import_batch_actor_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "supporter_import_batch_expires_at_idx": { + "name": "supporter_import_batch_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "supporter_import_batch_updated_at_idx": { + "name": "supporter_import_batch_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "supporter_import_batch_created_at_idx": { + "name": "supporter_import_batch_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "supporter_import_batch_actor_id_campaign_user_id_fk": { + "name": "supporter_import_batch_actor_id_campaign_user_id_fk", + "tableFrom": "supporter_import_batch", + "tableTo": "campaign_user", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.municipality_update": { + "name": "municipality_update", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "municipality_id": { + "name": "municipality_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "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 + }, + "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_updated_at_idx": { + "name": "municipality_update_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "municipality_update_created_at_idx": { + "name": "municipality_update_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "municipality_update_municipality_id_municipality_id_fk": { + "name": "municipality_update_municipality_id_municipality_id_fk", + "tableFrom": "municipality_update", + "tableTo": "municipality", + "columnsFrom": [ + "municipality_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "municipality_update_author_id_campaign_user_id_fk": { + "name": "municipality_update_author_id_campaign_user_id_fk", + "tableFrom": "municipality_update", + "tableTo": "campaign_user", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.activity_tasks": { + "name": "activity_tasks", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "responsible_id": { + "name": "responsible_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "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": true + }, + "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 + }, + "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_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_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.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 + }, + "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": true + }, + "city": { + "name": "city", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "postal_code": { + "name": "postal_code", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "contact_updated_at_idx": { + "name": "contact_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "contact_created_at_idx": { + "name": "contact_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.consent": { + "name": "consent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "text": { + "name": "text", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "consent_key_idx": { + "name": "consent_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "consent_updated_at_idx": { + "name": "consent_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "consent_created_at_idx": { + "name": "consent_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.signature": { + "name": "signature", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "petition_id": { + "name": "petition_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "consent_id": { + "name": "consent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "comment": { + "name": "comment", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "signature_contact_idx": { + "name": "signature_contact_idx", + "columns": [ + { + "expression": "contact_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "signature_petition_idx": { + "name": "signature_petition_idx", + "columns": [ + { + "expression": "petition_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "signature_consent_idx": { + "name": "signature_consent_idx", + "columns": [ + { + "expression": "consent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "signature_updated_at_idx": { + "name": "signature_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "signature_created_at_idx": { + "name": "signature_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "signature_contact_id_contact_id_fk": { + "name": "signature_contact_id_contact_id_fk", + "tableFrom": "signature", + "tableTo": "contact", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "signature_petition_id_petition_id_fk": { + "name": "signature_petition_id_petition_id_fk", + "tableFrom": "signature", + "tableTo": "petition", + "columnsFrom": [ + "petition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "signature_consent_id_consent_id_fk": { + "name": "signature_consent_id_consent_id_fk", + "tableFrom": "signature", + "tableTo": "consent", + "columnsFrom": [ + "consent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "consent_id": { + "name": "consent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "comment": { + "name": "comment", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "subscription_contact_idx": { + "name": "subscription_contact_idx", + "columns": [ + { + "expression": "contact_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "subscription_consent_idx": { + "name": "subscription_consent_idx", + "columns": [ + { + "expression": "consent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "subscription_updated_at_idx": { + "name": "subscription_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "subscription_created_at_idx": { + "name": "subscription_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "subscription_contact_id_contact_id_fk": { + "name": "subscription_contact_id_contact_id_fk", + "tableFrom": "subscription", + "tableTo": "contact", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "subscription_consent_id_consent_id_fk": { + "name": "subscription_consent_id_consent_id_fk", + "tableFrom": "subscription", + "tableTo": "consent", + "columnsFrom": [ + "consent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.post": { + "name": "post", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "enum_post_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "category_id": { + "name": "category_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "subtitle": { + "name": "subtitle", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "cover_image_id": { + "name": "cover_image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "published_date": { + "name": "published_date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "_status": { + "name": "_status", + "type": "enum_post_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'draft'" + } + }, + "indexes": { + "post_slug_idx": { + "name": "post_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "post_category_idx": { + "name": "post_category_idx", + "columns": [ + { + "expression": "category_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "post_cover_image_idx": { + "name": "post_cover_image_idx", + "columns": [ + { + "expression": "cover_image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "post_updated_at_idx": { + "name": "post_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "post_created_at_idx": { + "name": "post_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "post__status_idx": { + "name": "post__status_idx", + "columns": [ + { + "expression": "_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "post_category_id_tag_id_fk": { + "name": "post_category_id_tag_id_fk", + "tableFrom": "post", + "tableTo": "tag", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "post_cover_image_id_media_id_fk": { + "name": "post_cover_image_id_media_id_fk", + "tableFrom": "post", + "tableTo": "media", + "columnsFrom": [ + "cover_image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.post_rels": { + "name": "post_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "post_rels_order_idx": { + "name": "post_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "post_rels_parent_idx": { + "name": "post_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "post_rels_path_idx": { + "name": "post_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "post_rels_tag_id_idx": { + "name": "post_rels_tag_id_idx", + "columns": [ + { + "expression": "tag_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "post_rels_parent_fk": { + "name": "post_rels_parent_fk", + "tableFrom": "post_rels", + "tableTo": "post", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "post_rels_tag_fk": { + "name": "post_rels_tag_fk", + "tableFrom": "post_rels", + "tableTo": "tag", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._post_v": { + "name": "_post_v", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_title": { + "name": "version_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_slug": { + "name": "version_slug", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_type": { + "name": "version_type", + "type": "enum__post_v_version_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "version_category_id": { + "name": "version_category_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_subtitle": { + "name": "version_subtitle", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version_cover_image_id": { + "name": "version_cover_image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "version_published_date": { + "name": "version_published_date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_body": { + "name": "version_body", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "version_updated_at": { + "name": "version_updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_created_at": { + "name": "version_created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version__status": { + "name": "version__status", + "type": "enum__post_v_version_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'draft'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "latest": { + "name": "latest", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "_post_v_parent_idx": { + "name": "_post_v_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_post_v_version_version_slug_idx": { + "name": "_post_v_version_version_slug_idx", + "columns": [ + { + "expression": "version_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_post_v_version_version_category_idx": { + "name": "_post_v_version_version_category_idx", + "columns": [ + { + "expression": "version_category_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_post_v_version_version_cover_image_idx": { + "name": "_post_v_version_version_cover_image_idx", + "columns": [ + { + "expression": "version_cover_image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_post_v_version_version_updated_at_idx": { + "name": "_post_v_version_version_updated_at_idx", + "columns": [ + { + "expression": "version_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_post_v_version_version_created_at_idx": { + "name": "_post_v_version_version_created_at_idx", + "columns": [ + { + "expression": "version_created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_post_v_version_version__status_idx": { + "name": "_post_v_version_version__status_idx", + "columns": [ + { + "expression": "version__status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_post_v_created_at_idx": { + "name": "_post_v_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_post_v_updated_at_idx": { + "name": "_post_v_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_post_v_latest_idx": { + "name": "_post_v_latest_idx", + "columns": [ + { + "expression": "latest", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_post_v_parent_id_post_id_fk": { + "name": "_post_v_parent_id_post_id_fk", + "tableFrom": "_post_v", + "tableTo": "post", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "_post_v_version_category_id_tag_id_fk": { + "name": "_post_v_version_category_id_tag_id_fk", + "tableFrom": "_post_v", + "tableTo": "tag", + "columnsFrom": [ + "version_category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "_post_v_version_cover_image_id_media_id_fk": { + "name": "_post_v_version_cover_image_id_media_id_fk", + "tableFrom": "_post_v", + "tableTo": "media", + "columnsFrom": [ + "version_cover_image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._post_v_rels": { + "name": "_post_v_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "_post_v_rels_order_idx": { + "name": "_post_v_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_post_v_rels_parent_idx": { + "name": "_post_v_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_post_v_rels_path_idx": { + "name": "_post_v_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_post_v_rels_tag_id_idx": { + "name": "_post_v_rels_tag_id_idx", + "columns": [ + { + "expression": "tag_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_post_v_rels_parent_fk": { + "name": "_post_v_rels_parent_fk", + "tableFrom": "_post_v_rels", + "tableTo": "_post_v", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "_post_v_rels_tag_fk": { + "name": "_post_v_rels_tag_fk", + "tableFrom": "_post_v_rels", + "tableTo": "tag", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tag": { + "name": "tag", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "hidden": { + "name": "hidden", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tag_slug_idx": { + "name": "tag_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tag_updated_at_idx": { + "name": "tag_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tag_created_at_idx": { + "name": "tag_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.exports": { + "name": "exports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "format": { + "name": "format", + "type": "enum_exports_format", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'csv'" + }, + "limit": { + "name": "limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "page": { + "name": "page", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "sort": { + "name": "sort", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "enum_exports_sort_order", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "drafts": { + "name": "drafts", + "type": "enum_exports_drafts", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'yes'" + }, + "collection_slug": { + "name": "collection_slug", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'signature'" + }, + "where": { + "name": "where", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "thumbnail_u_r_l": { + "name": "thumbnail_u_r_l", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "filename": { + "name": "filename", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "filesize": { + "name": "filesize", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "focal_x": { + "name": "focal_x", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "focal_y": { + "name": "focal_y", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "exports_updated_at_idx": { + "name": "exports_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "exports_created_at_idx": { + "name": "exports_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "exports_filename_idx": { + "name": "exports_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.exports_texts": { + "name": "exports_texts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "exports_texts_order_parent": { + "name": "exports_texts_order_parent", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "exports_texts_parent_fk": { + "name": "exports_texts_parent_fk", + "tableFrom": "exports_texts", + "tableTo": "exports", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.imports": { + "name": "imports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "collection_slug": { + "name": "collection_slug", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "import_mode": { + "name": "import_mode", + "type": "enum_imports_import_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "match_field": { + "name": "match_field", + "type": "varchar", + "primaryKey": false, + "notNull": false, + "default": "'id'" + }, + "status": { + "name": "status", + "type": "enum_imports_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'pending'" + }, + "summary_imported": { + "name": "summary_imported", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "summary_updated": { + "name": "summary_updated", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "summary_total": { + "name": "summary_total", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "summary_issues": { + "name": "summary_issues", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "summary_issue_details": { + "name": "summary_issue_details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "thumbnail_u_r_l": { + "name": "thumbnail_u_r_l", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "filename": { + "name": "filename", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "filesize": { + "name": "filesize", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "focal_x": { + "name": "focal_x", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "focal_y": { + "name": "focal_y", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "imports_updated_at_idx": { + "name": "imports_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "imports_created_at_idx": { + "name": "imports_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "imports_filename_idx": { + "name": "imports_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_kv": { + "name": "payload_kv", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "payload_kv_key_idx": { + "name": "payload_kv_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_jobs_log": { + "name": "payload_jobs_log", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "executed_at": { + "name": "executed_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + }, + "task_slug": { + "name": "task_slug", + "type": "enum_payload_jobs_log_task_slug", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "task_i_d": { + "name": "task_i_d", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "input": { + "name": "input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "enum_payload_jobs_log_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "payload_jobs_log_order_idx": { + "name": "payload_jobs_log_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_log_parent_id_idx": { + "name": "payload_jobs_log_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payload_jobs_log_parent_id_fk": { + "name": "payload_jobs_log_parent_id_fk", + "tableFrom": "payload_jobs_log", + "tableTo": "payload_jobs", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_jobs": { + "name": "payload_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "input": { + "name": "input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "total_tried": { + "name": "total_tried", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "has_error": { + "name": "has_error", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "error": { + "name": "error", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "task_slug": { + "name": "task_slug", + "type": "enum_payload_jobs_task_slug", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "queue": { + "name": "queue", + "type": "varchar", + "primaryKey": false, + "notNull": false, + "default": "'default'" + }, + "wait_until": { + "name": "wait_until", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "processing": { + "name": "processing", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payload_jobs_completed_at_idx": { + "name": "payload_jobs_completed_at_idx", + "columns": [ + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_total_tried_idx": { + "name": "payload_jobs_total_tried_idx", + "columns": [ + { + "expression": "total_tried", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_has_error_idx": { + "name": "payload_jobs_has_error_idx", + "columns": [ + { + "expression": "has_error", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_task_slug_idx": { + "name": "payload_jobs_task_slug_idx", + "columns": [ + { + "expression": "task_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_queue_idx": { + "name": "payload_jobs_queue_idx", + "columns": [ + { + "expression": "queue", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_wait_until_idx": { + "name": "payload_jobs_wait_until_idx", + "columns": [ + { + "expression": "wait_until", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_processing_idx": { + "name": "payload_jobs_processing_idx", + "columns": [ + { + "expression": "processing", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_updated_at_idx": { + "name": "payload_jobs_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_jobs_created_at_idx": { + "name": "payload_jobs_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_locked_documents": { + "name": "payload_locked_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "global_slug": { + "name": "global_slug", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payload_locked_documents_global_slug_idx": { + "name": "payload_locked_documents_global_slug_idx", + "columns": [ + { + "expression": "global_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_updated_at_idx": { + "name": "payload_locked_documents_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_created_at_idx": { + "name": "payload_locked_documents_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_locked_documents_rels": { + "name": "payload_locked_documents_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "users_id": { + "name": "users_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "campaign_user_id": { + "name": "campaign_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "campaign_web_authn_credential_id": { + "name": "campaign_web_authn_credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "notification_id": { + "name": "notification_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "push_subscription_id": { + "name": "push_subscription_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "campaign_invite_id": { + "name": "campaign_invite_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "campaign_vote_summary_snapshot_id": { + "name": "campaign_vote_summary_snapshot_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "municipality_id": { + "name": "municipality_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "leadership_id": { + "name": "leadership_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "state_deputy_id": { + "name": "state_deputy_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vote_pledge_id": { + "name": "vote_pledge_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "allocation_decision_id": { + "name": "allocation_decision_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "campaign_demand_id": { + "name": "campaign_demand_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "supporter_id": { + "name": "supporter_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "supporter_import_batch_id": { + "name": "supporter_import_batch_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "municipality_update_id": { + "name": "municipality_update_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "activity_id": { + "name": "activity_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "calendar_feed_id": { + "name": "calendar_feed_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "google_calendar_sync_id": { + "name": "google_calendar_sync_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "election_tally_id": { + "name": "election_tally_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "election_candidate_vote_id": { + "name": "election_candidate_vote_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "election_candidate_id": { + "name": "election_candidate_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "media_id": { + "name": "media_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "petition_id": { + "name": "petition_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "contact_id": { + "name": "contact_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "consent_id": { + "name": "consent_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "signature_id": { + "name": "signature_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "subscription_id": { + "name": "subscription_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "post_id": { + "name": "post_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tag_id": { + "name": "tag_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "payload_locked_documents_rels_order_idx": { + "name": "payload_locked_documents_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_parent_idx": { + "name": "payload_locked_documents_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_path_idx": { + "name": "payload_locked_documents_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_users_id_idx": { + "name": "payload_locked_documents_rels_users_id_idx", + "columns": [ + { + "expression": "users_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_campaign_user_id_idx": { + "name": "payload_locked_documents_rels_campaign_user_id_idx", + "columns": [ + { + "expression": "campaign_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_campaign_web_authn_credent_idx": { + "name": "payload_locked_documents_rels_campaign_web_authn_credent_idx", + "columns": [ + { + "expression": "campaign_web_authn_credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_notification_id_idx": { + "name": "payload_locked_documents_rels_notification_id_idx", + "columns": [ + { + "expression": "notification_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_push_subscription_id_idx": { + "name": "payload_locked_documents_rels_push_subscription_id_idx", + "columns": [ + { + "expression": "push_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_campaign_invite_id_idx": { + "name": "payload_locked_documents_rels_campaign_invite_id_idx", + "columns": [ + { + "expression": "campaign_invite_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_campaign_vote_summary_snap_idx": { + "name": "payload_locked_documents_rels_campaign_vote_summary_snap_idx", + "columns": [ + { + "expression": "campaign_vote_summary_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_municipality_id_idx": { + "name": "payload_locked_documents_rels_municipality_id_idx", + "columns": [ + { + "expression": "municipality_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_leadership_id_idx": { + "name": "payload_locked_documents_rels_leadership_id_idx", + "columns": [ + { + "expression": "leadership_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_organization_id_idx": { + "name": "payload_locked_documents_rels_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_state_deputy_id_idx": { + "name": "payload_locked_documents_rels_state_deputy_id_idx", + "columns": [ + { + "expression": "state_deputy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_vote_pledge_id_idx": { + "name": "payload_locked_documents_rels_vote_pledge_id_idx", + "columns": [ + { + "expression": "vote_pledge_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_allocation_decision_id_idx": { + "name": "payload_locked_documents_rels_allocation_decision_id_idx", + "columns": [ + { + "expression": "allocation_decision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_campaign_demand_id_idx": { + "name": "payload_locked_documents_rels_campaign_demand_id_idx", + "columns": [ + { + "expression": "campaign_demand_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_supporter_id_idx": { + "name": "payload_locked_documents_rels_supporter_id_idx", + "columns": [ + { + "expression": "supporter_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_supporter_import_batch_id_idx": { + "name": "payload_locked_documents_rels_supporter_import_batch_id_idx", + "columns": [ + { + "expression": "supporter_import_batch_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_municipality_update_id_idx": { + "name": "payload_locked_documents_rels_municipality_update_id_idx", + "columns": [ + { + "expression": "municipality_update_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_activity_id_idx": { + "name": "payload_locked_documents_rels_activity_id_idx", + "columns": [ + { + "expression": "activity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_calendar_feed_id_idx": { + "name": "payload_locked_documents_rels_calendar_feed_id_idx", + "columns": [ + { + "expression": "calendar_feed_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_google_calendar_sync_id_idx": { + "name": "payload_locked_documents_rels_google_calendar_sync_id_idx", + "columns": [ + { + "expression": "google_calendar_sync_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_election_tally_id_idx": { + "name": "payload_locked_documents_rels_election_tally_id_idx", + "columns": [ + { + "expression": "election_tally_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_election_candidate_vote_id_idx": { + "name": "payload_locked_documents_rels_election_candidate_vote_id_idx", + "columns": [ + { + "expression": "election_candidate_vote_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_election_candidate_id_idx": { + "name": "payload_locked_documents_rels_election_candidate_id_idx", + "columns": [ + { + "expression": "election_candidate_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_media_id_idx": { + "name": "payload_locked_documents_rels_media_id_idx", + "columns": [ + { + "expression": "media_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_petition_id_idx": { + "name": "payload_locked_documents_rels_petition_id_idx", + "columns": [ + { + "expression": "petition_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_contact_id_idx": { + "name": "payload_locked_documents_rels_contact_id_idx", + "columns": [ + { + "expression": "contact_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_consent_id_idx": { + "name": "payload_locked_documents_rels_consent_id_idx", + "columns": [ + { + "expression": "consent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_signature_id_idx": { + "name": "payload_locked_documents_rels_signature_id_idx", + "columns": [ + { + "expression": "signature_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_subscription_id_idx": { + "name": "payload_locked_documents_rels_subscription_id_idx", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_post_id_idx": { + "name": "payload_locked_documents_rels_post_id_idx", + "columns": [ + { + "expression": "post_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_tag_id_idx": { + "name": "payload_locked_documents_rels_tag_id_idx", + "columns": [ + { + "expression": "tag_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payload_locked_documents_rels_parent_fk": { + "name": "payload_locked_documents_rels_parent_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "payload_locked_documents", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_users_fk": { + "name": "payload_locked_documents_rels_users_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "users", + "columnsFrom": [ + "users_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_campaign_user_fk": { + "name": "payload_locked_documents_rels_campaign_user_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "campaign_user", + "columnsFrom": [ + "campaign_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_campaign_web_authn_credenti_fk": { + "name": "payload_locked_documents_rels_campaign_web_authn_credenti_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "campaign_web_authn_credential", + "columnsFrom": [ + "campaign_web_authn_credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_notification_fk": { + "name": "payload_locked_documents_rels_notification_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "notification", + "columnsFrom": [ + "notification_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_push_subscription_fk": { + "name": "payload_locked_documents_rels_push_subscription_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "push_subscription", + "columnsFrom": [ + "push_subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_campaign_invite_fk": { + "name": "payload_locked_documents_rels_campaign_invite_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "campaign_invite", + "columnsFrom": [ + "campaign_invite_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_campaign_vote_summary_snaps_fk": { + "name": "payload_locked_documents_rels_campaign_vote_summary_snaps_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "campaign_vote_summary_snapshot", + "columnsFrom": [ + "campaign_vote_summary_snapshot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_municipality_fk": { + "name": "payload_locked_documents_rels_municipality_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "municipality", + "columnsFrom": [ + "municipality_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_leadership_fk": { + "name": "payload_locked_documents_rels_leadership_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "leadership", + "columnsFrom": [ + "leadership_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_organization_fk": { + "name": "payload_locked_documents_rels_organization_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_state_deputy_fk": { + "name": "payload_locked_documents_rels_state_deputy_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "state_deputy", + "columnsFrom": [ + "state_deputy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_vote_pledge_fk": { + "name": "payload_locked_documents_rels_vote_pledge_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "vote_pledge", + "columnsFrom": [ + "vote_pledge_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_allocation_decision_fk": { + "name": "payload_locked_documents_rels_allocation_decision_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "allocation_decision", + "columnsFrom": [ + "allocation_decision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_campaign_demand_fk": { + "name": "payload_locked_documents_rels_campaign_demand_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "campaign_demand", + "columnsFrom": [ + "campaign_demand_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_supporter_fk": { + "name": "payload_locked_documents_rels_supporter_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "supporter", + "columnsFrom": [ + "supporter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_supporter_import_batch_fk": { + "name": "payload_locked_documents_rels_supporter_import_batch_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "supporter_import_batch", + "columnsFrom": [ + "supporter_import_batch_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_municipality_update_fk": { + "name": "payload_locked_documents_rels_municipality_update_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "municipality_update", + "columnsFrom": [ + "municipality_update_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_activity_fk": { + "name": "payload_locked_documents_rels_activity_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "activity", + "columnsFrom": [ + "activity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_calendar_feed_fk": { + "name": "payload_locked_documents_rels_calendar_feed_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "calendar_feed", + "columnsFrom": [ + "calendar_feed_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_google_calendar_sync_fk": { + "name": "payload_locked_documents_rels_google_calendar_sync_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "google_calendar_sync", + "columnsFrom": [ + "google_calendar_sync_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_election_tally_fk": { + "name": "payload_locked_documents_rels_election_tally_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "election_tally", + "columnsFrom": [ + "election_tally_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_election_candidate_vote_fk": { + "name": "payload_locked_documents_rels_election_candidate_vote_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "election_candidate_vote", + "columnsFrom": [ + "election_candidate_vote_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_election_candidate_fk": { + "name": "payload_locked_documents_rels_election_candidate_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "election_candidate", + "columnsFrom": [ + "election_candidate_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_media_fk": { + "name": "payload_locked_documents_rels_media_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "media", + "columnsFrom": [ + "media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_petition_fk": { + "name": "payload_locked_documents_rels_petition_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "petition", + "columnsFrom": [ + "petition_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_contact_fk": { + "name": "payload_locked_documents_rels_contact_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "contact", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_consent_fk": { + "name": "payload_locked_documents_rels_consent_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "consent", + "columnsFrom": [ + "consent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_signature_fk": { + "name": "payload_locked_documents_rels_signature_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "signature", + "columnsFrom": [ + "signature_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_subscription_fk": { + "name": "payload_locked_documents_rels_subscription_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "subscription", + "columnsFrom": [ + "subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_post_fk": { + "name": "payload_locked_documents_rels_post_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "post", + "columnsFrom": [ + "post_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_tag_fk": { + "name": "payload_locked_documents_rels_tag_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "tag", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_preferences": { + "name": "payload_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payload_preferences_key_idx": { + "name": "payload_preferences_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_updated_at_idx": { + "name": "payload_preferences_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_created_at_idx": { + "name": "payload_preferences_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_preferences_rels": { + "name": "payload_preferences_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "users_id": { + "name": "users_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "campaign_user_id": { + "name": "campaign_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "payload_preferences_rels_order_idx": { + "name": "payload_preferences_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_rels_parent_idx": { + "name": "payload_preferences_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_rels_path_idx": { + "name": "payload_preferences_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_rels_users_id_idx": { + "name": "payload_preferences_rels_users_id_idx", + "columns": [ + { + "expression": "users_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_rels_campaign_user_id_idx": { + "name": "payload_preferences_rels_campaign_user_id_idx", + "columns": [ + { + "expression": "campaign_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payload_preferences_rels_parent_fk": { + "name": "payload_preferences_rels_parent_fk", + "tableFrom": "payload_preferences_rels", + "tableTo": "payload_preferences", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_preferences_rels_users_fk": { + "name": "payload_preferences_rels_users_fk", + "tableFrom": "payload_preferences_rels", + "tableTo": "users", + "columnsFrom": [ + "users_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_preferences_rels_campaign_user_fk": { + "name": "payload_preferences_rels_campaign_user_fk", + "tableFrom": "payload_preferences_rels", + "tableTo": "campaign_user", + "columnsFrom": [ + "campaign_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_migrations": { + "name": "payload_migrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "batch": { + "name": "batch", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payload_migrations_updated_at_idx": { + "name": "payload_migrations_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_migrations_created_at_idx": { + "name": "payload_migrations_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.site_settings_social_links": { + "name": "site_settings_social_links", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "enum_site_settings_social_links_platform", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "site_settings_social_links_order_idx": { + "name": "site_settings_social_links_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "site_settings_social_links_parent_id_idx": { + "name": "site_settings_social_links_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "site_settings_social_links_parent_id_fk": { + "name": "site_settings_social_links_parent_id_fk", + "tableFrom": "site_settings_social_links", + "tableTo": "site_settings", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.site_settings": { + "name": "site_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "header_title": { + "name": "header_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "_status": { + "name": "_status", + "type": "enum_site_settings_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'draft'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "site_settings__status_idx": { + "name": "site_settings__status_idx", + "columns": [ + { + "expression": "_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._site_settings_v_version_social_links": { + "name": "_site_settings_v_version_social_links", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "enum__site_settings_v_version_social_links_platform", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "_uuid": { + "name": "_uuid", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "_site_settings_v_version_social_links_order_idx": { + "name": "_site_settings_v_version_social_links_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_site_settings_v_version_social_links_parent_id_idx": { + "name": "_site_settings_v_version_social_links_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "_site_settings_v_version_social_links_parent_id_fk": { + "name": "_site_settings_v_version_social_links_parent_id_fk", + "tableFrom": "_site_settings_v_version_social_links", + "tableTo": "_site_settings_v", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public._site_settings_v": { + "name": "_site_settings_v", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "version_header_title": { + "name": "version_header_title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "version__status": { + "name": "version__status", + "type": "enum__site_settings_v_version_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'draft'" + }, + "version_updated_at": { + "name": "version_updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "version_created_at": { + "name": "version_created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "latest": { + "name": "latest", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "autosave": { + "name": "autosave", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "_site_settings_v_version_version__status_idx": { + "name": "_site_settings_v_version_version__status_idx", + "columns": [ + { + "expression": "version__status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_site_settings_v_created_at_idx": { + "name": "_site_settings_v_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_site_settings_v_updated_at_idx": { + "name": "_site_settings_v_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_site_settings_v_latest_idx": { + "name": "_site_settings_v_latest_idx", + "columns": [ + { + "expression": "latest", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "_site_settings_v_autosave_idx": { + "name": "_site_settings_v_autosave_idx", + "columns": [ + { + "expression": "autosave", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.home": { + "name": "home", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "image_id": { + "name": "image_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "home_image_idx": { + "name": "home_image_idx", + "columns": [ + { + "expression": "image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "home_image_id_media_id_fk": { + "name": "home_image_id_media_id_fk", + "tableFrom": "home", + "tableTo": "media", + "columnsFrom": [ + "image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.metadata_keywords": { + "name": "metadata_keywords", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "keyword": { + "name": "keyword", + "type": "varchar", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "metadata_keywords_order_idx": { + "name": "metadata_keywords_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "metadata_keywords_parent_id_idx": { + "name": "metadata_keywords_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "metadata_keywords_parent_id_fk": { + "name": "metadata_keywords_parent_id_fk", + "tableFrom": "metadata_keywords", + "tableTo": "metadata", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.metadata": { + "name": "metadata", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "open_graph_site_name": { + "name": "open_graph_site_name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "open_graph_description": { + "name": "open_graph_description", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "twitter_creator": { + "name": "twitter_creator", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "twitter_description": { + "name": "twitter_description", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "image_id": { + "name": "image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "metadata_image_idx": { + "name": "metadata_image_idx", + "columns": [ + { + "expression": "image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "metadata_image_id_media_id_fk": { + "name": "metadata_image_id_media_id_fk", + "tableFrom": "metadata", + "tableTo": "media", + "columnsFrom": [ + "image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.privacy_policy": { + "name": "privacy_policy", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "published": { + "name": "published", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "body": { + "name": "body", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.campaign_goals": { + "name": "campaign_goals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "state_goal": { + "name": "state_goal", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 150000 + }, + "margin": { + "name": "margin", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "base_year": { + "name": "base_year", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "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", + "leader" + ] + }, + "public.enum_notification_type": { + "name": "enum_notification_type", + "schema": "public", + "values": [ + "municipality_update", + "new_supporter", + "activity_attention", + "invite_accepted" + ] + }, + "public.enum_campaign_invite_kind": { + "name": "enum_campaign_invite_kind", + "schema": "public", + "values": [ + "login", + "autopreenchimento" + ] + }, + "public.enum_municipality_kind": { + "name": "enum_municipality_kind", + "schema": "public", + "values": [ + "municipio", + "zona" + ] + }, + "public.enum_municipality_priority": { + "name": "enum_municipality_priority", + "schema": "public", + "values": [ + "alta", + "normal" + ] + }, + "public.enum_municipality_engagement_level": { + "name": "enum_municipality_engagement_level", + "schema": "public", + "values": [ + "n0", + "n1", + "n2", + "n3", + "n4" + ] + }, + "public.enum_municipality_political_trend_status": { + "name": "enum_municipality_political_trend_status", + "schema": "public", + "values": [ + "favoravel", + "neutra", + "desfavoravel" + ] + }, + "public.enum_leadership_support_status": { + "name": "enum_leadership_support_status", + "schema": "public", + "values": [ + "engajado", + "a_abordar", + "em_disputa", + "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_election_tally_office": { + "name": "enum_election_tally_office", + "schema": "public", + "values": [ + "presidente", + "governador", + "deputado_federal", + "deputado_estadual" + ] + }, + "public.enum_election_tally_turn": { + "name": "enum_election_tally_turn", + "schema": "public", + "values": [ + "1", + "2" + ] + }, + "public.enum_election_candidate_vote_office": { + "name": "enum_election_candidate_vote_office", + "schema": "public", + "values": [ + "presidente", + "governador", + "deputado_federal", + "deputado_estadual" + ] + }, + "public.enum_election_candidate_vote_turn": { + "name": "enum_election_candidate_vote_turn", + "schema": "public", + "values": [ + "1", + "2" + ] + }, + "public.enum_election_candidate_vote_vote_type": { + "name": "enum_election_candidate_vote_vote_type", + "schema": "public", + "values": [ + "nominal", + "legenda" + ] + }, + "public.enum_election_candidate_office": { + "name": "enum_election_candidate_office", + "schema": "public", + "values": [ + "presidente", + "governador", + "deputado_federal", + "deputado_estadual" + ] + }, + "public.enum_election_candidate_turn": { + "name": "enum_election_candidate_turn", + "schema": "public", + "values": [ + "1", + "2" + ] + }, + "public.enum_election_candidate_elected_by": { + "name": "enum_election_candidate_elected_by", + "schema": "public", + "values": [ + "QP", + "média", + "2º turno" + ] + }, + "public.enum_election_candidate_running_again2026": { + "name": "enum_election_candidate_running_again2026", + "schema": "public", + "values": [ + "sim", + "nao", + "desconhecido" + ] + }, + "public.enum_petition_status": { + "name": "enum_petition_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "public.enum__petition_v_version_status": { + "name": "enum__petition_v_version_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "public.enum_contact_gender": { + "name": "enum_contact_gender", + "schema": "public", + "values": [ + "feminino", + "masculino", + "outro", + "nao_informado" + ] + }, + "public.enum_contact_state": { + "name": "enum_contact_state", + "schema": "public", + "values": [ + "AC", + "AL", + "AM", + "AP", + "BA", + "CE", + "DF", + "ES", + "GO", + "MA", + "MG", + "MS", + "MT", + "PA", + "PB", + "PE", + "PI", + "PR", + "RJ", + "RN", + "RO", + "RR", + "RS", + "SC", + "SE", + "SP", + "TO" + ] + }, + "public.enum_post_type": { + "name": "enum_post_type", + "schema": "public", + "values": [ + "noticia", + "campanha", + "artigo", + "evento" + ] + }, + "public.enum_post_status": { + "name": "enum_post_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "public.enum__post_v_version_type": { + "name": "enum__post_v_version_type", + "schema": "public", + "values": [ + "noticia", + "campanha", + "artigo", + "evento" + ] + }, + "public.enum__post_v_version_status": { + "name": "enum__post_v_version_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "public.enum_exports_format": { + "name": "enum_exports_format", + "schema": "public", + "values": [ + "csv", + "json" + ] + }, + "public.enum_exports_sort_order": { + "name": "enum_exports_sort_order", + "schema": "public", + "values": [ + "asc", + "desc" + ] + }, + "public.enum_exports_drafts": { + "name": "enum_exports_drafts", + "schema": "public", + "values": [ + "yes", + "no" + ] + }, + "public.enum_imports_import_mode": { + "name": "enum_imports_import_mode", + "schema": "public", + "values": [ + "create", + "update", + "upsert" + ] + }, + "public.enum_imports_status": { + "name": "enum_imports_status", + "schema": "public", + "values": [ + "pending", + "completed", + "partial", + "failed" + ] + }, + "public.enum_payload_jobs_log_task_slug": { + "name": "enum_payload_jobs_log_task_slug", + "schema": "public", + "values": [ + "inline", + "createCollectionExport", + "createCollectionImport", + "schedulePublish" + ] + }, + "public.enum_payload_jobs_log_state": { + "name": "enum_payload_jobs_log_state", + "schema": "public", + "values": [ + "failed", + "succeeded" + ] + }, + "public.enum_payload_jobs_task_slug": { + "name": "enum_payload_jobs_task_slug", + "schema": "public", + "values": [ + "inline", + "createCollectionExport", + "createCollectionImport", + "schedulePublish" + ] + }, + "public.enum_site_settings_social_links_platform": { + "name": "enum_site_settings_social_links_platform", + "schema": "public", + "values": [ + "instagram", + "facebook", + "youtube", + "whatsapp" + ] + }, + "public.enum_site_settings_status": { + "name": "enum_site_settings_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "public.enum__site_settings_v_version_social_links_platform": { + "name": "enum__site_settings_v_version_social_links_platform", + "schema": "public", + "values": [ + "instagram", + "facebook", + "youtube", + "whatsapp" + ] + }, + "public.enum__site_settings_v_version_status": { + "name": "enum__site_settings_v_version_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "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": "4de11381-4895-4061-a25a-6393344ce709", + "prevId": "00000000-0000-0000-0000-000000000000" +} \ No newline at end of file diff --git a/src/migrations/20260820_003426_add_instagram_sync_status.ts b/src/migrations/20260820_003426_add_instagram_sync_status.ts new file mode 100644 index 00000000..a0d9e78e --- /dev/null +++ b/src/migrations/20260820_003426_add_instagram_sync_status.ts @@ -0,0 +1,11 @@ +import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres' + +export async function up({ db, payload, req }: MigrateUpArgs): Promise { + await db.execute(sql` + ALTER TABLE "social_feed_settings" ADD COLUMN "instagram_sync_status" jsonb;`) +} + +export async function down({ db, payload, req }: MigrateDownArgs): Promise { + await db.execute(sql` + ALTER TABLE "social_feed_settings" DROP COLUMN "instagram_sync_status";`) +} diff --git a/src/migrations/index.ts b/src/migrations/index.ts index 8e9e0075..5517a8c7 100644 --- a/src/migrations/index.ts +++ b/src/migrations/index.ts @@ -59,6 +59,8 @@ import * as migration_20260818_192948_add_instagram_social_feed_fields from './2 import * as migration_20260819_192605_add_campaign_newsletter_capture from './20260819_192605_add_campaign_newsletter_capture'; import * as migration_20260819_213947_add_site_settings_facebook_pixel_id from './20260819_213947_add_site_settings_facebook_pixel_id'; import * as migration_20260819_214755_add_campaign_user_advisor_permission_profile from './20260819_214755_add_campaign_user_advisor_permission_profile'; +import * as migration_20260819_233503_add_campaign_demand_responsibles from './20260819_233503_add_campaign_demand_responsibles'; +import * as migration_20260820_003426_add_instagram_sync_status from './20260820_003426_add_instagram_sync_status'; export const migrations = [ { @@ -366,4 +368,14 @@ export const migrations = [ down: migration_20260819_214755_add_campaign_user_advisor_permission_profile.down, name: '20260819_214755_add_campaign_user_advisor_permission_profile', }, + { + up: migration_20260819_233503_add_campaign_demand_responsibles.up, + down: migration_20260819_233503_add_campaign_demand_responsibles.down, + name: '20260819_233503_add_campaign_demand_responsibles', + }, + { + up: migration_20260820_003426_add_instagram_sync_status.up, + down: migration_20260820_003426_add_instagram_sync_status.down, + name: '20260820_003426_add_instagram_sync_status', + }, ]; diff --git a/src/payload-types.ts b/src/payload-types.ts index e03c9e58..677caeaf 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -725,6 +725,10 @@ export interface CampaignDemand { municipality: number | Municipality; activity?: (number | null) | Activity; leadership?: (number | null) | Leadership; + /** + * Só responsáveis, candidato e coordenador veem a demanda. O criador entra automaticamente. + */ + responsibles?: (number | CampaignUser)[] | null; status: 'aberta' | 'em_analise' | 'escalada' | 'aprovada' | 'rejeitada'; decisionNote?: string | null; decidedBy?: (number | null) | CampaignUser; @@ -1791,6 +1795,7 @@ export interface CampaignDemandSelect { municipality?: T; activity?: T; leadership?: T; + responsibles?: T; status?: T; decisionNote?: T; decidedBy?: T; @@ -2461,6 +2466,15 @@ export interface SocialFeedSetting { | number | boolean | null; + instagramSyncStatus?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; updatedAt?: string | null; createdAt?: string | null; } @@ -2578,6 +2592,7 @@ export interface SocialFeedSettingsSelect { }; youtubeFeedSnapshot?: T; instagramFeedSnapshot?: T; + instagramSyncStatus?: T; updatedAt?: T; createdAt?: T; globalType?: T; diff --git a/src/utilities/access/demands.ts b/src/utilities/access/demands.ts index 21721860..d1895cfd 100644 --- a/src/utilities/access/demands.ts +++ b/src/utilities/access/demands.ts @@ -2,7 +2,7 @@ // Campaign demands // --------------------------------------------------------------------------- -import type { Access } from 'payload' +import type { Access, Where } from 'payload' import { relationshipId } from '@/lib/relationship' import { getAccessibleMunicipalityIds } from '@/utilities/access/municipalities' @@ -12,7 +12,6 @@ import { isCampaignLeader, isCampaignUnrestricted, isPayloadAdmin, - resolveActorScopedRead, } from '@/utilities/access/shared' export const canCreateCampaignDemand: Access = async ({ data, req }) => { @@ -34,8 +33,24 @@ export const canCreateCampaignDemand: Access = async ({ data, req }) => { return municipalityIDs?.includes(municipalityID) ?? false } -export const canReadCampaignDemand: Access = ({ req }) => - resolveActorScopedRead(req, 'municipality', getAccessibleMunicipalityIds) +/** + * C143 — explicit-responsible visibility, fail-closed. An advisor reads ONLY + * demands where they are a listed responsible (the creator is auto-added on + * create); a municipality-related advisor who is not responsible sees nothing + * — not the list, not the URL, not the search (all reads run the collection + * access). Candidate and coordinator (unrestricted) always see everything. + */ +export const canReadCampaignDemand: Access = async ({ req }): Promise => { + if (isPayloadAdmin(req.user)) return true + + const currentUser = await getFreshCampaignUser(req) + if (!currentUser || isCampaignLeader(currentUser)) return false + if (isCampaignUnrestricted(currentUser)) return true + + if (currentUser.role !== 'advisor') return false + + return { responsibles: { contains: currentUser.id } } +} /** * C141 — demands stay out of the profile axes (C143 replaces this rule with @@ -49,7 +64,7 @@ export const canUpdateCampaignDemand: Access = async ({ req }) => { if (isCampaignUnrestricted(currentUser)) return true if (advisorEditingAccess(currentUser) === 'none') return false - return resolveActorScopedRead(req, 'municipality', getAccessibleMunicipalityIds) + return canReadCampaignDemand({ req }) } export const canDeleteCampaignDemand: Access = ({ req }) => isPayloadAdmin(req.user) diff --git a/src/utilities/campaignDemandData.ts b/src/utilities/campaignDemandData.ts index c75b4fa2..561a829e 100644 --- a/src/utilities/campaignDemandData.ts +++ b/src/utilities/campaignDemandData.ts @@ -13,6 +13,7 @@ import { type DemandListState, } from '@/utilities/demand/demandListUrl' import { + loadCampaignUserDisplayByIds, loadCampaignUserNamesByIds, loadLeadershipContactNamesByIds, loadMunicipalityLabelsByIds, @@ -148,6 +149,10 @@ export type DemandDetailViewModel = DemandRowViewModel & { authorName: string | null createdAt: string | null }> + /** C143 — explicit responsibles (creator included by the collection hook). */ + responsibles: Array<{ id: number; name: string; avatarUrl: string | null }> + /** C143 — the creator id, for the "(criador)" chip marker on the detail. */ + createdBy: number | null canLeaderEdit: boolean activity: { title: string; slug: string } | null } @@ -183,11 +188,21 @@ export const loadDemandDetail = async ( ] const authorNameById = await loadCampaignUserNamesByIds(payload, authorIDs) + const responsibleIDs = (demand.responsibles ?? []) + .map((responsible) => relationshipId(responsible)) + .filter((id): id is number => id !== null) + const responsibleById = await loadCampaignUserDisplayByIds(payload, responsibleIDs) + return { ...row, description: demand.description ?? null, decisionNote: demand.decisionNote ?? null, decidedAt: demand.decidedAt ?? null, + createdBy: relationshipId(demand.createdBy), + responsibles: responsibleIDs.flatMap((id) => { + const display = responsibleById.get(id) + return display ? [{ id, ...display }] : [] + }), cost: isCampaignStaff(user) ? (demand.cost ?? null) : null, receipts: isCampaignStaff(user) ? (demand.receipts ?? []).flatMap((receipt) => diff --git a/src/utilities/demand/campaignDemandFormData.ts b/src/utilities/demand/campaignDemandFormData.ts index 54355ea6..1084a354 100644 --- a/src/utilities/demand/campaignDemandFormData.ts +++ b/src/utilities/demand/campaignDemandFormData.ts @@ -1,5 +1,6 @@ import { nullableRelationshipFormValue, + repeatedRelationshipFormValues, requiredFormText, requiredRelationshipFormValue, } from '@/lib/formData' @@ -22,5 +23,6 @@ export const parseCampaignDemandCreateFormData = ( description: requiredFormText(formData, 'description'), municipality: requiredRelationshipFormValue(formData, 'municipalityId'), activity: nullableRelationshipFormValue(formData, 'activityId') ?? undefined, + responsibles: repeatedRelationshipFormValues(formData, 'responsibles'), } } diff --git a/src/utilities/socialFeed/instagramFeed.ts b/src/utilities/socialFeed/instagramFeed.ts index 4683f469..13af5afe 100644 --- a/src/utilities/socialFeed/instagramFeed.ts +++ b/src/utilities/socialFeed/instagramFeed.ts @@ -1,11 +1,8 @@ import 'server-only' import type { SocialFeedSetting } from '@/payload-types' -import { REVALIDATE_SOCIAL_FEED_TAG } from '@/utilities/revalidateRequest' -import configPromise from '@payload-config' import { sql } from '@payloadcms/db-postgres' -import { unstable_cache } from 'next/cache' -import { getPayload, type Payload } from 'payload' +import type { Payload } from 'payload' const INSTAGRAM_API_BASE_URL = process.env.INSTAGRAM_API_BASE_URL ?? 'https://graph.instagram.com' @@ -33,6 +30,8 @@ export type LoadInstagramFeedArgs = { maxResults: number fetchImpl?: (input: string, init?: RequestInit) => Promise baseUrl?: string + /** Deadline for the Graph API calls (hook-triggered sync must not hang a save). */ + signal?: AbortSignal } export type LoadInstagramFeedResult = InstagramFeedResult & { @@ -40,6 +39,57 @@ export type LoadInstagramFeedResult = InstagramFeedResult & { refreshedAccessToken?: string } +/** + * API-level failure of the Instagram Graph API: carries the HTTP status and + * the parsed `{ error: { message, type } }` body when present, so + * `describeInstagramError` can turn it into product language (invalid token + * vs wrong user id vs API down) for the admin sync-status panel. + */ +export class InstagramApiError extends Error { + readonly status: number | undefined + readonly apiMessage: string | null + readonly apiType: string | null + + constructor( + status: number | undefined, + apiMessage: string | null, + apiType: string | null, + message: string, + ) { + super(message) + this.name = 'InstagramApiError' + this.status = status + this.apiMessage = apiMessage + this.apiType = apiType + } +} + +const parseApiErrorBody = async ( + response: Response, +): Promise<{ message: string | null; type: string | null }> => { + try { + const body = (await response.json()) as { error?: { message?: unknown; type?: unknown } } | null + const error = body?.error + if (typeof error !== 'object' || error === null) return { message: null, type: null } + return { + message: typeof error.message === 'string' && error.message ? error.message : null, + type: typeof error.type === 'string' && error.type ? error.type : null, + } + } catch { + return { message: null, type: null } + } +} + +const apiErrorFrom = async (response: Response): Promise => { + const { message, type } = await parseApiErrorBody(response) + return new InstagramApiError( + response.status, + message, + type, + `Instagram API falhou com status ${response.status}`, + ) +} + const MEDIA_FIELDS = [ 'id', 'caption', @@ -156,12 +206,13 @@ export const loadInstagramFeed = async ({ maxResults, fetchImpl = fetch, baseUrl = INSTAGRAM_API_BASE_URL, + signal, }: LoadInstagramFeedArgs): Promise => { const attempt = async (token: string): Promise => { const userParams = new URLSearchParams({ fields: 'username', access_token: token }) - const userResponse = await fetchImpl(`${baseUrl}/${userId}?${userParams}`) + const userResponse = await fetchImpl(`${baseUrl}/${userId}?${userParams}`, { signal }) if (!userResponse.ok) { - throw new Error(`Instagram user falhou com status ${userResponse.status}`) + throw await apiErrorFrom(userResponse) } const userJson = (await jsonFrom(userResponse)) as { username?: unknown } const username = typeof userJson.username === 'string' ? userJson.username : null @@ -171,9 +222,9 @@ export const loadInstagramFeed = async ({ limit: String(Math.min(Math.max(maxResults, 1), INSTAGRAM_MAX_RESULTS_CAP)), access_token: token, }) - const mediaResponse = await fetchImpl(`${baseUrl}/${userId}/media?${mediaParams}`) + const mediaResponse = await fetchImpl(`${baseUrl}/${userId}/media?${mediaParams}`, { signal }) if (!mediaResponse.ok) { - throw new Error(`Instagram media falhou com status ${mediaResponse.status}`) + throw await apiErrorFrom(mediaResponse) } return { username, posts: parseInstagramMediaResponse(await jsonFrom(mediaResponse)) } @@ -186,9 +237,11 @@ export const loadInstagramFeed = async ({ grant_type: 'ig_refresh_token', access_token: accessToken, }) - const refreshResponse = await fetchImpl(`${baseUrl}/refresh_access_token?${refreshParams}`) + const refreshResponse = await fetchImpl(`${baseUrl}/refresh_access_token?${refreshParams}`, { + signal, + }) if (!refreshResponse.ok) { - throw new Error(`Instagram token refresh falhou com status ${refreshResponse.status}`) + throw await apiErrorFrom(refreshResponse) } const refreshJson = (await jsonFrom(refreshResponse)) as { access_token?: unknown } if (typeof refreshJson.access_token !== 'string' || !refreshJson.access_token) { @@ -201,42 +254,153 @@ export const loadInstagramFeed = async ({ } } -const excludedInstagramIds = (settings: SocialFeedSetting): string[] => - (settings.excludedItems ?? []) - .filter((item) => item.platform === 'instagram') - .map((item) => item.itemId) - .filter(Boolean) +/** + * Operational state of the Instagram sync, persisted in the global's + * `instagram_sync_status` column and shown by the admin status panel: + * `lastSyncAt`/`postCount` are the last SUCCESSFUL sync; `error`/`errorAt` + * the last failure. A failure overwrites the success fields on purpose — the + * panel shows either state, never both (draft cenas 1/2). + */ +export type InstagramSyncStatus = { + lastSyncAt?: string + postCount?: number + error?: string + errorAt?: string +} -const isInstagramSnapshot = (value: unknown): value is { username?: unknown; posts?: unknown } => - typeof value === 'object' && value !== null +export const successInstagramSyncStatus = (postCount: number): InstagramSyncStatus => ({ + lastSyncAt: new Date().toISOString(), + postCount, +}) + +export const failedInstagramSyncStatus = (error: string): InstagramSyncStatus => ({ + error, + errorAt: new Date().toISOString(), +}) + +const isTokenError = (error: InstagramApiError): boolean => + error.apiType === 'OAuthException' || + /(expired|invalid|revoked|token|session)/i.test(error.apiMessage ?? '') + +/** + * True when the API message points at a bad user id rather than a bad token. + * Requires "user" AND "id" together (or an explicit "object with id ... does + * not exist") — a single "user" alone is NOT enough, because real token + * rejections read "the user must be logged in" / "the user has changed their + * password", and those must stay on the token correction, not the ID one. + */ +const isInvalidUserIdMessage = (message: string): boolean => + /(?=.*\buser\b)(?=.*\bid\b)/i.test(message) || + /\bid\b[\s\S]*\b(?:does not exist|cannot find)\b/i.test(message) + +const isAbortError = (cause: unknown): boolean => + cause instanceof Error && cause.name === 'AbortError' + +/** + * Turns a sync failure into product language for the admin status panel: the + * assessoria must know WHY the board has no Instagram cards and what to do + * (the S3 fail-closed silence is the bug this exists to fix). Token errors + * carry the correction — regenerate via Instagram Login, never Facebook + * Login (the Graph API refresh endpoint rejects FB-issued page tokens). + */ +export const describeInstagramError = (cause: unknown): string => { + if (isAbortError(cause)) { + return 'A sincronização demorou demais para responder (a API do Instagram não respondeu a tempo). Tente novamente.' + } + if (cause instanceof InstagramApiError) { + if (cause.status === 400 && isInvalidUserIdMessage(cause.apiMessage ?? '')) { + return 'O ID do usuário não foi reconhecido pela Graph API. Confira se é o ID numérico da conta Business/Creator.' + } + if (typeof cause.status === 'number' && cause.status >= 400 && cause.status < 500) { + if (isTokenError(cause)) { + return 'O token do Instagram foi recusado pela Graph API — está inválido/expirado ou foi emitido via Facebook Login (a Graph API só aceita tokens gerados pelo Instagram Login). Gere um novo token de longa duração pelo Instagram Login e atualize o campo acima.' + } + return 'A Graph API recusou a solicitação. Confira o token e o ID do usuário configurados.' + } + return 'A Graph API está indisponível no momento. Tente novamente em alguns minutos.' + } + if (cause instanceof TypeError) { + return 'Não foi possível falar com a API do Instagram (rede indisponível).' + } + return 'Erro inesperado ao sincronizar o Instagram.' +} + +/** + * Database seam for the raw-SQL persists. The default is the pool + * (`payload.db.drizzle`); the hook-triggered sync passes the transaction-bound + * database (`getPostgresTransactionDatabase`, S11) so the write lands inside + * the global save's transaction — writing the same row from a second pool + * connection while the save holds its row lock deadlocks on that lock. + */ +export type InstagramPersistDatabase = { + execute: (query: ReturnType) => Promise +} + +const countAffected = (result: unknown): number | null | undefined => + (result as { rowCount?: number | null } | undefined)?.rowCount + +/** + * Persists the sync status the same way as the snapshot — deliberately raw + * SQL instead of `payload.updateGlobal`: this runs INSIDE `unstable_cache` + * during a page render, and Next 15.4 throws when `revalidateTag` (the + * global's `afterChange` hook) fires inside a cached function. Same + * precondition as the snapshot: at most one `social_feed_settings` row + * exists; the INSERT only fires when none does. + */ +export const persistInstagramSyncStatus = async ( + payload: Payload, + status: InstagramSyncStatus, + database: InstagramPersistDatabase | null = null, +): Promise => { + const db = database ?? payload.db.drizzle + const json = JSON.stringify(status) + const updated = await db.execute(sql` + UPDATE "social_feed_settings" + SET "instagram_sync_status" = ${json}::jsonb, "updated_at" = now() + WHERE "id" = (SELECT "id" FROM "social_feed_settings" ORDER BY "id" LIMIT 1) + `) + if (!countAffected(updated)) { + await db.execute(sql` + INSERT INTO "social_feed_settings" ("instagram_sync_status", "created_at", "updated_at") + VALUES (${json}::jsonb, now(), now()) + `) + } +} + +/** True when the Instagram feed is armed to call the Graph API. */ +export const isInstagramFeedConfigured = (settings: SocialFeedSetting): boolean => + settings.enabled !== false && + settings.instagramEnabled !== false && + Boolean(settings.instagramAccessToken) && + Boolean(settings.instagramUserId) /** * Persists the raw feed as the global's snapshot column. Deliberately raw SQL - * (`payload.db.drizzle`) instead of `payload.updateGlobal`: this runs INSIDE - * `unstable_cache` during a page render, and Next 15.4 throws when - * `revalidateTag` (the global's `afterChange` hook) fires inside a cached - * function. Raw SQL bypasses hooks; the row is created on first write when the - * admin has not saved the global yet (a serial id is assigned, so no id - * assumption is made). + * instead of `payload.updateGlobal`: this runs INSIDE `unstable_cache` during + * a page render, and Next 15.4 throws when `revalidateTag` (the global's + * `afterChange` hook) fires inside a cached function. Raw SQL bypasses hooks; + * the row is created on first write when the admin has not saved the global + * yet (a serial id is assigned, so no id assumption is made). * * Precondition: at most one `social_feed_settings` row exists. The UPDATE * targets the single row and the INSERT only fires when none exists; a * concurrent double-insert would need two writers racing the very first write * (the row is created by the first admin save or feed render) — accepted. */ -const persistInstagramSnapshot = async ( +export const persistInstagramSnapshot = async ( payload: Payload, snapshot: InstagramFeedResult, + database: InstagramPersistDatabase | null = null, ): Promise => { - const database = payload.db.drizzle + const db = database ?? payload.db.drizzle const json = JSON.stringify(snapshot) - const updated = await database.execute(sql` + const updated = await db.execute(sql` UPDATE "social_feed_settings" SET "instagram_feed_snapshot" = ${json}::jsonb, "updated_at" = now() WHERE "id" = (SELECT "id" FROM "social_feed_settings" ORDER BY "id" LIMIT 1) `) - if (!updated?.rowCount) { - await database.execute(sql` + if (!countAffected(updated)) { + await db.execute(sql` INSERT INTO "social_feed_settings" ("instagram_feed_snapshot", "created_at", "updated_at") VALUES (${json}::jsonb, now(), now()) `) @@ -244,69 +408,15 @@ const persistInstagramSnapshot = async ( } /** Persists a refreshed token the same way as the snapshot (raw SQL, no hooks). */ -const persistInstagramAccessToken = async (payload: Payload, token: string): Promise => { - await payload.db.drizzle.execute(sql` +export const persistInstagramAccessToken = async ( + payload: Payload, + token: string, + database: InstagramPersistDatabase | null = null, +): Promise => { + const db = database ?? payload.db.drizzle + await db.execute(sql` UPDATE "social_feed_settings" SET "instagram_access_token" = ${token}, "updated_at" = now() WHERE "id" = (SELECT "id" FROM "social_feed_settings" ORDER BY "id" LIMIT 1) `) } - -/** - * Cached Instagram feed for the campaign home content board: `null` when the - * feed is off (kill switch, platform toggle or missing credentials), otherwise - * the latest eligible posts plus the profile username. A successful fetch - * persists the RAW list (pre-exclusion) as the global snapshot; a failed fetch - * falls back to that snapshot with the current exclusions applied — so a new - * exclusion works even while the API is down, and the page never breaks. - * Busted by the global's `afterChange` (tag `social-feed`); the per-entry - * `revalidate` keeps new uploads appearing without an admin edit. - */ -export const getInstagramFeed = unstable_cache( - async (): Promise => { - const payload = await getPayload({ config: configPromise }) - const settings = await payload.findGlobal({ - slug: 'social-feed-settings', - depth: 0, - }) - - if ( - settings.enabled === false || - settings.instagramEnabled === false || - !settings.instagramAccessToken || - !settings.instagramUserId - ) { - return null - } - - const maxItems = Math.min(Math.max(settings.instagramMaxItems ?? 3, 1), 5) - const excludedIds = excludedInstagramIds(settings) - - try { - const result = await loadInstagramFeed({ - accessToken: settings.instagramAccessToken, - userId: settings.instagramUserId, - maxResults: Math.min(maxItems + 10, INSTAGRAM_MAX_RESULTS_CAP), - }) - await persistInstagramSnapshot(payload, { username: result.username, posts: result.posts }) - if (result.refreshedAccessToken) { - await persistInstagramAccessToken(payload, result.refreshedAccessToken) - } - return { - username: result.username, - posts: eligibleInstagramPosts(result.posts, excludedIds, maxItems), - } - } catch { - const snapshot = settings.instagramFeedSnapshot as unknown - if (!isInstagramSnapshot(snapshot) || !Array.isArray(snapshot.posts)) { - return { username: null, posts: [] } - } - return { - username: typeof snapshot.username === 'string' ? snapshot.username : null, - posts: eligibleInstagramPosts(snapshot.posts, excludedIds, maxItems), - } - } - }, - ['instagram-feed'], - { tags: [REVALIDATE_SOCIAL_FEED_TAG], revalidate: 300 }, -) diff --git a/src/utilities/socialFeed/instagramFeedView.ts b/src/utilities/socialFeed/instagramFeedView.ts new file mode 100644 index 00000000..f950fc56 --- /dev/null +++ b/src/utilities/socialFeed/instagramFeedView.ts @@ -0,0 +1,97 @@ +import 'server-only' + +import type { SocialFeedSetting } from '@/payload-types' +import { REVALIDATE_SOCIAL_FEED_TAG } from '@/utilities/revalidateRequest' +import configPromise from '@payload-config' +import { unstable_cache } from 'next/cache' +import { getPayload } from 'payload' + +import { + describeInstagramError, + eligibleInstagramPosts, + failedInstagramSyncStatus, + INSTAGRAM_MAX_RESULTS_CAP, + type InstagramFeedResult, + isInstagramFeedConfigured, + loadInstagramFeed, + persistInstagramAccessToken, + persistInstagramSnapshot, + persistInstagramSyncStatus, + successInstagramSyncStatus, +} from '@/utilities/socialFeed/instagramFeed' + +const excludedInstagramIds = (settings: SocialFeedSetting): string[] => + (settings.excludedItems ?? []) + .filter((item) => item.platform === 'instagram') + .map((item) => item.itemId) + .filter(Boolean) + +const isInstagramSnapshot = (value: unknown): value is { username?: unknown; posts?: unknown } => + typeof value === 'object' && value !== null + +/** + * Cached Instagram feed for the campaign home content board: `null` when the + * feed is off (kill switch, platform toggle or missing credentials), otherwise + * the latest eligible posts plus the profile username. A successful fetch + * persists the RAW list (pre-exclusion) as the global snapshot AND the sync + * status (so the admin panel shows "Sincronizado · há X min · N posts"); a + * failed fetch persists the failure status with the product-language reason + * and falls back to the snapshot with the current exclusions applied — so a + * new exclusion works even while the API is down, and the page never breaks. + * Busted by the global's `afterChange` (tag `social-feed`); the per-entry + * `revalidate` keeps new uploads appearing without an admin edit. + * + * Lives in `instagramFeedView.ts` (NOT `instagramFeed.ts`) on purpose: this + * module is the only one that imports `@payload-config`, while + * `instagramFeed.ts` is reachable from `payload.config.ts` via the global's + * sync hook — importing the config back from the feed module would close a + * cycle the `check:cycles` gate rejects. + */ +export const getInstagramFeed = unstable_cache( + async (): Promise => { + const payload = await getPayload({ config: configPromise }) + const settings = await payload.findGlobal({ + slug: 'social-feed-settings', + depth: 0, + }) + + if (!isInstagramFeedConfigured(settings)) { + return null + } + + const maxItems = Math.min(Math.max(settings.instagramMaxItems ?? 3, 1), 5) + const excludedIds = excludedInstagramIds(settings) + + try { + const result = await loadInstagramFeed({ + accessToken: settings.instagramAccessToken as string, + userId: settings.instagramUserId as string, + maxResults: Math.min(maxItems + 10, INSTAGRAM_MAX_RESULTS_CAP), + }) + await persistInstagramSnapshot(payload, { username: result.username, posts: result.posts }) + if (result.refreshedAccessToken) { + await persistInstagramAccessToken(payload, result.refreshedAccessToken) + } + await persistInstagramSyncStatus(payload, successInstagramSyncStatus(result.posts.length)) + return { + username: result.username, + posts: eligibleInstagramPosts(result.posts, excludedIds, maxItems), + } + } catch (cause) { + await persistInstagramSyncStatus( + payload, + failedInstagramSyncStatus(describeInstagramError(cause)), + ) + const snapshot = settings.instagramFeedSnapshot as unknown + if (!isInstagramSnapshot(snapshot) || !Array.isArray(snapshot.posts)) { + return { username: null, posts: [] } + } + return { + username: typeof snapshot.username === 'string' ? snapshot.username : null, + posts: eligibleInstagramPosts(snapshot.posts, excludedIds, maxItems), + } + } + }, + ['instagram-feed'], + { tags: [REVALIDATE_SOCIAL_FEED_TAG], revalidate: 300 }, +) diff --git a/src/utilities/socialFeed/instagramSync.ts b/src/utilities/socialFeed/instagramSync.ts new file mode 100644 index 00000000..a319dd42 --- /dev/null +++ b/src/utilities/socialFeed/instagramSync.ts @@ -0,0 +1,117 @@ +import 'server-only' + +import type { SocialFeedSetting } from '@/payload-types' +import { getPostgresTransactionDatabase } from '@/utilities/postgresTransactionLocks' +import { + describeInstagramError, + failedInstagramSyncStatus, + INSTAGRAM_MAX_RESULTS_CAP, + isInstagramFeedConfigured, + loadInstagramFeed, + persistInstagramAccessToken, + persistInstagramSnapshot, + persistInstagramSyncStatus, + successInstagramSyncStatus, + type InstagramPersistDatabase, + type InstagramSyncStatus, +} from '@/utilities/socialFeed/instagramFeed' +import type { Payload, PayloadRequest } from 'payload' + +/** Deadline for a sync triggered by the admin (hook or button) — a hanging + * Graph API must never stall a global save. */ +export const INSTAGRAM_SYNC_TIMEOUT_MS = 10_000 + +export type InstagramSyncOutcome = { + ok: boolean + status: InstagramSyncStatus +} + +/** + * True when a save of the global must re-sync Instagram: the credentials + * changed (the primary flow — "fill token + ID, save, see the state") or the + * feed was re-enabled. Exclusion-only saves skip the sync on purpose: they + * change no credential and would burn an API call for nothing. + */ +export const instagramCredentialsChanged = ( + doc: SocialFeedSetting, + previousDoc: SocialFeedSetting | null | undefined, +): boolean => { + if (!previousDoc) return true + const credentialsChanged = + (doc.instagramAccessToken ?? '') !== (previousDoc.instagramAccessToken ?? '') || + (doc.instagramUserId ?? '') !== (previousDoc.instagramUserId ?? '') + const reenabled = doc.instagramEnabled === true && previousDoc.instagramEnabled !== true + return credentialsChanged || reenabled +} + +/** + * The afterChange hook runs INSIDE the global save's transaction (the row lock + * is held until it returns), so a write to the same row from a second pool + * connection would block on that lock. When a `req` with a `transactionID` is + * present (hook path), the persists go through the transaction-bound database + * instead — the status/snapshot land atomically with the save, no deadlock. + * The render path and the sync route have no open transaction and keep the + * pool (`payload.db.drizzle`). + */ +const resolvePersistDatabase = async ( + payload: Payload, + req?: PayloadRequest, +): Promise => { + if (!req?.transactionID) return null + try { + return await getPostgresTransactionDatabase(payload, req) + } catch { + // No transaction adapter available (e.g. non-postgres in tests) — pool. + return null + } +} + +/** + * Runs one Instagram sync from the persisted settings: fetches the Graph API, + * persists the raw snapshot + a refreshed token when one was minted, and + * writes the outcome status. Never throws — the status IS the result (a + * failure writes the product-language reason the admin panel shows). Shared + * by the global's `afterChange` hook (on credential change) and the + * `POST /api/social-feed/sync` route (the "Tentar sincronizar de novo" + * button) — one implementation, two call sites. + */ +export const syncInstagramFeed = async ( + payload: Payload, + options: { signal?: AbortSignal; req?: PayloadRequest } = {}, +): Promise => { + const settings = await payload.findGlobal({ + slug: 'social-feed-settings', + depth: 0, + }) + + if (!isInstagramFeedConfigured(settings)) { + return { ok: false, status: {} } + } + + const maxItems = Math.min(Math.max(settings.instagramMaxItems ?? 3, 1), 5) + const database = await resolvePersistDatabase(payload, options.req) + + try { + const result = await loadInstagramFeed({ + accessToken: settings.instagramAccessToken as string, + userId: settings.instagramUserId as string, + maxResults: Math.min(maxItems + 10, INSTAGRAM_MAX_RESULTS_CAP), + signal: options.signal, + }) + await persistInstagramSnapshot( + payload, + { username: result.username, posts: result.posts }, + database, + ) + if (result.refreshedAccessToken) { + await persistInstagramAccessToken(payload, result.refreshedAccessToken, database) + } + const status = successInstagramSyncStatus(result.posts.length) + await persistInstagramSyncStatus(payload, status, database) + return { ok: true, status } + } catch (cause) { + const status = failedInstagramSyncStatus(describeInstagramError(cause)) + await persistInstagramSyncStatus(payload, status, database) + return { ok: false, status } + } +} diff --git a/tests/e2e/admin.e2e.spec.ts b/tests/e2e/admin.e2e.spec.ts index 6413f3b5..eeb06e7b 100644 --- a/tests/e2e/admin.e2e.spec.ts +++ b/tests/e2e/admin.e2e.spec.ts @@ -1,5 +1,6 @@ import { login } from '../helpers/login' import { cleanupTestUser, seedTestUser, testUser } from '../helpers/seedUser' +import { instagramStubUrlFor } from '../helpers/socialStub' import { expect, test } from './fixtures/e2eTest' const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? 'http://localhost:3000' @@ -127,6 +128,140 @@ test.describe('Admin Panel', () => { excludedItems: [], youtubeFeedSnapshot: null, instagramFeedSnapshot: null, + instagramSyncStatus: null, + }, + }) + .catch(() => undefined) + } + }) + + test('shows the Instagram sync status and retries the sync from the panel', async ({ + page, + request, + }) => { + // The seed POST fires the global's afterChange hook, whose sync hits the + // stub — put it in a known state first and let the hook persist the + // status so the panel has a real one to render. + const stubUrl = instagramStubUrlFor(baseURL) + const setStub = async (state: 'ok' | 'fail' | 'invalid-token') => { + const response = await request.post(`${stubUrl}/__stub/state`, { data: { state } }) + expect(response.ok()).toBeTruthy() + } + await setStub('ok') + + const login = await request.post(`${baseURL}/api/users/login`, { + data: { email: testUser.email, password: testUser.password }, + }) + expect(login.ok()).toBeTruthy() + const { token } = await login.json() + const headers = { cookie: `payload-token=${token}` } + + const seed = await request.post(`${baseURL}/api/globals/social-feed-settings`, { + headers, + data: { + enabled: true, + instagramEnabled: true, + instagramAccessToken: 'e2e-ig-token', + instagramUserId: '17841400000000000', + instagramMaxItems: 3, + excludedItems: [], + instagramFeedSnapshot: null, + }, + }) + expect(seed.ok()).toBeTruthy() + + const seedToken = async (accessToken: string) => { + const response = await request.post(`${baseURL}/api/globals/social-feed-settings`, { + headers, + data: { + enabled: true, + instagramEnabled: true, + instagramAccessToken: accessToken, + instagramUserId: '17841400000000000', + instagramMaxItems: 3, + excludedItems: [], + instagramFeedSnapshot: null, + }, + }) + expect(response.ok()).toBeTruthy() + } + + try { + await page.goto(`${baseURL}/admin/globals/social-feed-settings`) + await expect(page.getByText(/Sincronizado · .* · 5 posts/)).toBeVisible() + + // Credential change + save triggers the hook sync — with the API now + // rejecting the token (OAuthException), the save persists a failure + // status and the reload shows it with the retry button. + await setStub('invalid-token') + await seedToken('e2e-ig-token-2') + await page.goto(`${baseURL}/admin/globals/social-feed-settings`) + await expect(page.getByText('Falha na última sincronização')).toBeVisible() + await expect(page.getByText(/token do Instagram foi recusado/)).toBeVisible() + + // Retry after fixing: the button runs the sync route → synchronized. + await setStub('ok') + await page.getByRole('button', { name: 'Tentar sincronizar de novo' }).click() + await expect(page.getByText(/Sincronizado · .* · 5 posts/)).toBeVisible() + + const read = await request.get(`${baseURL}/api/globals/social-feed-settings`, { headers }) + expect(read.ok()).toBeTruthy() + const { instagramSyncStatus } = await read.json() + expect(instagramSyncStatus).toEqual( + expect.objectContaining({ + lastSyncAt: expect.any(String), + postCount: 5, + }), + ) + expect(instagramSyncStatus.error).toBeUndefined() + + // Fail-500 scene: a server-side API failure maps to the generic notice. + await setStub('fail') + await seedToken('e2e-ig-token-3') + await page.goto(`${baseURL}/admin/globals/social-feed-settings`) + await expect(page.getByText('Falha na última sincronização')).toBeVisible() + await expect(page.getByText(/indisponível/)).toBeVisible() + + // Disabled scene: the master kill switch off renders the disabled box. + await setStub('ok') + await request.post(`${baseURL}/api/globals/social-feed-settings`, { + headers, + data: { + enabled: false, + instagramEnabled: true, + instagramAccessToken: 'e2e-ig-token', + instagramUserId: '17841400000000000', + instagramMaxItems: 3, + excludedItems: [], + instagramFeedSnapshot: null, + }, + }) + await page.goto(`${baseURL}/admin/globals/social-feed-settings`) + await expect(page.getByText('Instagram desativado')).toBeVisible() + + // Unconfigured state: no credentials (feed enabled) → the panel says so. + await seedToken('') + await page.goto(`${baseURL}/admin/globals/social-feed-settings`) + await expect(page.getByText('Instagram ainda não configurado')).toBeVisible() + } finally { + await setStub('ok').catch(() => undefined) + await request + .post(`${baseURL}/api/globals/social-feed-settings`, { + headers, + data: { + enabled: true, + youtubeEnabled: true, + youtubeApiKey: '', + youtubeChannelId: '', + youtubeMaxItems: 3, + instagramEnabled: true, + instagramAccessToken: '', + instagramUserId: '', + instagramMaxItems: 3, + excludedItems: [], + youtubeFeedSnapshot: null, + instagramFeedSnapshot: null, + instagramSyncStatus: null, }, }) .catch(() => undefined) diff --git a/tests/e2e/campaignDemandVisibility.e2e.spec.ts b/tests/e2e/campaignDemandVisibility.e2e.spec.ts new file mode 100644 index 00000000..8670238c --- /dev/null +++ b/tests/e2e/campaignDemandVisibility.e2e.spec.ts @@ -0,0 +1,102 @@ +import { + campaignPageChrome, + expect, + test, + type CampaignE2EFixture, +} from './fixtures/campaignE2EFixtures.js' + +test.describe('C143 — demand visibility (explicit responsibles)', () => { + const setupDemand = async ( + campaign: CampaignE2EFixture, + fixtures: CampaignE2EFixture['fixtures'], + ) => { + const municipality = await fixtures.claimMunicipality() + const coordinator = await fixtures.createCampaignUser('coordinator', { + name: fixtures.value('Coordenadora C143'), + }) + const responsible = await fixtures.createCampaignUser('advisor', { + name: fixtures.value('Assessora Responsável'), + }) + const peer = await fixtures.createCampaignUser('advisor', { + name: fixtures.value('Assessor Colega'), + }) + await campaign.payload.update({ + collection: 'municipality', + id: municipality.id, + data: { advisors: [responsible.id, peer.id] }, + depth: 0, + }) + + const demand = await fixtures.createCampaignDemand({ + municipality: municipality.id, + createdBy: coordinator.id, + responsibles: [responsible.id], + title: fixtures.value('Demanda de acesso restrito'), + }) + + return { municipality, coordinator, responsible, peer, demand } + } + + test('hides the demand from a same-municipality advisor who is not responsible', async ({ + campaign, + context, + page, + }) => { + const { fixtures } = campaign + const { peer, demand } = await setupDemand(campaign, fixtures) + + await campaign.sessionFor(context, peer) + await page.goto(`${campaign.baseURL}/campanha/demandas`) + // The list rendered for him: the empty state proves the page settled + // (and that the demand is absent — the link assertion below is the + // fail-closed complement). + await expect(page.getByText('Nenhuma demanda por aqui')).toBeVisible() + await expect(page.getByRole('link', { name: demand.title })).toHaveCount(0) + + // The URL does not open either — the demand simply does not exist for him + // (the default 404 boundary is the root one, no campaign-specific page). + await page.goto(`${campaign.baseURL}/campanha/demandas/${demand.slug}`) + await expect(page.getByText('This page could not be found.')).toBeVisible() + }) + + test('lets a responsible hand access to a peer and revoke it again', async ({ + campaign, + context, + page, + }) => { + const { fixtures } = campaign + const { peer, responsible, demand } = await setupDemand(campaign, fixtures) + const demandURL = `${campaign.baseURL}/campanha/demandas/${demand.slug}` + + // The responsible manages the list: add the peer through the search dialog. + await campaign.sessionFor(context, responsible) + await page.goto(demandURL) + await expect(page.getByText('Só quem é responsável vê esta demanda.')).toBeVisible() + + // The trigger announces the current selection (e.g. "…: 1 responsável"). + await page.getByRole('button', { name: /Responsáveis pela demanda/ }).click() + await page.getByPlaceholder('Buscar assessor…').fill(peer.name) + await page.getByRole('option', { name: peer.name }).click() + // The multi-add dialog stays open (modal) — close it before saving. + await page.keyboard.press('Escape') + await page.getByRole('button', { name: 'Salvar responsáveis' }).click() + await expect(page.getByText('Responsáveis atualizados.')).toBeVisible() + await expect(page.getByRole('button', { name: `Remover ${peer.name}` })).toBeVisible() + + // The peer now opens the demand. + await campaign.sessionFor(context, peer) + await page.goto(demandURL) + await expect(campaignPageChrome(page, demand.title)).toBeVisible() + + // Back as the responsible, remove the peer and revoke the access again. + await campaign.sessionFor(context, responsible) + await page.goto(demandURL) + await page.getByRole('button', { name: `Remover ${peer.name}` }).click() + await page.getByRole('button', { name: 'Salvar responsáveis' }).click() + await expect(page.getByRole('button', { name: `Remover ${peer.name}` })).toHaveCount(0) + + await campaign.sessionFor(context, peer) + await page.goto(demandURL) + await expect(page.getByText('This page could not be found.')).toBeVisible() + }) +}) diff --git a/tests/e2e/fixtures/campaignE2EFixtures.ts b/tests/e2e/fixtures/campaignE2EFixtures.ts index c1472ed5..54daf5c9 100644 --- a/tests/e2e/fixtures/campaignE2EFixtures.ts +++ b/tests/e2e/fixtures/campaignE2EFixtures.ts @@ -220,6 +220,32 @@ export class CampaignE2EOwnership { return { contactName, contactId: contact.id, leadershipId: leadership.id } } + /** + * C143 — demand factory with explicit ownership: the row is registered for + * cleanup and the municipality touched (mirror of the int fixture helper). + */ + async createCampaignDemand(input: { + municipality: number + title: string + createdBy?: number + responsibles?: number[] + }): Promise<{ id: number; slug: string; title: string }> { + const demand = await this.payload.create({ + collection: 'campaignDemand', + data: { + kind: 'material', + slug: this.value('demanda'), + status: 'aberta', + ...input, + }, + depth: 0, + draft: false, + }) + this.own('campaignDemand', demand.id) + this.touchMunicipality(input.municipality) + return demand + } + private isOwnedCollection(collection: CollectionSlug): collection is OwnedCollection { return this.owned.has(collection as OwnedCollection) } diff --git a/tests/e2e/frontend.e2e.spec.ts b/tests/e2e/frontend.e2e.spec.ts index 92384f73..b0e7fd27 100644 --- a/tests/e2e/frontend.e2e.spec.ts +++ b/tests/e2e/frontend.e2e.spec.ts @@ -747,6 +747,7 @@ test.describe('Campaign home content section', () => { excludedItems: [], youtubeFeedSnapshot: null, instagramFeedSnapshot: null, + instagramSyncStatus: null, }) } diff --git a/tests/e2e/instagram-stub.mjs b/tests/e2e/instagram-stub.mjs index a0c6d023..b083778f 100644 --- a/tests/e2e/instagram-stub.mjs +++ b/tests/e2e/instagram-stub.mjs @@ -3,11 +3,14 @@ * board (S3) fetches the feed server-side, so the Next dev/prod server points * `INSTAGRAM_API_BASE_URL` here (see playwright.config.ts) and this process * answers `/media`, `/user` and `/refresh_access_token` with a fixed fixture. - * `POST /__stub/state` with `{ "state": "ok" | "fail" }` makes the API fail - * (HTTP 500) so specs can exercise the fail-closed/snapshot paths without any - * network. Thumbnails are served locally from `/thumbs/.jpg` — the - * sandbox never reaches `scontent.cdninstagram.com`, and next/image would 404 - * (a console error the e2e guard treats as a failure). + * `POST /__stub/state` with `{ "state": "ok" | "fail" | "invalid-token" }` + * switches the API: `fail` answers HTTP 500 (network/API-down paths), and + * `invalid-token` answers 400 with an OAuthException body (the S11 admin + * status panel's token-rejected copy) — so specs can exercise the + * fail-closed/snapshot paths without any network. Thumbnails are served + * locally from `/thumbs/.jpg` — the sandbox never reaches + * `scontent.cdninstagram.com`, and next/image would 404 (a console error the + * e2e guard treats as a failure). */ import { createServer } from 'node:http' @@ -99,7 +102,7 @@ const server = createServer((req, res) => { req.on('end', () => { try { const parsed = JSON.parse(body) - if (parsed.state === 'ok' || parsed.state === 'fail') state = parsed.state + if (['ok', 'fail', 'invalid-token'].includes(parsed.state)) state = parsed.state } catch { // keep the current state } @@ -108,6 +111,19 @@ const server = createServer((req, res) => { return } + if (state === 'invalid-token') { + // The token-rejected shape the Graph API returns for an invalid/expired + // token or one minted via Facebook Login (S11 status panel copy). + json(res, 400, { + error: { + message: 'Error validating access token: Session has expired', + type: 'OAuthException', + code: 190, + }, + }) + return + } + if (state === 'fail') { res.writeHead(500) res.end('stub fail') diff --git a/tests/helpers/campaignFixtures.ts b/tests/helpers/campaignFixtures.ts index 3267f38a..7c2cf6c9 100644 --- a/tests/helpers/campaignFixtures.ts +++ b/tests/helpers/campaignFixtures.ts @@ -109,7 +109,14 @@ type StateDeputyInput = Partial< type CampaignDemandInput = Partial< Pick< CampaignDemand, - 'title' | 'slug' | 'kind' | 'description' | 'status' | 'leadership' | 'createdBy' + | 'title' + | 'slug' + | 'kind' + | 'description' + | 'status' + | 'leadership' + | 'createdBy' + | 'responsibles' > > & Pick @@ -662,6 +669,7 @@ export class CampaignFixtures { municipality: relationId(input.municipality), ...(input.leadership ? { leadership: relationId(input.leadership) } : {}), ...(input.createdBy ? { createdBy: relationId(input.createdBy) } : {}), + ...(input.responsibles ? { responsibles: input.responsibles } : {}), }, depth: 0, }) diff --git a/tests/int/campaignDemandWorkflow.int.spec.ts b/tests/int/campaignDemandWorkflow.int.spec.ts index f851ed72..0be89bea 100644 --- a/tests/int/campaignDemandWorkflow.int.spec.ts +++ b/tests/int/campaignDemandWorkflow.int.spec.ts @@ -7,6 +7,7 @@ import { beforeAll, describe, expect, it } from 'vitest' import { createCampaignDemandRecord, setCampaignDemandCostRecord, + setCampaignDemandResponsiblesRecord, transitionCampaignDemandRecord, updateCampaignDemandRecord, } from '@/app/(campaign)/campanha/actions/demand' @@ -253,9 +254,10 @@ describe('campaign demand workflow (staff-only)', () => { expect(staffRead.cost).toBe(1500.5) }) - it('scopes advisors to demands of administered municipalities', async () => { + it('shows demands only to explicit responsibles (C143)', async () => { const { fixtures, municipality, advisor } = await createScenario() - const outsideAdvisor = await fixtures.createCampaignUser('advisor') + const peerAdvisor = await fixtures.createCampaignUser('advisor') + await fixtures.assignMunicipalityAdvisors(municipality.id, [advisor.id, peerAdvisor.id]) const demand = await createCampaignDemandRecord(payload, advisor, { kind: 'material', @@ -264,15 +266,213 @@ describe('campaign demand workflow (staff-only)', () => { }) fixtures.own('campaignDemand', demand.id) + // The creator enters the responsible list automatically (C143 contract). + const raw = await payload.findByID({ + collection: 'campaignDemand', + id: demand.id, + depth: 0, + overrideAccess: true, + }) + expect(raw.responsibles).toContain(advisor.id) + + const creatorRead = await payload.find({ + collection: 'campaignDemand', + where: { id: { equals: demand.id } }, + depth: 0, + pagination: false, + user: advisor, + overrideAccess: false, + }) + expect(creatorRead.docs).toHaveLength(1) + + // Same-municipality advisor who is not a responsible sees nothing — + // fail-closed, no municipality inheritance. + const peerRead = await payload.find({ + collection: 'campaignDemand', + where: { id: { equals: demand.id } }, + depth: 0, + pagination: false, + user: peerAdvisor, + overrideAccess: false, + }) + expect(peerRead.docs).toHaveLength(0) + }) + + it('keeps demands without responsibles readable only by unrestricted roles (C143 fail-closed)', async () => { + const { fixtures, municipality, coordinator, candidate, advisor, leaderAccount } = + await createScenario() + + const demand = await fixtures.createCampaignDemand({ + municipality: municipality.id, + createdBy: coordinator.id, + }) + + for (const actor of [coordinator, candidate]) { + const visible = await payload.find({ + collection: 'campaignDemand', + where: { id: { equals: demand.id } }, + depth: 0, + pagination: false, + user: actor, + overrideAccess: false, + }) + expect(visible.docs).toHaveLength(1) + } + + // Advisor: the Where scope filters the row out; leader: access is a hard + // false, so Payload refuses the read outright. + const advisorRead = await payload.find({ + collection: 'campaignDemand', + where: { id: { equals: demand.id } }, + depth: 0, + pagination: false, + user: advisor, + overrideAccess: false, + }) + expect(advisorRead.docs).toHaveLength(0) + await expect( + payload.find({ + collection: 'campaignDemand', + where: { id: { equals: demand.id } }, + depth: 0, + pagination: false, + user: leaderAccount, + overrideAccess: false, + }), + ).rejects.toThrow(/permissão/i) + }) + + it('manages the responsible list within the visibility rule (C143)', async () => { + const { fixtures, municipality, coordinator, advisor } = await createScenario() + const secondAdvisor = await fixtures.createCampaignUser('advisor') + await fixtures.assignMunicipalityAdvisors(municipality.id, [advisor.id, secondAdvisor.id]) + + const demand = await createCampaignDemandRecord(payload, advisor, { + kind: 'material', + description: fixtures.value('Comunicação'), + municipality: municipality.id, + }) + fixtures.own('campaignDemand', demand.id) + + // The responsible adds a peer. + await setCampaignDemandResponsiblesRecord(payload, advisor, { + id: demand.id, + responsibles: [advisor.id, secondAdvisor.id], + }) + const raw = await payload.findByID({ + collection: 'campaignDemand', + id: demand.id, + depth: 0, + overrideAccess: true, + }) + expect(raw.responsibles).toEqual(expect.arrayContaining([advisor.id, secondAdvisor.id])) + + // The added peer can now read the demand. + const peerRead = await payload.find({ + collection: 'campaignDemand', + where: { id: { equals: demand.id } }, + depth: 0, + pagination: false, + user: secondAdvisor, + overrideAccess: false, + }) + expect(peerRead.docs).toHaveLength(1) + + // A non-responsible cannot manage the list (update scope = read scope). + const outsideAdvisor = await fixtures.createCampaignUser('advisor') + await expect( + setCampaignDemandResponsiblesRecord(payload, outsideAdvisor, { + id: demand.id, + responsibles: [outsideAdvisor.id], + }), + ).rejects.toThrow() + + // Self-removal hands the demand off: the former responsible loses access. + await setCampaignDemandResponsiblesRecord(payload, advisor, { + id: demand.id, + responsibles: [secondAdvisor.id], + }) + const afterRemoval = await payload.find({ + collection: 'campaignDemand', + where: { id: { equals: demand.id } }, + depth: 0, + pagination: false, + user: advisor, + overrideAccess: false, + }) + expect(afterRemoval.docs).toHaveLength(0) + + // An unrestricted coordinator can always restore/read it. + const coordinatorRead = await payload.find({ + collection: 'campaignDemand', + where: { id: { equals: demand.id } }, + depth: 0, + pagination: false, + user: coordinator, + overrideAccess: false, + }) + expect(coordinatorRead.docs).toHaveLength(1) + }) + + it('lets a responsible outside the municipality portfolio read and manage (C143)', async () => { + const { fixtures, municipality, advisor } = await createScenario() + const remoteAdvisor = await fixtures.createCampaignUser('advisor') + + const demand = await createCampaignDemandRecord(payload, advisor, { + kind: 'material', + description: fixtures.value('Demanda fora da carteira'), + municipality: municipality.id, + }) + fixtures.own('campaignDemand', demand.id) + + // The remote advisor administers no municipality of this demand, but an + // explicit responsibility grants visibility by design (no municipality + // inheritance — the link is the responsibility itself). + await setCampaignDemandResponsiblesRecord(payload, advisor, { + id: demand.id, + responsibles: [advisor.id, remoteAdvisor.id], + }) const visible = await payload.find({ collection: 'campaignDemand', where: { id: { equals: demand.id } }, depth: 0, pagination: false, - user: outsideAdvisor, + user: remoteAdvisor, overrideAccess: false, }) - expect(visible.docs).toHaveLength(0) + expect(visible.docs).toHaveLength(1) + + const managed = await setCampaignDemandResponsiblesRecord(payload, remoteAdvisor, { + id: demand.id, + responsibles: [remoteAdvisor.id], + }) + expect(managed.responsibles).toEqual([remoteAdvisor.id]) + }) + + it('drops non-eligible ids from the responsible list (C143 fail-closed)', async () => { + const { fixtures, municipality, advisor, leaderAccount } = await createScenario() + + const demand = await createCampaignDemandRecord(payload, advisor, { + kind: 'material', + description: fixtures.value('Lista limpa'), + municipality: municipality.id, + }) + fixtures.own('campaignDemand', demand.id) + + // A forged request naming a leader as responsible: the beforeValidate + // hook intersects with eligible staff, so the leader never lands on the + // list (they read hard-false anyway — this pins the invariant server-side). + await setCampaignDemandResponsiblesRecord(payload, advisor, { + id: demand.id, + responsibles: [advisor.id, leaderAccount.id], + }) + const raw = await payload.findByID({ + collection: 'campaignDemand', + id: demand.id, + depth: 0, + overrideAccess: true, + }) + expect(raw.responsibles).toEqual([advisor.id]) }) it('keeps the previous title and slug when the edited description has no AI', async () => { diff --git a/tests/int/homeSearchDemands.int.spec.ts b/tests/int/homeSearchDemands.int.spec.ts index 8d54c848..41de8222 100644 --- a/tests/int/homeSearchDemands.int.spec.ts +++ b/tests/int/homeSearchDemands.int.spec.ts @@ -64,7 +64,7 @@ describe('searchHomeDemands (B53)', () => { expect(result.some((row) => row.id === demand.id)).toBe(false) }) - it('scopes demand hits to the advisor portfolio by municipality', async () => { + it('scopes demand hits to explicit responsibles (C143)', async () => { const fixtures = campaignFixtures() const advisor = await fixtures.createCampaignUser('advisor') const coordinator = await fixtures.createCampaignUser('coordinator') @@ -74,23 +74,30 @@ describe('searchHomeDemands (B53)', () => { await fixtures.assignMunicipalityAdvisors(administered.id, [advisor.id]) const scopeToken = fixtures.value('scope-demanda') - const inScope = await fixtures.createCampaignDemand({ + const responsible = await fixtures.createCampaignDemand({ municipality: administered, title: `Material ${scopeToken} ${administered.name}`, createdBy: coordinator, + responsibles: [advisor.id], }) - const outOfScope = await fixtures.createCampaignDemand({ + const notResponsible = await fixtures.createCampaignDemand({ + // Same administered municipality — but no explicit responsibility. + municipality: administered, + title: `Material ${scopeToken} ${administered.name} fora`, + createdBy: coordinator, + }) + const outOfPortfolio = await fixtures.createCampaignDemand({ municipality: other, title: `Material ${scopeToken} ${other.name}`, createdBy: coordinator, }) const advisorResult = await searchHomeDemands(payload, advisor, scopeToken) - expect(advisorResult.map((hit) => hit.id)).toEqual([inScope.id]) + expect(advisorResult.map((hit) => hit.id)).toEqual([responsible.id]) const coordinatorResult = await searchHomeDemands(payload, coordinator, scopeToken) expect(coordinatorResult.map((hit) => hit.id).sort((a, b) => a - b)).toEqual( - [inScope.id, outOfScope.id].sort((a, b) => a - b), + [responsible.id, notResponsible.id, outOfPortfolio.id].sort((a, b) => a - b), ) }) diff --git a/tests/unit/branchProtection.unit.spec.ts b/tests/unit/branchProtection.unit.spec.ts deleted file mode 100644 index babaed5e..00000000 --- a/tests/unit/branchProtection.unit.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { describe, expect, it } from 'vitest' - -import { - DESIRED_RULE, - planBranchProtectionRule, - ruleMatches, -} from '../../scripts/lib/branch-protection.mjs' - -describe('branch-protection rule planning (OPS61)', () => { - it('plans create when no rule exists', () => { - expect(planBranchProtectionRule(null).action).toBe('create') - }) - - it('plans noop when the rule already matches', () => { - const compliant = { ...DESIRED_RULE, branch_name: 'main' } - expect(planBranchProtectionRule(compliant).action).toBe('noop') - expect(ruleMatches(compliant)).toBe(true) - }) - - it('plans update when the rule drifts', () => { - const drift = { ...DESIRED_RULE, status_check_contexts: ['checks'] } - expect(planBranchProtectionRule(drift).action).toBe('update') - expect(planBranchProtectionRule(drift).action).not.toBe('noop') - }) - - it('detects drift in the contexts list (extra context)', () => { - const extra = { - ...DESIRED_RULE, - status_check_contexts: ['CI (PR) / checks*', 'migration-lock'], - } - expect(planBranchProtectionRule(extra).action).toBe('update') - }) - - it('compares status_check_contexts as a set (order does not matter)', () => { - const desired = { ...DESIRED_RULE, status_check_contexts: ['a', 'b'] } - const existing = { ...desired, status_check_contexts: ['b', 'a'] } - expect(ruleMatches(existing, desired)).toBe(true) - expect(ruleMatches({ ...desired, status_check_contexts: ['a'] }, desired)).toBe(false) - }) - - it('the desired glob matches the real observed rollup context, not the push rollup', () => { - // Forgejo matches rule contexts as gobwas globs against the full context - // string — including the ` (event)` suffix — so the observed rollup is - // `CI (PR) / checks (pull_request)`. A literal `checks` would never match. - const glob = DESIRED_RULE.status_check_contexts[0] - const toRegex = (pattern: string) => - new RegExp('^' + pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace('*', '.*') + '$') - const match = toRegex(glob) - expect(match.test('CI (PR) / checks (pull_request)')).toBe(true) - expect(match.test('CI / checks (push)')).toBe(false) - expect(match.test('CI (PR) / checks')).toBe(true) - }) -}) diff --git a/tests/unit/ciClassifyProduction.unit.spec.ts b/tests/unit/ciClassifyProduction.unit.spec.ts deleted file mode 100644 index 7687a6b4..00000000 --- a/tests/unit/ciClassifyProduction.unit.spec.ts +++ /dev/null @@ -1,194 +0,0 @@ -// @vitest-environment node - -import { execFileSync, spawnSync } from 'node:child_process' -import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs' -import { tmpdir } from 'node:os' -import { join } from 'node:path' -import { fileURLToPath } from 'node:url' - -import { afterAll, beforeAll, describe, expect, it } from 'vitest' - -import { classifyProduction } from '../../scripts/ci-classify-production.mjs' -import { parseDockerignore } from '../../scripts/lib/dockerignore.mjs' - -const repoRoot = join(fileURLToPath(new URL('.', import.meta.url)), '../..') -const cliPath = join(repoRoot, 'scripts/ci-classify-production.mjs') -const realRules = parseDockerignore(readFileSync(join(repoRoot, '.dockerignore'), 'utf8')) - -let fixturesDir: string -let filesFrom: string -let gitRepo: string -let fixtureShas: { baseSha: string; prodSha: string; docsSha: string } - -beforeAll(() => { - fixturesDir = mkdtempSync(join(tmpdir(), 'ci-classify-production-')) - filesFrom = join(fixturesDir, 'changed.txt') - gitRepo = join(fixturesDir, 'repo') - execFileSync('git', ['init', '-q', '-b', 'main', gitRepo]) - writeFileSync(join(gitRepo, '.dockerignore'), 'docs\n') - writeFileSync(join(gitRepo, 'README.md'), 'a\n') - const commit = (message: string) => - execFileSync('git', [ - '-C', - gitRepo, - '-c', - 'user.name=t', - '-c', - 'user.email=t@t', - 'commit', - '-q', - '-m', - message, - ]) - execFileSync('git', ['-C', gitRepo, 'add', '-A']) - commit('base') - const baseSha = execFileSync('git', ['-C', gitRepo, 'rev-parse', 'HEAD'], { - encoding: 'utf8', - }).trim() - execFileSync('mkdir', ['-p', join(gitRepo, 'src'), join(gitRepo, 'docs')]) - writeFileSync(join(gitRepo, 'src', 'a.ts'), 'x\n') - writeFileSync(join(gitRepo, 'docs', 'x.md'), 'x\n') - execFileSync('git', ['-C', gitRepo, 'add', '-A']) - commit('prod change') - const prodSha = execFileSync('git', ['-C', gitRepo, 'rev-parse', 'HEAD'], { - encoding: 'utf8', - }).trim() - writeFileSync(join(gitRepo, 'docs', 'y.md'), 'x\n') - execFileSync('git', ['-C', gitRepo, 'add', '-A']) - commit('docs only') - const docsSha = execFileSync('git', ['-C', gitRepo, 'rev-parse', 'HEAD'], { - encoding: 'utf8', - }).trim() - fixtureShas = { baseSha, prodSha, docsSha } -}) - -afterAll(() => { - execFileSync('rm', ['-rf', fixturesDir]) -}) - -const fixture = () => fixtureShas - -const runCli = (args: string[], options: { cwd?: string } = {}) => { - const env = { ...process.env, GITHUB_EVENT_NAME: 'schedule' } - const result = spawnSync('node', [cliPath, ...args], { - encoding: 'utf8', - env, - cwd: options.cwd, - }) - return { out: result.stdout.trim(), status: result.status } -} - -const writeChanged = (paths: string[]) => writeFileSync(filesFrom, paths.join('\n')) - -describe('classifyProduction (pure)', () => { - it('keeps paths the docker build context receives', () => { - const { production, kept, skipped } = classifyProduction( - ['src/app/page.tsx', 'docs/plans/x.md', 'package.json'], - realRules, - ) - expect(production).toBe(true) - expect(kept).toEqual(['src/app/page.tsx', 'package.json']) - expect(skipped).toEqual(['docs/plans/x.md']) - }) - - it('reports no production change when every path is ignored', () => { - const { production, kept, skipped } = classifyProduction( - ['docs/ops/teqo-1313-deploy.md', '.agents/skills/foo/SKILL.md'], - realRules, - ) - expect(production).toBe(false) - expect(kept).toEqual([]) - expect(skipped).toHaveLength(2) - }) -}) - -describe('ci-classify-production CLI (OPS65 gate)', () => { - it('reports JSON with production, changed, kept and skipped', () => { - writeChanged(['docs/plans/x.md', 'src/lib/foo.ts']) - const { out, status } = runCli(['--files-from', filesFrom]) - expect(status).toBe(0) - const result = JSON.parse(out) - expect(result.production).toBe(true) - expect(result.kept).toEqual(['src/lib/foo.ts']) - expect(result.skipped).toEqual(['docs/plans/x.md']) - }) - - it('--value prints only the boolean', () => { - writeChanged(['docs/plans/x.md']) - expect(runCli(['--files-from', filesFrom, '--value']).out).toBe('false') - writeChanged(['src/app/page.tsx']) - expect(runCli(['--files-from', filesFrom, '--value']).out).toBe('true') - }) - - it('an empty diff means nothing to do (production=false)', () => { - writeChanged([]) - const { out, status } = runCli(['--files-from', filesFrom]) - expect(status).toBe(0) - const result = JSON.parse(out) - expect(result.production).toBe(false) - expect(result.reason).toContain('nada a fazer') - }) - - it('classifies a real git diff against the deployed revision', () => { - const { baseSha, prodSha, docsSha } = fixture() - // HEAD is the docs-only commit; from the base SHA the diff mixes the - // production change (src/a.ts) with the docs-only commits. - const prod = JSON.parse(runCli(['--deployed', baseSha], { cwd: gitRepo }).out) - expect(prod.production).toBe(true) - expect(prod.changed).toEqual(['docs/x.md', 'docs/y.md', 'src/a.ts']) - expect(prod.kept).toEqual(['src/a.ts']) - expect(prod.skipped).toEqual(['docs/x.md', 'docs/y.md']) - - const docs = JSON.parse(runCli(['--deployed', prodSha], { cwd: gitRepo }).out) - expect(docs.production).toBe(false) - expect(docs.skipped).toEqual(['docs/y.md']) - - const same = JSON.parse(runCli(['--deployed', docsSha], { cwd: gitRepo }).out) - expect(same.production).toBe(false) - }) - - it('keeps the run green when the deployed SHA is unknown (empty)', () => { - const { out, status } = runCli(['--deployed', '']) - expect(status).toBe(0) - const result = JSON.parse(out) - expect(result.production).toBe(true) - expect(result.reason).toContain('sem SHA deployado') - }) - - it('turns the run red when it cannot classify (no input)', () => { - const { out, status } = runCli([]) - expect(status).not.toBe(0) - expect(JSON.parse(out).production).toBe(true) - }) - - it('turns the run red when the deployed revision cannot be diffed', () => { - const { out, status } = runCli(['--deployed', 'ffffffffffffffffffffffffffffffffffffffff'], { - cwd: gitRepo, - }) - expect(status).not.toBe(0) - const result = JSON.parse(out) - expect(result.production).toBe(true) - expect(result.reason).toContain('erro ao obter o diff') - }) - - it('turns the run red when the .dockerignore is unreadable', () => { - writeChanged(['src/app/page.tsx']) - const { out, status } = runCli(['--files-from', filesFrom], { cwd: fixturesDir }) - expect(status).not.toBe(0) - const result = JSON.parse(out) - expect(result.production).toBe(true) - expect(result.reason).toContain('.dockerignore ilegível') - }) - - it('workflow_dispatch always runs the full pipeline', () => { - const env = { ...process.env, GITHUB_EVENT_NAME: 'workflow_dispatch' } - const result = spawnSync('node', [cliPath, '--files-from', filesFrom], { - encoding: 'utf8', - env, - }) - const parsed = JSON.parse(result.stdout) - expect(result.status).toBe(0) - expect(parsed.production).toBe(true) - expect(parsed.reason).toContain('workflow_dispatch') - }) -}) diff --git a/tests/unit/ciSkipInvariants.unit.spec.ts b/tests/unit/ciSkipInvariants.unit.spec.ts index 8570d5f1..593aa34d 100644 --- a/tests/unit/ciSkipInvariants.unit.spec.ts +++ b/tests/unit/ciSkipInvariants.unit.spec.ts @@ -77,8 +77,6 @@ describe('ciSkipInvariants', () => { it('keeps local e2e build artifacts outside the development dist directory', () => { for (const file of [ - '.forgejo/workflows/ci.yml', - '.forgejo/workflows/ci-pr.yml', '.gitignore', '.prettierignore', 'eslint.config.mjs', @@ -96,8 +94,6 @@ describe('ciSkipInvariants', () => { for (const path of [ 'scripts/lib/test-affected-core.mjs', 'scripts/ci-scope.mjs', - 'scripts/ci-classify-production.mjs', - 'scripts/lib/dockerignore.mjs', 'scripts/check-test-locations.mjs', 'scripts/check-plans-only-pr-closes.mjs', 'scripts/lib/plansOnlyClosesGuard.mjs', @@ -111,30 +107,17 @@ describe('ciSkipInvariants', () => { } }) - it('main CI runs in a 30-minute window with a production gate (OPS65)', () => { - const ci = readFileSync(join(repoRoot, '.forgejo/workflows/ci.yml'), 'utf8') - expect(ci).toContain('schedule:') - expect(ci).toContain("cron: '*/30 * * * *'") - expect(ci).not.toMatch(/push:\s*\n\s*branches:\s*\[main\]/) - expect(ci).toContain('workflow_dispatch:') + it('deploy is manual-only — never a push/schedule trigger (OPS71)', () => { + const deploy = readFileSync(join(repoRoot, '.github/workflows/deploy.yml'), 'utf8') + expect(deploy).toContain('workflow_dispatch:') + expect(deploy).not.toMatch(/^\s*push:/m) + expect(deploy).not.toMatch(/^\s*schedule:/m) - // The cheap gate (no pnpm) classifies production change and every - // expensive job consumes it, fail-open on gate errors. The gate is - // main-only: a branch dispatch must not run branch code on the host. - expect(ci).toContain('gate production change') - expect(ci).toContain("if: github.ref == 'refs/heads/main'") - expect(ci).toContain('fetch-depth: 0') - expect(ci).toContain('scripts/ci-classify-production.mjs') - expect(ci).toContain( - "needs.gate.result != 'success' || needs.gate.outputs.production == 'true'", - ) - - // Deploy is double-gated: green rollup AND production change (never - // deploy blind, never deploy with a red suite). - expect(ci).toContain('needs: [checks, gate]') - expect(ci).toContain( - "needs.checks.result == 'success' && needs.gate.outputs.production == 'true'", - ) + // The OPS65 production-change gate died with the Forgejo ci.yml: the + // manual dispatch always runs the full suite (verify) before deploy, so + // no PR workflow may reference the removed classifier. + const ciPr = readFileSync(join(repoRoot, '.github/workflows/ci-pr.yml'), 'utf8') + expect(ciPr).not.toContain('ci-classify-production.mjs') }) it('every campaign domain dir is covered by the e2e affected manifest', () => { diff --git a/tests/unit/codebaseConventions.unit.spec.ts b/tests/unit/codebaseConventions.unit.spec.ts index cec1974d..195c02e5 100644 --- a/tests/unit/codebaseConventions.unit.spec.ts +++ b/tests/unit/codebaseConventions.unit.spec.ts @@ -206,6 +206,10 @@ describe('campaign JSON mutation route convention', () => { 'src/app/(frontend)/api/revalidate/route.ts', 'server-to-server: authenticated by a secret header, deliberately callable cross-origin', ], + [ + 'src/app/(frontend)/api/social-feed/sync/route.ts', + 'admin-session authenticated (payload.auth, users collection) + explicit isSameOriginRequest; bodyless action returning the sync status — the campaignJsonMutationRoute envelope is campaign-scoped ({status:error})', + ], [ 'src/app/(campaign)/campanha/api/ai-chat/route.ts', 'streaming AI endpoint (ReadableStream, not JSON) — cookie-authenticated via campaign-token, origin-checked by cookie path', diff --git a/tests/unit/dockerignoreMatch.unit.spec.ts b/tests/unit/dockerignoreMatch.unit.spec.ts deleted file mode 100644 index 82f99d0e..00000000 --- a/tests/unit/dockerignoreMatch.unit.spec.ts +++ /dev/null @@ -1,107 +0,0 @@ -// @vitest-environment node - -import { readFileSync } from 'node:fs' -import { join } from 'node:path' -import { fileURLToPath } from 'node:url' - -import { describe, expect, it } from 'vitest' - -import { - compileDockerignoreRule, - isDockerignored, - parseDockerignore, -} from '../../scripts/lib/dockerignore.mjs' - -const repoRoot = join(fileURLToPath(new URL('.', import.meta.url)), '../..') -const dockerignore = readFileSync(join(repoRoot, '.dockerignore'), 'utf8') - -describe('dockerignoreMatch (OPS65 production-change gate)', () => { - it('compiles the moby semantics: basename at any depth, subtree exclusion', () => { - const rules = parseDockerignore('docs\n*.log\n') - expect(isDockerignored('docs', rules)).toBe(true) - expect(isDockerignored('docs/plans/x.md', rules)).toBe(true) - expect(isDockerignored('a/docs/plans/x.md', rules)).toBe(true) - expect(isDockerignored('x.log', rules)).toBe(true) - expect(isDockerignored('a/b/x.log', rules)).toBe(true) - expect(isDockerignored('src/app/page.tsx', rules)).toBe(false) - }) - - it('anchors leading-slash patterns to the context root', () => { - const rules = parseDockerignore('/*.png\n') - expect(isDockerignored('a.png', rules)).toBe(true) - expect(isDockerignored('sub/b.png', rules)).toBe(false) - }) - - it('honours ! negations with last-match-wins', () => { - const rules = parseDockerignore('.env.*\n!.env.example\n') - expect(isDockerignored('.env.local', rules)).toBe(true) - expect(isDockerignored('.env.example', rules)).toBe(false) - expect(isDockerignored('a/.env.example', rules)).toBe(false) - }) - - it('supports globstar and directory-only patterns', () => { - const rules = parseDockerignore('**/fixtures\na/**/b\n') - expect(isDockerignored('x/fixtures/y.json', rules)).toBe(true) - expect(isDockerignored('a/b', rules)).toBe(true) - expect(isDockerignored('a/x/b', rules)).toBe(true) - expect(isDockerignored('a/x/c/b', rules)).toBe(true) - }) - - it('ignores blank lines and comments', () => { - const rules = parseDockerignore('# header\n\ndocs\n') - expect(rules).toHaveLength(1) - expect(isDockerignored('docs/x', rules)).toBe(true) - }) - - it('escapes regex metacharacters in plain patterns', () => { - const rules = parseDockerignore('foo.bar\n') - expect(isDockerignored('foo.bar', rules)).toBe(true) - expect(isDockerignored('fooXbar', rules)).toBe(false) - }) - - it('treats the repo .dockerignore as the source of truth', () => { - const rules = parseDockerignore(dockerignore) - // Anything the image needs is production: changes force the full suite. - for (const path of [ - 'src/app/page.tsx', - 'src/migrations/20260818_x.ts', - 'public/favicon.ico', - 'Dockerfile', - '.dockerignore', - 'AGENTS.md', - 'package.json', - 'pnpm-lock.yaml', - 'scripts/deploy-homeserver.sh', - 'scripts/ci-classify-production.mjs', - '.forgejo/workflows/ci.yml', - '.forgejo/workflows/ci-pr.yml', - 'tests/unit/dockerignoreMatch.unit.spec.ts', - '.env.example', - ]) { - expect(isDockerignored(path, rules), path).toBe(false) - } - // Non-artifact paths skip the suite (quiet days cost seconds). - for (const path of [ - 'docs/plans/x.md', - 'docs/ops/teqo-1313-deploy.md', - '.agents/skills/foo/SKILL.md', - '.env.local', - '.env.test.local', - 'data/tse/x.zip', - 'media/x.jpg', - 'foo.png', - 'x.log', - 'x.tsbuildinfo', - 'node_modules/x', - '.next/x', - '.next-e2e/x', - ]) { - expect(isDockerignored(path, rules), path).toBe(true) - } - }) - - it('keeps the .dockerignore itself explicit (self-referential gate)', () => { - const rule = compileDockerignoreRule('!.dockerignore') - expect(rule.negate).toBe(true) - }) -}) diff --git a/tests/unit/forgejoApi.unit.spec.ts b/tests/unit/forgejoApi.unit.spec.ts index e8d0c70b..18cb5de2 100644 --- a/tests/unit/forgejoApi.unit.spec.ts +++ b/tests/unit/forgejoApi.unit.spec.ts @@ -39,18 +39,32 @@ describe('forgejo-api', () => { }) it('defaults the base URL to git.solla.dev with the default repo path', async () => { - const calls: FetchCall[] = [] - const api = createApi({ - token: 'tok', - fetchImpl: async (url) => { - calls.push({ url: String(url) }) - return ok([{ number: 1, title: '', body: '', state: 'open', created_at: '', labels: [] }]) - }, - }) + // OPS71: the CI now runs on GitHub Actions, where GITHUB_SERVER_URL is + // set (https://github.com) — forgejo-api's env chain would resolve a + // different base. Pin the fallback default by isolating the env vars. + const savedApi = process.env.FORGEJO_API_URL + const savedServer = process.env.GITHUB_SERVER_URL + delete process.env.FORGEJO_API_URL + delete process.env.GITHUB_SERVER_URL + try { + const calls: FetchCall[] = [] + const api = createApi({ + token: 'tok', + fetchImpl: async (url) => { + calls.push({ url: String(url) }) + return ok([{ number: 1, title: '', body: '', state: 'open', created_at: '', labels: [] }]) + }, + }) - await api.listIssues() + await api.listIssues() - expect(calls[0].url).toContain('https://git.solla.dev/api/v1/repos/fsolla/teqo/issues?') + expect(calls[0].url).toContain('https://git.solla.dev/api/v1/repos/fsolla/teqo/issues?') + } finally { + if (savedApi) process.env.FORGEJO_API_URL = savedApi + else delete process.env.FORGEJO_API_URL + if (savedServer) process.env.GITHUB_SERVER_URL = savedServer + else delete process.env.GITHUB_SERVER_URL + } }) it('normalizes state and labels to the gh-flavored contract', async () => { @@ -100,6 +114,9 @@ describe('forgejo-api', () => { it('setLabels removes by id and adds by name', async () => { const calls: FetchCall[] = [] const api = createApi({ + // OPS71: explicit base — the GitHub Actions CI sets GITHUB_SERVER_URL, + // which would change the env-resolved base and break the URL pins. + base: 'https://git.solla.dev/api/v1', token: 'tok', fetchImpl: async (url, init) => { calls.push({ url: String(url), method: init?.method, body: init?.body }) @@ -120,6 +137,28 @@ describe('forgejo-api', () => { expect(JSON.parse(calls[2].body as string)).toEqual({ labels: ['ready'] }) }) + it('closeIssue PATCHes state=closed (OPS71-FLIP: the GitHub PR merge no longer closes the Forgejo issue)', async () => { + const calls: FetchCall[] = [] + const api = createApi({ + base: 'https://git.solla.dev/api/v1', + token: 'tok', + fetchImpl: async (url, init) => { + calls.push({ url: String(url), method: init?.method, body: init?.body }) + return ok(null) + }, + }) + + await api.closeIssue(9) + + expect(calls).toEqual([ + { + url: 'https://git.solla.dev/api/v1/repos/fsolla/teqo/issues/9', + method: 'PATCH', + body: JSON.stringify({ state: 'closed' }), + }, + ]) + }) + it('workflowDispatch posts inputs and ref', async () => { const calls: FetchCall[] = [] const api = createApi({ @@ -440,6 +479,9 @@ describe('forgejo-api', () => { const calls: string[] = [] let poll = 0 const api = createApi({ + // OPS71: explicit base — the GitHub Actions CI sets GITHUB_SERVER_URL, + // which would change the env-resolved base and break the URL pins. + base: 'https://git.solla.dev/api/v1', token: 'tok', fetchImpl: async (url, init) => { calls.push(`${init?.method ?? 'GET'} ${url}`) @@ -851,5 +893,69 @@ describe('forgejo-api', () => { await expect(api.addComment(1, 'x')).rejects.toThrow(/503/) expect(calls).toBe(1) }) + + it('retries a 410 on GET (proxy answered — origin untouched)', async () => { + silentWarn() + let calls = 0 + const api = createApi({ + token: 'tok', + retries: 2, + backoffMs: 0, + jitter: false, + sleepImpl: async () => {}, + fetchImpl: async () => { + calls += 1 + return calls === 1 ? ok('410 Gone', 410) : ok(issueOk) + }, + }) + + const issues = await api.listIssues() + + expect(calls).toBe(2) + expect(issues).toHaveLength(1) + }) + + it('retries a 410 on write methods too (nginx answered before the origin — no duplicated side effect)', async () => { + silentWarn() + let calls = 0 + const api = createApi({ + token: 'tok', + retries: 2, + backoffMs: 0, + jitter: false, + sleepImpl: async () => {}, + fetchImpl: async () => { + calls += 1 + return calls === 1 ? ok('410 Gone', 410) : ok(null) + }, + }) + + await api.addComment(1, 'x') + + expect(calls).toBe(2) + }) + + it('throws a compact proxy marker after exhausting 410 retries (nginx html not swallowed raw)', async () => { + silentWarn() + let calls = 0 + const api = createApi({ + token: 'tok', + retries: 1, + backoffMs: 0, + jitter: false, + sleepImpl: async () => {}, + fetchImpl: async () => { + calls += 1 + return ok('\n410 Gone\ngone', 410) + }, + }) + + const error = await api.addComment(1, 'x').catch((e: unknown) => e) + + expect((error as Error).message).toMatch(/410/) + expect((error as Error).message).toMatch(/corpo não-JSON \(proxy\/nginx/) + expect((error as Error).message).toMatch(/410 Gone/) + expect(calls).toBe(2) + }) }) }) diff --git a/tests/unit/githubApi.unit.spec.ts b/tests/unit/githubApi.unit.spec.ts new file mode 100644 index 00000000..5e16363f --- /dev/null +++ b/tests/unit/githubApi.unit.spec.ts @@ -0,0 +1,228 @@ +import { describe, expect, it } from 'vitest' + +import { createApi } from '../../scripts/lib/github-api.mjs' + +type FetchCall = { + url: string + method?: string + init?: { headers?: Record; method?: string; body?: string } + body?: unknown +} + +const ok = (body: unknown, status = 200) => + new Response(typeof body === 'string' ? body : JSON.stringify(body), { + status, + headers: { 'Content-Type': 'application/json' }, + }) + +describe('github-api', () => { + it('builds the request with bearer token, JSON body and explicit User-Agent', async () => { + const calls: FetchCall[] = [] + const api = createApi({ + token: 'tok', + repository: 'fsolla/teqo', + fetchImpl: async (url, init) => { + calls.push({ url: String(url), init }) + return ok({ number: 1, title: 't', body: '', state: 'open', node_id: 'PR_x' }) + }, + }) + + await api.getPullRequest(1) + + expect(calls).toHaveLength(1) + expect(calls[0].url).toBe('https://api.github.com/repos/fsolla/teqo/pulls/1') + expect(calls[0].init!.headers!.Authorization).toBe('Bearer tok') + expect(calls[0].init!.headers!['User-Agent']).toContain('teqo-agent-scripts') + }) + + it('fails closed when no token is available', async () => { + delete process.env.GITHUB_TOKEN + const api = createApi({ fetchImpl: async () => ok('{}') }) + + await expect(api.getPullRequest(1)).rejects.toThrow(/GITHUB_TOKEN/) + }) + + it('normalizes the pull request to the CLI contract (state/draft/nodeId/head/base)', async () => { + const api = createApi({ + token: 'tok', + fetchImpl: async () => + ok({ + number: 7, + title: 't', + body: 'Closes #1', + state: 'closed', + merged_at: '2026-08-19T00:00:00Z', + draft: false, + mergeable: true, + node_id: 'PR_abc123', + head: { ref: 'OPS71-x', sha: 'deadbeef' }, + base: { ref: 'main' }, + }), + }) + + const pr = await api.getPullRequest(7) + + expect(pr).toEqual({ + number: 7, + title: 't', + body: 'Closes #1', + state: 'CLOSED', + merged: true, + draft: false, + mergeable: true, + nodeId: 'PR_abc123', + head: { ref: 'OPS71-x', sha: 'deadbeef' }, + base: { ref: 'main' }, + }) + }) + + it('markPullRequestReady patches draft:false', async () => { + const calls: FetchCall[] = [] + const api = createApi({ + token: 'tok', + fetchImpl: async (url, init) => { + calls.push({ url: String(url), method: init?.method, body: init?.body }) + return ok(null) + }, + }) + + await api.markPullRequestReady(4) + + expect(calls).toEqual([ + { + url: 'https://api.github.com/repos/fsolla/teqo/pulls/4', + method: 'PATCH', + body: JSON.stringify({ draft: false }), + }, + ]) + }) + + it('createPullRequest posts a Ready PR (never draft) with the given body', async () => { + const calls: FetchCall[] = [] + const api = createApi({ + token: 'tok', + fetchImpl: async (url, init) => { + calls.push({ url: String(url), method: init?.method, body: init?.body }) + return ok({ number: 9, title: 't', html_url: 'https://github.com/fsolla/teqo/pull/9' }) + }, + }) + + const created = await api.createPullRequest({ + head: 'OPS71-x', + base: 'main', + title: 'OPS71 — x', + body: 'Closes #97', + }) + + expect(created).toEqual({ + number: 9, + title: 't', + htmlUrl: 'https://github.com/fsolla/teqo/pull/9', + }) + expect(calls[0].url).toBe('https://api.github.com/repos/fsolla/teqo/pulls') + expect(JSON.parse(String(calls[0].body))).toEqual({ + head: 'OPS71-x', + base: 'main', + title: 'OPS71 — x', + body: 'Closes #97', + }) + }) + + it('enableAutoMerge posts the GraphQL mutation with rebase', async () => { + const calls: FetchCall[] = [] + const api = createApi({ + token: 'tok', + fetchImpl: async (url, init) => { + calls.push({ url: String(url), body: init?.body }) + return ok({ data: { enablePullRequestAutoMerge: { pullRequest: { number: 4 } } } }) + }, + }) + + await api.enableAutoMerge('PR_abc123') + + expect(calls).toHaveLength(1) + expect(calls[0].url).toBe('https://api.github.com/graphql') + const { query, variables } = JSON.parse(String(calls[0].body)) + expect(query).toContain('enablePullRequestAutoMerge') + expect(query).toContain('mergeMethod') + expect(variables).toEqual({ id: 'PR_abc123', mergeMethod: 'REBASE' }) + }) + + it('enableAutoMerge throws with the GraphQL error message', async () => { + const api = createApi({ + token: 'tok', + fetchImpl: async () => + ok({ + data: null, + errors: [{ message: 'auto-merge is disabled on this repository' }], + }), + }) + + await expect(api.enableAutoMerge('PR_x')).rejects.toThrow(/auto-merge is disabled/) + }) + + it('getBranchProtection returns null on 404 and normalizes an existing rule', async () => { + let calls = 0 + const api = createApi({ + token: 'tok', + fetchImpl: async () => { + calls += 1 + if (calls === 1) return ok('{"message":"Not Found"}', 404) + return ok({ + url: 'https://api.github.com/repos/fsolla/teqo/branches/main/protection', + required_status_checks: { + strict: false, + checks: [{ context: 'CI (PR) / checks', app_id: null }], + contexts: ['CI (PR) / checks'], + }, + enforce_admins: { enabled: true, url: '…' }, + required_pull_request_reviews: null, + }) + }, + }) + + expect(await api.getBranchProtection('main')).toBeNull() + + const rule = await api.getBranchProtection('main') + expect(rule).toEqual({ + required_status_checks: { + strict: false, + contexts: ['CI (PR) / checks'], + }, + enforce_admins: true, + required_pull_request_reviews: null, + }) + }) + + it('updateBranchProtection PUTs the full desired rule', async () => { + const calls: FetchCall[] = [] + const api = createApi({ + token: 'tok', + fetchImpl: async (url, init) => { + calls.push({ url: String(url), method: init?.method }) + return ok(null) + }, + }) + + await api.updateBranchProtection({ enforce_admins: true }, 'main') + + expect(calls[0].url).toBe('https://api.github.com/repos/fsolla/teqo/branches/main/protection') + expect(calls[0].method).toBe('PUT') + }) + + it('throws with the API error body on non-2xx', async () => { + let calls = 0 + const api = createApi({ + token: 'tok', + retries: 0, + fetchImpl: async () => { + calls += 1 + return ok('{"message":"Bad credentials"}', 401) + }, + }) + + await expect(api.getPullRequest(1)).rejects.toThrow(/401/) + await expect(api.getPullRequest(1)).rejects.toThrow(/Bad credentials/) + expect(calls).toBe(2) + }) +}) diff --git a/tests/unit/githubBranchProtection.unit.spec.ts b/tests/unit/githubBranchProtection.unit.spec.ts new file mode 100644 index 00000000..d9803035 --- /dev/null +++ b/tests/unit/githubBranchProtection.unit.spec.ts @@ -0,0 +1,88 @@ +import { describe, expect, it } from 'vitest' + +import { + DESIRED_RULE, + REQUIRED_CHECK_CONTEXT, + planBranchProtectionRule, + ruleMatches, +} from '../../scripts/lib/github-branch-protection.mjs' + +const compliant = { + required_status_checks: { strict: false, contexts: [REQUIRED_CHECK_CONTEXT] }, + enforce_admins: true, + required_pull_request_reviews: null, +} + +describe('github branch-protection rule planning (OPS71)', () => { + it('plans create when no rule exists', () => { + expect(planBranchProtectionRule(null).action).toBe('create') + }) + + it('plans noop when the rule already matches', () => { + expect(planBranchProtectionRule(compliant).action).toBe('noop') + expect(ruleMatches(compliant)).toBe(true) + }) + + it('plans update when enforce_admins is off (admin bypass = red-CI merge path)', () => { + const drift = { ...compliant, enforce_admins: false } + expect(planBranchProtectionRule(drift).action).toBe('update') + expect(ruleMatches(drift)).toBe(false) + }) + + it('plans update when the required check context drifts', () => { + const drift = { + ...compliant, + required_status_checks: { strict: false, contexts: ['CI (PR) / checks'] }, + } + expect(planBranchProtectionRule(drift).action).toBe('update') + }) + + it('plans update when a second check is required', () => { + const extra = { + ...compliant, + required_status_checks: { + strict: false, + contexts: [REQUIRED_CHECK_CONTEXT, 'migration-lock'], + }, + } + expect(planBranchProtectionRule(extra).action).toBe('update') + }) + + it('plans update when strict becomes true', () => { + const drift = { + ...compliant, + required_status_checks: { strict: true, contexts: [REQUIRED_CHECK_CONTEXT] }, + } + expect(planBranchProtectionRule(drift).action).toBe('update') + }) + + it('plans update when pull request reviews are required (blocks auto-merge)', () => { + const drift = { + ...compliant, + required_pull_request_reviews: { required_approving_review_count: 1 }, + } + expect(planBranchProtectionRule(drift).action).toBe('update') + }) + + it('plans update when status checks are missing entirely', () => { + const drift = { ...compliant, required_status_checks: null } + expect(planBranchProtectionRule(drift).action).toBe('update') + }) + + it('compares contexts as a set (order does not matter)', () => { + const existing = { + ...compliant, + required_status_checks: { strict: false, contexts: [REQUIRED_CHECK_CONTEXT] }, + } + expect(ruleMatches(existing)).toBe(true) + }) + + it('the desired rule uses the check-run literal GitHub matches (job name, live pin)', () => { + // Verified on PR #742 (OPS71): required-check matching uses the CHECK-RUN + // name (`checks`), not the UI display `CI (PR) / checks` — the + // workflow-prefixed literal never matches and leaves the PR blocked. + const check = DESIRED_RULE.required_status_checks.checks[0] + expect(check.context).toBe('checks') + expect(DESIRED_RULE.required_status_checks.checks).toHaveLength(1) + }) +}) diff --git a/tests/unit/githubPrFlow.unit.spec.ts b/tests/unit/githubPrFlow.unit.spec.ts new file mode 100644 index 00000000..66b889e4 --- /dev/null +++ b/tests/unit/githubPrFlow.unit.spec.ts @@ -0,0 +1,96 @@ +import { describe, expect, it } from 'vitest' + +import { + CURSOR_HEAD_PREFIX, + automergeArmingToken, + decideAutomergeAction, + isCursorHead, +} from '../../scripts/lib/github-pr-flow.mjs' + +const pr = (overrides: Record = {}) => ({ + number: 4, + title: 't', + body: '', + state: 'OPEN', + merged: false, + draft: false, + mergeable: true, + nodeId: 'PR_x', + head: { ref: 'OPS71-x', sha: 'abc' }, + base: { ref: 'main' }, + ...overrides, +}) + +describe('github-pr-flow automerge decision (OPS71)', () => { + it('skips a null PR', () => { + expect(decideAutomergeAction(null).action).toBe('skip') + }) + + it('skips an already merged PR (idempotent)', () => { + expect(decideAutomergeAction(pr({ merged: true, state: 'CLOSED' })).action).toBe('skip') + }) + + it('skips a closed PR without merge', () => { + expect(decideAutomergeAction(pr({ state: 'CLOSED' })).action).toBe('skip') + }) + + it('skips a PR whose base is not main', () => { + expect(decideAutomergeAction(pr({ base: { ref: 'stage' } })).action).toBe('skip') + }) + + it('enables auto-merge on a ready PR to main', () => { + const verdict = decideAutomergeAction(pr()) + expect(verdict.action).toBe('enable-auto-merge') + expect(verdict.reason).toBe('ready') + }) + + it('marks a cursor/* draft ready (OPS57 pool flow)', () => { + const verdict = decideAutomergeAction( + pr({ draft: true, head: { ref: `${CURSOR_HEAD_PREFIX}abc-123`, sha: 'abc' } }), + ) + expect(verdict.action).toBe('mark-ready') + }) + + it('skips any non-cursor/* draft — the actor veto (OPS57)', () => { + const verdict = decideAutomergeAction( + pr({ draft: true, head: { ref: 'plans/ops71-x', sha: 'abc' } }), + ) + expect(verdict.action).toBe('skip') + expect(verdict.reason).toBe('draft-veto') + }) + + it('isCursorHead only matches the cursor/ prefix', () => { + expect(isCursorHead('cursor/abc-123')).toBe(true) + expect(isCursorHead('OPS71-x')).toBe(false) + expect(isCursorHead('')).toBe(false) + expect(isCursorHead(undefined)).toBe(false) + }) + + it('the merge contract has no poll path: ready always arms native auto-merge', () => { + // OPS64 pin, GitHub shape: the Forgejo rollup-lie bug is structural here + // (single job → one honest check-run), and the server only merges with + // required checks green — so the decision surface is exactly this. + const actions = [ + decideAutomergeAction(pr()).action, + decideAutomergeAction(pr({ draft: true, head: { ref: 'cursor/x', sha: 'a' } })).action, + ] + expect(actions).toEqual(['enable-auto-merge', 'mark-ready']) + }) + + it('automergeArmingToken accepts a non-empty AUTOMERGE_PAT (OPS71-FLIP)', () => { + expect(automergeArmingToken({ AUTOMERGE_PAT: 'ghp_abc' })).toEqual({ + ok: true, + token: 'ghp_abc', + }) + }) + + it('automergeArmingToken fails closed without the PAT — never falls back to the run token', () => { + const verdict = automergeArmingToken({}) + expect(verdict.ok).toBe(false) + expect(verdict.ok === false && verdict.reason).toContain('AUTOMERGE_PAT') + }) + + it('automergeArmingToken treats an empty-string PAT as missing', () => { + expect(automergeArmingToken({ AUTOMERGE_PAT: '' }).ok).toBe(false) + }) +}) diff --git a/tests/unit/instagramFeed.unit.spec.ts b/tests/unit/instagramFeed.unit.spec.ts index 8048c1a4..328115b2 100644 --- a/tests/unit/instagramFeed.unit.spec.ts +++ b/tests/unit/instagramFeed.unit.spec.ts @@ -334,4 +334,77 @@ describe('loadInstagramFeed', () => { }) expect(result.username).toBeNull() }) + + it('throws an InstagramApiError carrying the parsed Graph API error body', async () => { + const oauthError = { + error: { + message: 'Error validating access token: Session has expired', + type: 'OAuthException', + code: 190, + }, + } + const userCall = loadInstagramFeed({ + ...args, + fetchImpl: async (input) => + input.includes('/media') ? fakeResponse({}, false) : fakeResponse(oauthError, false), + }) + await expect(userCall).rejects.toMatchObject({ + name: 'InstagramApiError', + status: undefined, + apiMessage: 'Error validating access token: Session has expired', + apiType: 'OAuthException', + }) + + // A refresh rejected with the same OAuth body surfaces the typed error too + // (the FB-page-token case: refresh fails → the panel must say why). + const refreshRejectCall = loadInstagramFeed({ + ...args, + fetchImpl: async (input) => + input.includes('/refresh_access_token') + ? fakeResponse(oauthError, false) + : fakeResponse({}, false), + }) + await expect(refreshRejectCall).rejects.toMatchObject({ apiType: 'OAuthException' }) + }) + + it('survives a non-JSON error body (status kept, api message null)', async () => { + const errorResponse = { + ok: false, + status: 502, + json: async () => { + throw new Error('boom') + }, + } as unknown as Response + await expect( + loadInstagramFeed({ + ...args, + fetchImpl: async () => errorResponse, + }), + ).rejects.toMatchObject({ + name: 'InstagramApiError', + status: 502, + apiMessage: null, + apiType: null, + }) + }) + + it('forwards the abort signal to every fetch', async () => { + const receivedInits: RequestInit[] = [] + const fetchImpl = async (input: string, init?: RequestInit): Promise => { + receivedInits.push(init ?? {}) + if (input.includes('/media')) { + return fakeResponse({ data: [MEDIA_ITEM('p')] }) + } + if (input.includes('/refresh_access_token')) { + return fakeResponse({ access_token: 't' }) + } + return fakeResponse({ username: 'depjorgesolla' }) + } + const controller = new AbortController() + await loadInstagramFeed({ ...args, fetchImpl, signal: controller.signal }) + expect(receivedInits).toHaveLength(2) + for (const init of receivedInits) { + expect(init.signal).toBe(controller.signal) + } + }) }) diff --git a/tests/unit/instagramSync.unit.spec.ts b/tests/unit/instagramSync.unit.spec.ts new file mode 100644 index 00000000..3b505695 --- /dev/null +++ b/tests/unit/instagramSync.unit.spec.ts @@ -0,0 +1,247 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' + +import type { SocialFeedSetting } from '@/payload-types' +import { + describeInstagramError, + failedInstagramSyncStatus, + InstagramApiError, + isInstagramFeedConfigured, + successInstagramSyncStatus, +} from '@/utilities/socialFeed/instagramFeed' +import { + instagramCredentialsChanged, + syncInstagramFeed, +} from '@/utilities/socialFeed/instagramSync' +import type { Payload, PayloadRequest } from 'payload' + +const configuredSettings = (overrides: Partial = {}): SocialFeedSetting => + ({ + id: 1, + enabled: true, + instagramEnabled: true, + instagramAccessToken: 'ig-token', + instagramUserId: '17841400000000000', + instagramMaxItems: 3, + ...overrides, + }) as SocialFeedSetting + +const fakePayload = (settings: SocialFeedSetting, db: unknown = undefined) => + ({ + findGlobal: vi.fn(async () => settings), + db: db ?? { drizzle: { execute: vi.fn(async () => ({ rowCount: 1 })) } }, + }) as unknown as Payload + +const feedResponse = (json: unknown, ok = true, status = 200) => + ({ ok, status, json: async () => json }) as unknown as Response + +afterEach(() => { + vi.unstubAllGlobals() +}) + +describe('successInstagramSyncStatus / failedInstagramSyncStatus', () => { + it('builds a success status with now and the raw post count', () => { + const status = successInstagramSyncStatus(4) + expect(status.postCount).toBe(4) + expect(status.lastSyncAt).toBeDefined() + expect(new Date(status.lastSyncAt as string).getTime()).toBeGreaterThan(0) + expect(status.error).toBeUndefined() + }) + + it('builds a failure status with the reason and the failure time', () => { + const status = failedInstagramSyncStatus('motivo') + expect(status.error).toBe('motivo') + expect(status.errorAt).toBeDefined() + expect(status.lastSyncAt).toBeUndefined() + }) +}) + +describe('isInstagramFeedConfigured', () => { + it('requires enablement and both credentials', () => { + expect(isInstagramFeedConfigured(configuredSettings())).toBe(true) + expect(isInstagramFeedConfigured(configuredSettings({ instagramAccessToken: '' }))).toBe(false) + expect(isInstagramFeedConfigured(configuredSettings({ instagramUserId: '' }))).toBe(false) + expect(isInstagramFeedConfigured(configuredSettings({ enabled: false }))).toBe(false) + expect(isInstagramFeedConfigured(configuredSettings({ instagramEnabled: false }))).toBe(false) + }) +}) + +describe('instagramCredentialsChanged', () => { + it('syncs on first save, credential changes and re-enable, not on exclusion-only saves', () => { + const previous = configuredSettings() + expect(instagramCredentialsChanged(previous, null)).toBe(true) + expect( + instagramCredentialsChanged( + configuredSettings({ instagramAccessToken: 'novo-token' }), + previous, + ), + ).toBe(true) + expect( + instagramCredentialsChanged(configuredSettings({ instagramUserId: '999' }), previous), + ).toBe(true) + expect( + instagramCredentialsChanged( + configuredSettings({ instagramEnabled: true }), + configuredSettings({ instagramEnabled: false }), + ), + ).toBe(true) + expect( + instagramCredentialsChanged( + configuredSettings({ excludedItems: [{ platform: 'instagram', itemId: 'x' }] }), + previous, + ), + ).toBe(false) + }) +}) + +describe('describeInstagramError', () => { + const apiError = (status: number, apiMessage: string | null, apiType: string | null) => + new InstagramApiError(status, apiMessage, apiType, 'fail') + + it('maps an OAuth rejection to the token correction (Instagram Login, never Facebook Login)', () => { + const message = describeInstagramError( + apiError(400, 'Error validating access token: Session has expired', 'OAuthException'), + ) + expect(message).toContain('token') + expect(message).toContain('Instagram Login') + expect(message).toContain('Facebook Login') + }) + + it('maps an invalid token by keyword even without an OAuthException type', () => { + expect( + describeInstagramError(apiError(403, 'Invalid OAuth access token', 'GraphMethodException')), + ).toContain('Instagram Login') + }) + + it('maps an unknown user id to the ID correction', () => { + const message = describeInstagramError(apiError(400, 'Invalid user id', 'OAuthException')) + expect(message).toContain('ID do usuário') + expect(message).not.toContain('Instagram Login') + }) + + it('keeps a token OAuth rejection on the token correction even when the message says "user"', () => { + // "The user must be logged in" contains the word "user" but is a TOKEN + // problem — mapping it to the ID branch would be the very misdiagnosis + // this panel exists to kill. + const message = describeInstagramError( + apiError(400, 'Error validating access token: The user must be logged in', 'OAuthException'), + ) + expect(message).toContain('Instagram Login') + expect(message).not.toContain('ID do usuário') + }) + + it('maps a request timeout (aborted sync) to a timeout reason', () => { + const abort = new Error('This operation was aborted') + abort.name = 'AbortError' + expect(describeInstagramError(abort)).toContain('demorou demais') + }) + + it('maps server errors and network failures honestly', () => { + expect(describeInstagramError(apiError(500, null, null))).toContain('indisponível') + expect(describeInstagramError(new TypeError('fetch failed'))).toContain('rede') + expect(describeInstagramError(new Error('whatever'))).toContain('inesperado') + }) +}) + +describe('syncInstagramFeed', () => { + const okFeedResponse = () => + ({ + ok: true, + json: async () => ({ data: [] }), + }) as unknown as Response + + it('persists snapshot and status and reports success', async () => { + const fetchImpl = vi.fn(async () => okFeedResponse()) + vi.stubGlobal('fetch', fetchImpl) + + const payload = fakePayload(configuredSettings()) + const outcome = await syncInstagramFeed(payload) + + expect(outcome.ok).toBe(true) + expect(outcome.status.lastSyncAt).toBeDefined() + expect(outcome.status.postCount).toBe(0) + expect(payload.db.drizzle.execute).toHaveBeenCalled() + }) + + it('reports the product-language failure status when the API rejects the token', async () => { + vi.stubGlobal( + 'fetch', + vi.fn(async () => + feedResponse( + { error: { message: 'Error validating access token', type: 'OAuthException' } }, + false, + 400, + ), + ), + ) + + const payload = fakePayload(configuredSettings()) + const outcome = await syncInstagramFeed(payload) + + expect(outcome.ok).toBe(false) + expect(outcome.status.error).toContain('Instagram Login') + expect(outcome.status.errorAt).toBeDefined() + }) + + it('returns an empty status without touching the API when not configured', async () => { + const fetchImpl = vi.fn() + vi.stubGlobal('fetch', fetchImpl) + + const payload = fakePayload(configuredSettings({ instagramAccessToken: '' })) + const outcome = await syncInstagramFeed(payload) + + expect(outcome.ok).toBe(false) + expect(outcome.status).toEqual({}) + expect(fetchImpl).not.toHaveBeenCalled() + }) + + it('propagates the abort signal to the fetch', async () => { + const fetchImpl = vi.fn( + async (_input: string, _init?: RequestInit): Promise => okFeedResponse(), + ) + vi.stubGlobal('fetch', fetchImpl) + + const payload = fakePayload(configuredSettings()) + const controller = new AbortController() + await syncInstagramFeed(payload, { signal: controller.signal }) + + expect(fetchImpl.mock.calls[0][1]).toEqual({ signal: controller.signal }) + }) + + it('writes through the transaction database when a transaction is open (hook path)', async () => { + const poolExecute = vi.fn(async () => ({ rowCount: 1 })) + const txExecute = vi.fn(async () => ({ rowCount: 1 })) + const payload = fakePayload(configuredSettings(), { + name: 'postgres', + sessions: { tx1: { db: { execute: txExecute } } }, + drizzle: { execute: poolExecute }, + }) + const fetchImpl = vi.fn(async () => okFeedResponse()) + vi.stubGlobal('fetch', fetchImpl) + + const result = await syncInstagramFeed(payload, { + req: { transactionID: 'tx1' } as unknown as PayloadRequest, + }) + + expect(result.ok).toBe(true) + expect(txExecute).toHaveBeenCalled() + expect(poolExecute).not.toHaveBeenCalled() + }) + + it('falls back to the pool when the transaction database is unavailable', async () => { + const poolExecute = vi.fn(async () => ({ rowCount: 1 })) + // `db.name` not postgres → getPostgresTransactionDatabase throws → pool. + const payload = fakePayload(configuredSettings(), { + name: 'not-postgres', + drizzle: { execute: poolExecute }, + }) + const fetchImpl = vi.fn(async () => okFeedResponse()) + vi.stubGlobal('fetch', fetchImpl) + + const result = await syncInstagramFeed(payload, { + req: { transactionID: 'tx1' } as unknown as PayloadRequest, + }) + + expect(result.ok).toBe(true) + expect(poolExecute).toHaveBeenCalled() + }) +})