Een dating-app op basis van brieven, niet van gezichten!
Deze repository wordt als momentopname gepubliceerd: een clone bevat de volledige inhoud, maar geen commitgeschiedenis.
/*
* Technische zelftest van één installatie: staat alles wat de app nodig heeft er nog, en werkt het?
*
* Dit is bewust géén onderdeel van `npm run health`. Dat rapport gaat over de gemeenschap — hoeveel
* mensen je mag aanschrijven, of je brief beantwoord wordt — en is bedoeld om gelezen te worden door
* iemand die over het product nadenkt. Dit hier gaat over de machine, en wordt gelezen door iemand
* die net uitgerold heeft of die iets raars ziet. Twee vragen, twee scripts.
*
* Wat het nakijkt, en waarom juist dit:
*
* database Draait hij, en kunnen we erbij? Zonder dit werkt niets.
* schema Staan alle tabellen uit db/schema.sql er echt? Dit vangt de gevaarlijkste uitrolfout
* die er is: nieuwe code op een oude database. Die valt pas om bij de eerste gebruiker
* die de nieuwe kolom raakt, en dan met een 500 waar niemand bij staat te kijken.
* betaalketen Sleutel → token → rechten bij Google. Dit is de stilste storing van allemaal: is de
* gratis bak leeg en hapert de kassa, dan komt er geen enkel nieuw lid meer binnen, de
* app ziet er verder kerngezond uit, en het enige spoor is een logregel.
* promotieteam Staat het slot op /promotieteam en is de ledenlijst bereikbaar? Die database staat
* los van de app (zie server/promotie.js) en komt dus ook niet mee in het schema hierboven.
*
* Afsluitcode 0 als alles in orde is, 1 als er iets kapot is — zo is hij ook na een uitrol te
* gebruiken zonder de uitvoer te lezen.
*
* npm run doctor
* cd /opt/liefdesbrieven && set -a && . ./.env && set +a && npm run doctor # op de server
*/
import { readFileSync, readdirSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import { pool } from "../server/db.js";
import { billingSelfTest } from "../server/billing.js";
import { promotieSelfTest, promotiePoolEnd } from "../server/promotie.js";
import { FREE_ACCOUNTS } from "../src/constants.js";
const HIER = dirname(fileURLToPath(import.meta.url));
/* ---------- opmaak ---------- */
const uitkomsten = [];
const meld = (naam, staat, tekst) => uitkomsten.push({ naam, staat, tekst });
/* null = niet vast te stellen (bijvoorbeeld doordat het schema stuk is). Alleen `true` rechtvaardigt
de uitspraak dat er niemand meer binnenkomt — anders zou dit script gaan gissen. */
let bakLeeg = null;
/* ---------- database en schema ---------- */
async function database() {
let versie;
try {
const { rows } = await pool.query("select current_setting('server_version') as v, current_database() as db");
versie = rows[0];
} catch (err) {
meld("database", "kapot", `geen verbinding: ${err.message}`);
return false;
}
meld("database", "goed", `postgres ${versie.v}, database ${versie.db}`);
/*
* De waarheid over het schema staat in db/schema.sql, niet in een lijstje hier: dan kan het niet
* verouderen. Wat er in het bestand `create table` heet, hoort in de database te staan.
*/
const sql = readFileSync(join(HIER, "..", "db", "schema.sql"), "utf8");
const verwacht = [...sql.matchAll(/^create table (?:if not exists )?(\w+)/gim)].map((m) => m[1]);
const { rows } = await pool.query(
`select table_name from information_schema.tables where table_schema = 'public'`
);
const aanwezig = new Set(rows.map((r) => r.table_name));
const ontbreekt = verwacht.filter((t) => !aanwezig.has(t));
if (ontbreekt.length) {
meld("schema", "kapot", `${ontbreekt.length} van de ${verwacht.length} tabellen ontbreken: ${ontbreekt.join(", ")}` +
` — de code loopt vóór op de database; draai db:migrate (op een ontwikkelmachine: db:setup)`);
return false;
}
meld("schema", "goed", `alle ${verwacht.length} tabellen uit db/schema.sql staan er`);
/*
* Openstaande migraties zijn dezelfde uitrolfout als een ontbrekende tabel — nieuwe code op een
* oude database — maar fijnmaziger: ook een kolom of index die alleen nog in db/migrations/
* bestaat valt pas om bij de eerste gebruiker die hem raakt. De tabellencheck hierboven ziet
* dat niet; deze wel.
*/
let migraties = [];
try {
migraties = readdirSync(join(HIER, "..", "db", "migrations")).filter((f) => f.endsWith(".sql"));
} catch { /* map bestaat nog niet: er is geen migratie geschreven */ }
const { rows: klaar } = await pool.query("select filename from schema_migrations");
const gedaan = new Set(klaar.map((r) => r.filename));
const open = migraties.filter((f) => !gedaan.has(f)).sort();
if (open.length) {
meld("migraties", "kapot", `${open.length} openstaand: ${open.join(", ")} — draai db:migrate`);
return false;
}
meld("migraties", "goed", migraties.length
? `alle ${migraties.length} uitgevoerd`
: "geen migraties — db/schema.sql is de hele stand");
return true;
}
/* ---------- betaalketen ---------- */
async function betalen(schemaGoed) {
/*
* Of de kassa dragend is, hangt af van de gratis bak. Zolang daar plekken in zitten komt een nieuw
* lid er ook zonder betalen in, en is een stukke keten wel een probleem maar geen dichte deur. Dat
* onderscheid bepaalt of dit script hierop faalt of alleen waarschuwt.
*/
if (schemaGoed) {
try {
const { rows } = await pool.query("select granted from signup_slots");
const uit = rows[0]?.granted ?? 0;
bakLeeg = uit >= FREE_ACCOUNTS;
meld("gratis plekken", "goed", `${uit} van de ${FREE_ACCOUNTS} uitgegeven` +
(bakLeeg ? " — de bak is leeg, dus betalen is de enige weg naar binnen" : " — er kan nog gratis in"));
} catch (err) {
meld("gratis plekken", "kapot", err.message);
}
}
const t = await billingSelfTest();
if (t.ok) {
meld("betaalketen", "goed", t.uitleg);
return true;
}
/* Kapot én de bak leeg = niemand komt er meer in. Kapot met plekken over = repareren voor het zover is. */
meld("betaalketen", bakLeeg === false ? "let op" : "kapot", `${t.stap}: ${t.uitleg}`);
return bakLeeg === false;
}
/* ---------- draaien ---------- */
try {
const schemaGoed = await database();
const betaalGoed = await betalen(schemaGoed);
/* De ledenlijst van het promotieteam: eigen database, eigen slot, dus een eigen regel */
const p = await promotieSelfTest();
meld("promotieteam", p.ok ? "goed" : p.hard ? "kapot" : "let op", p.uitleg);
const breed = Math.max(...uitkomsten.map((u) => u.naam.length));
const merk = { goed: " ", "let op": "! ", kapot: "X " };
console.log("Liefdesbrieven — technische zelftest");
console.log("─".repeat(78));
for (const u of uitkomsten) console.log(`${merk[u.staat]}${u.naam.padEnd(breed)} : ${u.tekst}`);
console.log("");
const kapot = uitkomsten.filter((u) => u.staat === "kapot");
if (kapot.length) {
console.log(`KAPOT: ${kapot.map((u) => u.naam).join(", ")}. Zie hierboven wat eraan scheelt.`);
/* Alleen zeggen als we het wéten: met een kapot schema is de bak niet af te lezen. */
if (!betaalGoed && bakLeeg === true && kapot.some((u) => u.naam === "betaalketen"))
console.log(`Zolang dit zo staat komt er NIEMAND meer binnen: iedereen die zich aanmeldt loopt op de kassa vast.`);
process.exitCode = 1;
} else if (uitkomsten.some((u) => u.staat === "let op")) {
console.log(`Alles draait, maar er staat iets open. Nu te repareren zonder dat iemand er last van heeft.`);
} else {
console.log(`Alles in orde.`);
}
} finally {
await pool.end();
await promotiePoolEnd();
}