import fs from "fs"; import path from "path"; import { createHash } from "crypto"; import pkg from "pg"; const { Pool } = pkg; import { sleep } from "../common/sleep.js"; import { getConfig } from "../common/config.js"; import { firstNonBlankLine } from "../util/firstNonBlankLine.js"; // NOTE: the domain `data` layer (which pulls in @jlinc/core) is imported lazily // inside populateAgreements() only. Keeping it out of this module's top-level // imports lets the pool/migrate code — and anything that just needs getPool() — // load without dragging in the crypto engine. let pool; export async function init() { const config = getConfig(); let ready = false; let client; while (!ready) { try { pool = new Pool({ connectionString: config.postgresUrl, }); client = await pool.connect(); const res = await client.query(`SELECT 1`); if (res.rows.length < 1) { throw new Error(""); } ready = true; // eslint-disable-next-line no-unused-vars } catch (e) { console.log("DB not ready, waiting..."); await sleep(1000); } } await client.release(); } export async function getPool() { return await pool.connect(); } export async function close() { console.log(`Closing DB`); await pool.end(); } export async function migrate() { console.log(`Starting migration`); const client = await pool.connect(); try { const migrationExists = await client.query(` SELECT CASE WHEN (SELECT COUNT(1) FROM information_schema.tables WHERE table_schema = 'system' AND table_name = 'migrate') > 0 THEN TRUE ELSE FALSE END AS exists `); if (!migrationExists.rows[0].exists) { await client.query(` DROP SCHEMA IF EXISTS system CASCADE; CREATE SCHEMA system; -- Migrations DROP TABLE IF EXISTS system.migrate CASCADE; CREATE TABLE system.migrate ( id TEXT NOT NULL, status TEXT NOT NULL, created_ts TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx__system__migrate__id ON system.migrate (id); CREATE INDEX idx__system__migrate__status ON system.migrate (status); CREATE INDEX idx__system__migrate__created_ts ON system.migrate (created_ts); `); } let lastMigration = "0"; const lastMigrationResult = await client.query(` SELECT id FROM system.migrate ORDER BY id DESC LIMIT 1; `); if (lastMigrationResult.rows.length > 0 && lastMigrationResult.rows[0].id) { lastMigration = lastMigrationResult.rows[0].id; } const files = fs.readdirSync("./db/migrations", { withFileTypes: true, }); for await (const file of files) { const migrationId = file.name.slice(0, 6); if (migrationId > lastMigration) { const label = file.name.slice(7, file.name.length - 4); console.log(`Running migration ${label} (${migrationId})`); const sqlStr = fs.readFileSync(path.join("./db/migrations", file.name), "utf8"); try { await client.query("BEGIN"); await client.query(sqlStr); await client.query( ` INSERT INTO system.migrate ( id, status ) VALUES ( $1, 'complete' ); `, [migrationId], ); await client.query("COMMIT"); } catch (e) { console.error(e); await client.query("ROLLBACK"); throw new Error("migration error"); } } } } catch (e) { console.error(e); } finally { await client.release(); } console.log(`Ending migration`); } export async function populateAgreements() { console.log(`Starting agreement population`); const config = getConfig(); const { data } = await import("../modules/core/data/index.js"); const client = await pool.connect(); try { const files = fs.readdirSync("./db/agreements", { withFileTypes: true, }); for await (const file of files) { if (file.name.endsWith('.json')) continue; const agreementUuid = file.name.slice(0, 36); const markdown = fs.readFileSync(path.join("./db/agreements", file.name), "utf8").trim(); const json = JSON.parse(fs.readFileSync(path.join("./db/agreements", file.name.replace('.md', '.json')), "utf8").trim()); const title = firstNonBlankLine(markdown); const hash = createHash('sha256') .update(markdown) .digest('hex') try { const agreementExists = await client.query(` SELECT CASE WHEN (SELECT COUNT(1) FROM agreement_content WHERE title = $1 AND hash = $2 AND user_id IS NULL) > 0 THEN TRUE ELSE FALSE END AS exists `, [ title, hash, ] ); if (!agreementExists.rows[0].exists) { console.log(`Adding agreement '${title}' (${agreementUuid})`); // Agreement content is immutable once inserted (it may already be // signed), so this is INSERT-only — never an upsert. await client.query(` INSERT INTO agreement_content ( title, markdown, hash, key, agreement_uuid ) VALUES ( $1, $2, $3, $4, $5 ); `, [ title, markdown, hash, json.key, agreementUuid, ]); const agreement = { uri: `${config.publicCoreUrl}/agreements/${hash}`, purposes: json.purposes || [], prohibitions: json.prohibitions || [], shortNames: [], validRoles: json.validRoles || [], } await data.agreement.create(agreement, null, client, agreementUuid); } } catch (e) { console.error(e); throw new Error("agreement population error"); } } } catch (e) { console.error(e); } finally { await client.release(); } console.log(`Ending agreement population`); }