208 lines
7.4 KiB
JavaScript
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`);
|
|
} |