Files
Art-gallery/scripts/db-env.js
T
Danila KhodjaefandCursor 8e444fa461 Fix harmonize prod DB targeting and large-hall loading hangs.
Prod env files now win over root .env so harmonize:db can open gallery_prod after gallery_dev; tour_stops gains updated_at; halls no longer block the overlay on every painting texture. Docs cover the fixes and one-artist image pull from prod; Duccio painting assets synced from TrueNAS.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-26 01:26:51 +03:00

125 lines
3.5 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const readline = require('readline');
const rootDir = path.resolve(__dirname, '..');
const PROD_DB_SUFFIX = '_prod';
const DEV_DB_NAME = 'gallery_dev';
const PROD_DB_NAME = 'gallery_prod';
const LEGACY_DB_NAMES = ['Gallery', 'gallery'];
function parseEnvFile(filePath) {
const out = {};
if (!fs.existsSync(filePath)) return out;
const content = fs.readFileSync(filePath, 'utf8');
for (const line of content.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const eq = trimmed.indexOf('=');
if (eq === -1) continue;
const key = trimmed.slice(0, eq).trim();
let value = trimmed.slice(eq + 1).trim();
if (
(value.startsWith('"') && value.endsWith('"'))
|| (value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
out[key] = value;
}
return out;
}
/** Load into process.env without overwriting keys already set. */
function loadEnvFile(filePath) {
const parsed = parseEnvFile(filePath);
for (const [key, value] of Object.entries(parsed)) {
if (process.env[key] === undefined) process.env[key] = value;
}
}
function pgConfigFromMap(env) {
const host = env.DB_HOST || '192.168.10.122';
const port = Number(env.DB_PORT || 5432);
const user = env.DB_USER;
const password = env.DB_PASSWORD;
const database = env.DB_NAME;
if (!user || !password || !database) {
throw new Error('DB_USER, DB_PASSWORD, and DB_NAME are required');
}
return { host, port, user, password, database };
}
function pgConfigFromEnv() {
return pgConfigFromMap(process.env);
}
function assertProdDatabase(dbName) {
if (!dbName.endsWith(PROD_DB_SUFFIX) && dbName !== PROD_DB_NAME) {
throw new Error(
`Refusing prod operation on database "${dbName}". `
+ `Target must be "${PROD_DB_NAME}" or end with "${PROD_DB_SUFFIX}".`,
);
}
return dbName;
}
function assertDevDatabase(dbName) {
if (dbName === PROD_DB_NAME || dbName.endsWith(PROD_DB_SUFFIX)) {
throw new Error(
`Refusing dev operation on production database "${dbName}". `
+ `Use "${DEV_DB_NAME}" for development.`,
);
}
return dbName;
}
/**
* File values win over process.env so loading prod after dev (or vice versa)
* in the same process cannot leave the wrong DB_NAME stuck.
*/
function loadDevPgConfig() {
const filePath = path.join(rootDir, '.env');
const fileEnv = parseEnvFile(filePath);
const config = pgConfigFromMap({ ...process.env, ...fileEnv });
assertDevDatabase(config.database);
return config;
}
function loadProdPgConfig() {
const filePath = path.join(rootDir, 'infra', 'docker', '.env.prod');
const fileEnv = parseEnvFile(filePath);
const config = pgConfigFromMap({ ...process.env, ...fileEnv });
assertProdDatabase(config.database);
return config;
}
async function confirmProdAction(message) {
if (process.env.CONFIRM_PROD === '1') return;
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const answer = await new Promise((resolve) => {
rl.question(`${message}\nType "yes" to continue: `, resolve);
});
rl.close();
if (answer.trim().toLowerCase() !== 'yes') {
throw new Error('Aborted.');
}
}
module.exports = {
PROD_DB_SUFFIX,
DEV_DB_NAME,
PROD_DB_NAME,
LEGACY_DB_NAMES,
rootDir,
parseEnvFile,
loadEnvFile,
pgConfigFromEnv,
assertProdDatabase,
assertDevDatabase,
loadDevPgConfig,
loadProdPgConfig,
confirmProdAction,
};