Compare commits
2 Commits
84632e043f
...
da5f8360bc
| Author | SHA1 | Date | |
|---|---|---|---|
| da5f8360bc | |||
| 37af1febcc |
@@ -0,0 +1,31 @@
|
||||
-- ============================================================================
|
||||
-- Fixes: password_change_required + audit trigger UUID cast
|
||||
-- ============================================================================
|
||||
|
||||
-- 1. All users use the passwords already set in the seed — no forced change
|
||||
UPDATE users SET password_change_required = FALSE WHERE password_change_required = TRUE;
|
||||
|
||||
-- 2. Fix audit_password_change trigger: current_setting returns TEXT but
|
||||
-- audit_logs.changed_by is UUID — cast to UUID to prevent type error
|
||||
CREATE OR REPLACE FUNCTION audit_password_change()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF OLD.password_hash IS DISTINCT FROM NEW.password_hash THEN
|
||||
INSERT INTO audit_logs (
|
||||
table_name, record_id, action, old_data, new_data, changed_by, ip_address
|
||||
) VALUES (
|
||||
'users',
|
||||
NEW.id,
|
||||
'UPDATE',
|
||||
jsonb_build_object('password_changed', true, 'password_change_required', OLD.password_change_required),
|
||||
jsonb_build_object('password_changed', true, 'password_change_required', NEW.password_change_required),
|
||||
NULLIF(current_setting('app.current_user_id', true), '')::UUID,
|
||||
NULLIF(current_setting('app.current_ip', true), '')
|
||||
);
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- 3. Re-enable the trigger (was disabled as workaround for the UUID bug)
|
||||
ALTER TABLE users ENABLE TRIGGER trg_audit_password_change;
|
||||
@@ -75,5 +75,9 @@ BEGIN;
|
||||
|
||||
\echo '=== Running 019_allow_null_start_time.sql (Allow Null Start Time) ==='
|
||||
\i 019_allow_null_start_time.sql
|
||||
|
||||
\echo '=== Running 020_fixes.sql (password_change_required + audit trigger fix) ==='
|
||||
\i 020_fixes.sql
|
||||
|
||||
\echo '=== Migration Complete ==='
|
||||
COMMIT;
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
"dev:repair": "node scripts/code-repair-agent.mjs --watch",
|
||||
"dev:start": "concurrently -n REPAIR,AI,BROWSE,SIGNAL,NEXT,OPEN -c red,cyan,magenta,yellow,green,white \"npm run dev:repair\" \"npm run dev:rust\" \"npm run dev:browser-use\" \"npm run dev:signaling\" \"npm run dev:next\" \"npm run dev:open\"",
|
||||
"dev:next": "next dev -p 3006",
|
||||
"dev:precheck": "node scripts/precheck.mjs",
|
||||
"dev:precheck": "node scripts/precheck.mjs && node scripts/run-migrations.mjs",
|
||||
"dev:ollama": "node scripts/ensure-ollama.mjs",
|
||||
"dev:rust": "node ai-server/index.mjs",
|
||||
"dev:browser-use": "cd browser-use-service && node ../scripts/run-python.mjs main.py",
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
// ── Automated Database Migration Runner ──────────────────────────────
|
||||
// Reads .env.local for DATABASE_URL, tracks applied migrations in
|
||||
// a _migrations table, and runs unapplied .sql files via psql.
|
||||
// Runs automatically on npm run dev and npm run setup.
|
||||
// ============================================================================
|
||||
|
||||
import { readFileSync, existsSync } from "node:fs"
|
||||
import { execSync } from "node:child_process"
|
||||
import { resolve, dirname } from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { createRequire } from "node:module"
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const ROOT = resolve(__dirname, "..")
|
||||
|
||||
// ── Load .env.local into process.env ─────────────────────────────────
|
||||
|
||||
const envPath = resolve(ROOT, ".env.local")
|
||||
if (existsSync(envPath)) {
|
||||
for (const line of readFileSync(envPath, "utf-8").split("\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)
|
||||
}
|
||||
if (!process.env[key]) process.env[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
const DATABASE_URL = process.env.DATABASE_URL
|
||||
if (!DATABASE_URL) {
|
||||
console.log(" ~ DATABASE_URL not set — skipping migrations")
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// ── Find psql ────────────────────────────────────────────────────────
|
||||
|
||||
function findPsql() {
|
||||
try {
|
||||
execSync("psql --version", { stdio: "pipe", timeout: 5000 })
|
||||
return "psql"
|
||||
} catch {}
|
||||
|
||||
const candidates = [
|
||||
"C:\\Program Files\\PostgreSQL\\16\\bin\\psql.exe",
|
||||
"C:\\Program Files\\PostgreSQL\\17\\bin\\psql.exe",
|
||||
"C:\\Program Files\\PostgreSQL\\15\\bin\\psql.exe",
|
||||
"C:\\Program Files\\PostgreSQL\\14\\bin\\psql.exe",
|
||||
]
|
||||
for (const p of candidates) {
|
||||
if (existsSync(p)) return `"${p}"`
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const PSQL = findPsql()
|
||||
if (!PSQL) {
|
||||
console.log(" ~ psql not found — skipping migrations (install PostgreSQL client tools)")
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// ── Database connection (for tracking table) ─────────────────────────
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const { Pool } = require("pg")
|
||||
|
||||
let pool
|
||||
try {
|
||||
pool = new Pool({
|
||||
connectionString: DATABASE_URL,
|
||||
max: 1,
|
||||
connectionTimeoutMillis: 5000,
|
||||
idleTimeoutMillis: 10000,
|
||||
})
|
||||
await pool.query("SELECT 1")
|
||||
} catch {
|
||||
console.log(" ~ Database not available — skipping migrations")
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// ── Migration logic ──────────────────────────────────────────────────
|
||||
|
||||
async function runMigrations() {
|
||||
// 1. Create tracking table if not exists
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS _migrations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
filename VARCHAR(255) NOT NULL UNIQUE,
|
||||
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)
|
||||
`)
|
||||
|
||||
// 2. Determine file order from run_all.sql (authoritative)
|
||||
const runAllPath = resolve(ROOT, "database", "migrations", "run_all.sql")
|
||||
const runAllContent = readFileSync(runAllPath, "utf-8")
|
||||
const fileOrder = []
|
||||
for (const line of runAllContent.split("\n")) {
|
||||
const match = line.match(/\\i\s+(\S+\.sql)/)
|
||||
if (match) fileOrder.push(match[1])
|
||||
}
|
||||
|
||||
if (fileOrder.length === 0) {
|
||||
console.log(" ~ No migration files found in run_all.sql")
|
||||
return
|
||||
}
|
||||
|
||||
// 3. Get already-applied files
|
||||
let appliedSet
|
||||
try {
|
||||
const { rows: applied } = await pool.query("SELECT filename FROM _migrations ORDER BY filename")
|
||||
appliedSet = new Set(applied.map((r) => r.filename))
|
||||
} catch {
|
||||
appliedSet = new Set()
|
||||
}
|
||||
|
||||
// 4. Build psql command base
|
||||
const psqlCmd = `${PSQL} -d "${DATABASE_URL}" -v ON_ERROR_STOP=1`
|
||||
|
||||
// 5. Run unapplied files in order.
|
||||
// psql -v ON_ERROR_STOP=1 aborts on the first error with exit code 3.
|
||||
// If the error is "already exists", the file was already applied before
|
||||
// tracking existed — mark it as applied and continue.
|
||||
// Other errors are genuine failures — abort.
|
||||
const ALREADY_APPLIED_PATTERNS = [
|
||||
"already exists",
|
||||
"duplicate key",
|
||||
"cannot insert multiple commands into a prepared statement",
|
||||
]
|
||||
|
||||
let count = 0
|
||||
for (const file of fileOrder) {
|
||||
if (appliedSet.has(file)) continue
|
||||
|
||||
const filePath = resolve(ROOT, "database", "migrations", file)
|
||||
if (!existsSync(filePath)) {
|
||||
console.error(` ✗ Migration file not found: ${file}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
let stderr = ""
|
||||
try {
|
||||
const result = execSync(`${psqlCmd} -f "${filePath}"`, {
|
||||
timeout: 30000,
|
||||
encoding: "utf-8",
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
})
|
||||
// psql outputs notices/warnings to stderr even on success
|
||||
stderr = result.stderr || ""
|
||||
} catch (err) {
|
||||
const msg = err.stderr || err.stdout || err.message
|
||||
const isAlreadyApplied = ALREADY_APPLIED_PATTERNS.some((p) => msg.includes(p))
|
||||
if (isAlreadyApplied) {
|
||||
await pool.query("INSERT INTO _migrations (filename) VALUES ($1) ON CONFLICT DO NOTHING", [file])
|
||||
console.log(` ~ Already applied: ${file}`)
|
||||
count++
|
||||
continue
|
||||
}
|
||||
console.error(` ✗ Migration failed: ${file}`)
|
||||
console.error(` ${msg.split("\n").slice(0, 5).join("\n ")}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
await pool.query("INSERT INTO _migrations (filename) VALUES ($1)", [file])
|
||||
console.log(` ✓ Applied: ${file}`)
|
||||
count++
|
||||
}
|
||||
|
||||
if (count === 0) {
|
||||
console.log(" ~ All migrations up to date")
|
||||
}
|
||||
}
|
||||
|
||||
// ── Run ──────────────────────────────────────────────────────────────
|
||||
|
||||
try {
|
||||
await runMigrations()
|
||||
} finally {
|
||||
await pool.end()
|
||||
}
|
||||
+11
-2
@@ -202,7 +202,16 @@ if (!existsSync(resolve(ROOT, ".env.local"))) {
|
||||
console.log("\n── .env.local already exists, skipping ──")
|
||||
}
|
||||
|
||||
// 5. Self-healing build check (--self-heal flag)
|
||||
// 5. Database migrations (auto-applies on fresh install)
|
||||
console.log("\n── Running Database Migrations ──")
|
||||
try {
|
||||
execSync("node scripts/run-migrations.mjs", { stdio: "inherit", timeout: 30000, cwd: ROOT })
|
||||
console.log(" ✓ Migrations complete")
|
||||
} catch {
|
||||
console.log(" ~ Database not available — run migrations later with: npm run dev")
|
||||
}
|
||||
|
||||
// 6. Self-healing build check (--self-heal flag)
|
||||
if (doSelfHeal) {
|
||||
console.log("\n── Self-Healing Build Check ──")
|
||||
const { generateModule } = loadRegistry()
|
||||
@@ -252,7 +261,7 @@ if (doSelfHeal) {
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Final summary
|
||||
// 7. Final summary
|
||||
console.log("\n── Final Steps ──")
|
||||
console.log(" 1. Make sure PostgreSQL is running with database 'crm'")
|
||||
console.log(" 2. Pull the Ollama model: ollama pull dolphin-llama3:8b")
|
||||
|
||||
Reference in New Issue
Block a user