Files
jlinc-server/backend/db/index.js
2026-08-20 15:08:32 +00:00

208 lines
7.4 KiB
JavaScript

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`);
}