Files
shd.build/scripts/db-migrate.mjs
T

102 lines
4.1 KiB
JavaScript

import { connect } from '@tursodatabase/database';
import { access, mkdir, readFile } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import process from 'node:process';
import { initializeDefaultUsers } from './default-users.mjs';
const databasePath = resolve(process.env.TDR_DATABASE_PATH || './data/shd-build.db');
const schemaPath = resolve('./database/schema.sql');
let databaseAlreadyExisted = true;
try {
await access(databasePath);
} catch {
databaseAlreadyExisted = false;
}
await mkdir(dirname(databasePath), { recursive: true });
const database = await connect(databasePath);
const schema = await readFile(schemaPath, 'utf8');
const usersTable = await (await database.prepare(
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='users'"
)).get();
if (usersTable) {
const userColumns = await (await database.prepare("PRAGMA table_info('users')")).all();
if (!userColumns.some((column) => column.name === 'login_username')) {
console.log('Separating optional local login usernames from internal account identifiers.');
await database.exec('ALTER TABLE users ADD COLUMN login_username TEXT');
await database.exec('UPDATE users SET login_username=username WHERE password_hash IS NOT NULL');
}
}
// The project schema deliberately avoids semicolons inside string literals.
const statements = schema
.split(';')
.map((statement) => statement.trim())
.filter(Boolean);
for (const statement of statements) {
await database.exec(statement);
}
const loginEventsDefinition = await (await database.prepare(
"SELECT sql FROM sqlite_master WHERE type='table' AND name='login_events'"
)).get();
if (typeof loginEventsDefinition?.sql === 'string' && !loginEventsDefinition.sql.includes('magic_link')) {
console.log('Expanding login event providers for passwordless authentication.');
await database.exec('ALTER TABLE login_events RENAME TO login_events_legacy');
await database.exec(`CREATE TABLE login_events (
id TEXT PRIMARY KEY,
user_id TEXT REFERENCES users(id) ON DELETE SET NULL,
provider TEXT NOT NULL CHECK(provider IN ('password', 'discord', 'magic_link', 'passkey')),
succeeded INTEGER NOT NULL CHECK(succeeded IN (0, 1)),
created_at TEXT NOT NULL
)`);
await database.exec(`INSERT INTO login_events(id,user_id,provider,succeeded,created_at)
SELECT id,user_id,provider,succeeded,created_at FROM login_events_legacy`);
await database.exec('DROP TABLE login_events_legacy');
await database.exec('CREATE INDEX IF NOT EXISTS login_events_user_created_idx ON login_events(user_id, created_at DESC)');
}
const loginEventColumns = await (await database.prepare("PRAGMA table_info('login_events')")).all();
for (const [name, definition] of [
['ip_address', 'TEXT'],
['user_agent', 'TEXT'],
['client_name', 'TEXT'],
['operating_system', 'TEXT'],
['device_type', 'TEXT'],
['location_country', 'TEXT'],
['location_region', 'TEXT'],
['location_city', 'TEXT'],
['failure_reason', 'TEXT'],
['risk_flags', 'TEXT'],
['credential_id', 'TEXT'],
['session_id', 'TEXT REFERENCES sessions(id) ON DELETE SET NULL'],
['session_ended_at', 'TEXT'],
['session_end_reason', 'TEXT'],
['sensitive_purged_at', 'TEXT']
]) {
if (!loginEventColumns.some((column) => column.name === name)) {
await database.exec(`ALTER TABLE login_events ADD COLUMN ${name} ${definition}`);
}
}
const proposalColumns = await (await database.prepare("PRAGMA table_info('data_change_proposals')")).all();
for (const [name, definition] of [
['draft_entity_id', 'TEXT'],
['applied_entity_id', 'TEXT']
]) {
if (!proposalColumns.some((column) => column.name === name)) {
await database.exec(`ALTER TABLE data_change_proposals ADD COLUMN ${name} ${definition}`);
}
}
await database.exec("UPDATE data_change_proposals SET draft_entity_id=id WHERE draft_entity_id IS NULL AND entity_id IS NULL");
await database.exec('CREATE UNIQUE INDEX IF NOT EXISTS data_change_proposals_draft_entity_idx ON data_change_proposals(draft_entity_id) WHERE draft_entity_id IS NOT NULL');
console.log(`Applied ${statements.length} schema statements to ${databasePath}`);
if (!databaseAlreadyExisted) {
console.log('New database detected; creating default users.');
await initializeDefaultUsers(database);
}