teqo-backup/scripts/lib/readOnlyExtract.mjs
Francisco Solla cb5854d17e feat(C187): dossiê Solla por instituição + boletim modelo (skill + builder) (Closes #1135)
Generaliza aditivamente o pipeline C186 (default município; specs verdes) via o
descritor scripts/lib/dossieUnit.mjs (municipality|institution), extrai o emit
A4/fit-guard para scripts/lib/buildPdf.mjs e a receita read-only para
scripts/lib/readOnlyExtract.mjs.

- Catálogo fail-closed src/lib/institutionCatalog.ts + institutionNameAliases.ts
  (seed UFBA/Correios/Enfermagem; escape one-off --slug/--name; dedup).
- Skill/command/subagente /dossie-solla-instituicao; eras A/B/C reusadas.
- Abrangência instituição × setor × rede nunca somada; era sem fonte vira página
  de lacuna explícita; lista capada declara "e mais N" (regra C188 no dono).
- Boletim modelo 1×A4 com fase junto ao valor, sem fontes, sem CTA; herda apenas
  fatos com fonte do dossiê (incl. falas do acervo read-only).
- Sem schema/migration/DB write; artefatos gitignored.
2026-09-17 19:25:13 -03:00

67 lines
2.4 KiB
JavaScript

/**
* Read-only extraction contract (owner: the C163 report pipeline; reused by the
* C187 institutional dossiê). Both extractors run where production lives, only
* ever read, and refuse to guess the target: an explicit confirmation env plus
* a connection forced read-only.
*
* Extracted so the actor resolution and the read-only session have one owner —
* the institution extractor must not twin the subtle fail-closed recipe.
*/
import {
databaseLabel,
READ_ONLY_PG_OPTIONS,
withReadOnlyDatabaseUrl,
} from './cityReportDatabase.mjs'
import { databaseHostname, isTruthyEnv } from './cli.mjs'
/**
* Resolves a real campaign actor that can see everything (candidate, then
* coordinator). `die` is injected so each CLI keeps its own label/exit.
*
* @param {any} payload
* @param {{ die: (message: string) => void, roles?: string[] }} options
* @returns {Promise<any>}
*/
export const resolveCampaignActor = async (
payload,
{ die, roles = ['candidate', 'coordinator'] },
) => {
for (const role of roles) {
const result = await payload.find({
collection: 'campaignUser',
where: { role: { equals: role } },
depth: 0,
limit: 1,
pagination: false,
overrideAccess: true,
})
const doc = result.docs[0]
if (doc) return { ...doc, collection: 'campaignUser' }
}
die(
`Nenhum campaignUser com papel ${roles.join('/')} encontrado — o relatório exige um ator real que enxergue tudo.`,
)
}
/**
* Enforces the explicit opt-in and rewrites `DATABASE_URL` to force
* `default_transaction_read_only=on`. Returns the raw URL (for labels).
*
* @param {{ confirmEnv: string, label: string, die: (message: string) => void, context?: string | null }} params
* @returns {string | undefined}
*/
export const enterReadOnlySession = ({ confirmEnv, label, die, context = null }) => {
if (!isTruthyEnv(process.env[confirmEnv])) {
die(
`Opt-in explícito ausente: rode com ${confirmEnv}=1.\n` +
` Alvo: ${databaseHostname(process.env.DATABASE_URL) ?? 'DATABASE_URL ausente'} — a leitura é read-only, mas o comando não adivinha o alvo.`,
)
}
const rawDatabaseUrl = process.env.DATABASE_URL
process.env.DATABASE_URL = withReadOnlyDatabaseUrl(rawDatabaseUrl, READ_ONLY_PG_OPTIONS)
console.log(
`[${label}] alvo ${databaseLabel(rawDatabaseUrl)} · sessão read-only${context ? ` · ${context}` : ''}`,
)
return rawDatabaseUrl
}