// Gallring (ALVA · TÜV T-4). // // ---- Varför den här filen finns --------------------------------------- // // `gallras_efter` räknades fram vid avslut, utifrån ärendetypen, och // skrevs till ärenderaden. Sedan hände ingenting. En sökning genom hela // repot visade att INGENTING läste kolumnen — ingen körning, inget // schemalagt jobb, ingen fråga, inget CI-steg. Lagringsbegränsningen var // en nedskriven avsikt, inte en verkställd kontroll. // // Det gjorde också att ett annat fel fick leva: triggern som förbjöd all // update gjorde det omöjligt att ens SKRIVA kolumnen, och ingen märkte // det, därför att ingenting längre ned konsumerade värdet. // // ---- Vad gallring är här ---------------------------------------------- // // Inte radering av ärenden. Loggen är append-only och ska förbli det — // protokollet över vad som kontrollerades har ett värde långt efter att // identifieringen har det. Gallring är att förstöra personnyckeln, precis // som en raderingsbegäran gör. Kvar blir ett ärende som fortfarande kan // visa att lufttrycket mättes till 2,4 bar klockan 08:42, bara inte // längre vems bil det gällde. // // ---- Försiktighetsregeln ---------------------------------------------- // // En nyckel förstörs bara när SAMTLIGA ärenden som delar den har passerat // sitt gallringsdatum. Ett fordon som varit inne fem gånger har fem // ärenden och en nyckel; att gallra på det äldsta hade gjort de fyra // senaste oläsbara i förtid. Ett ärende utan gallringsdatum — öppet, eller // avslutat innan datum sattes — räknas som ännu inte passerat, eftersom // för tidig gallring inte går att ångra. import pg from "pg"; import { createHash } from "node:crypto"; /** * Hittar och förstör nycklar vars samtliga ärenden har passerat sin tid. * * @param pool pg.Pool * @param nu tidpunkt att jämföra mot; injicerad för testbarhet * @param torrkor när true räknas allt fram men ingenting förstörs */ export async function gallra(pool, { nu = new Date(), torrkor = false } = {}) { // Ett subjekt i personnycklar är ett ärende-id (se personnyckel() i // server.mjs). Nyckeln kan delas av flera ärenden på samma fordon via // det blindade indexet, så gruppen måste bildas på indexet — inte på // subjektet — annars gallras en delad nyckel för tidigt. const kandidater = await pool.query( `with nyckelns_arenden as ( select n.id as nyckel_id, n.organisation_id, a.gallras_efter, coalesce(a.identifierare_index, a.id) as grupp from personnycklar n join felsokning_arenden a on a.organisation_id = n.organisation_id and a.id = n.subjekt ), samma_fordon as ( select k.nyckel_id, k.organisation_id, b.gallras_efter from nyckelns_arenden k join felsokning_arenden b on b.organisation_id = k.organisation_id and coalesce(b.identifierare_index, b.id) = k.grupp ) select nyckel_id, organisation_id, count(*)::int as antal_arenden from samma_fordon group by nyckel_id, organisation_id having bool_and(gallras_efter is not null and gallras_efter <= $1)`, [nu], ); if (kandidater.rowCount === 0 || torrkor) { return { forstorda: 0, arenden: 0, kandidater: kandidater.rowCount, torrkor }; } let forstorda = 0; let arenden = 0; for (const rad of kandidater.rows) { const klient = await pool.connect(); try { await klient.query("begin"); const bort = await klient.query(`delete from personnycklar where id = $1 returning id`, [rad.nyckel_id]); if (bort.rowCount > 0) { // Gallringen skrivs i samma register som en begärd radering, med // ett hashat subjekt: registret får inte självt bära uppgiften // det bevisar att någon gjort oåtkomlig. await klient.query( `insert into raderingar (organisation_id, subjekt_hash, begard, begard_av, antal_arenden) values ($1, $2, $3, $4, $5)`, [ rad.organisation_id, createHash("sha256").update(`gallring:${rad.nyckel_id}`).digest("hex"), nu, "Gallring (ALVA-PROC-0040)", rad.antal_arenden, ], ); forstorda += 1; arenden += rad.antal_arenden; } await klient.query("commit"); } catch (fel) { await klient.query("rollback").catch(() => {}); throw fel; } finally { klient.release(); } } return { forstorda, arenden, kandidater: kandidater.rowCount, torrkor }; } // Körs som eget jobb (CronJob i klustret), inte i webbtjänstens process: // en gallring som avbryts av en omstart mitt i ska kunna köras om, och // den ska synas som en egen körning i driften. // // node gallring.mjs verkställer // node gallring.mjs --torrkor räknar bara if (import.meta.url === `file://${process.argv[1]}`) { const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL, max: 2 }); try { const utfall = await gallra(pool, { torrkor: process.argv.includes("--torrkor") }); console.log( JSON.stringify({ nivå: "info", meddelande: "gallring", tid: new Date().toISOString(), ...utfall, }), ); } finally { await pool.end(); } }