import fs from 'node:fs'; import mysql from 'mysql2/promise'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { ensureDefaultSpaces } from './mindspace.mjs'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); export function isDatabaseConfigured() { return Boolean( process.env.DATABASE_URL || (process.env.MYSQL_HOST && process.env.MYSQL_DATABASE), ); } export function createDbPool() { if (!isDatabaseConfigured()) { throw new Error('MySQL 未配置,请设置 DATABASE_URL 或 MYSQL_* 环境变量'); } if (process.env.DATABASE_URL) { return mysql.createPool(process.env.DATABASE_URL); } return mysql.createPool({ host: process.env.MYSQL_HOST ?? 'localhost', port: Number(process.env.MYSQL_PORT ?? 3306), user: process.env.MYSQL_USER ?? 'boot', password: process.env.MYSQL_PASSWORD ?? '', database: process.env.MYSQL_DATABASE ?? 'tkmind', waitForConnections: true, connectionLimit: 10, }); } async function columnExists(pool, table, column) { const [rows] = await pool.query( `SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ? LIMIT 1`, [table, column], ); return rows.length > 0; } async function indexExists(pool, table, index) { const [rows] = await pool.query( `SELECT 1 FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND INDEX_NAME = ? LIMIT 1`, [table, index], ); return rows.length > 0; } async function foreignKeyDeleteRule(pool, table, constraint) { const [rows] = await pool.query( `SELECT DELETE_RULE FROM information_schema.REFERENTIAL_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND TABLE_NAME = ? AND CONSTRAINT_NAME = ? LIMIT 1`, [table, constraint], ); return rows[0]?.DELETE_RULE ?? null; } async function ensureForeignKeyDeleteRule( pool, { table, constraint, column, referencedTable, referencedColumn = 'id', deleteRule }, ) { const currentRule = await foreignKeyDeleteRule(pool, table, constraint); if (currentRule === deleteRule) return; if (currentRule) { await pool.query(`ALTER TABLE \`${table}\` DROP FOREIGN KEY \`${constraint}\``); } await pool.query( `ALTER TABLE \`${table}\` ADD CONSTRAINT \`${constraint}\` FOREIGN KEY (\`${column}\`) REFERENCES \`${referencedTable}\`(\`${referencedColumn}\`) ON DELETE ${deleteRule}`, ); } export async function migrateSchema(pool) { const renames = [ ['h5_user_sessions', 'goose_session_id', 'agent_session_id'], ['h5_session_billing_state', 'goose_session_id', 'agent_session_id'], ['h5_usage_records', 'goose_session_id', 'agent_session_id'], ]; for (const [table, oldCol, newCol] of renames) { if (await columnExists(pool, table, oldCol)) { await pool.query( `ALTER TABLE \`${table}\` CHANGE \`${oldCol}\` \`${newCol}\` VARCHAR(128) NOT NULL`, ); } } const userColumns = [ ['slug', 'VARCHAR(64) NULL AFTER username'], ['email', 'VARCHAR(255) NULL AFTER slug'], [ 'password_algorithm', "VARCHAR(32) NOT NULL DEFAULT 'pbkdf2-sha512' AFTER password_hash", ], ['plan_type', "VARCHAR(32) NOT NULL DEFAULT 'free' AFTER status"], ]; for (const [column, definition] of userColumns) { if (!(await columnExists(pool, 'h5_users', column))) { await pool.query(`ALTER TABLE h5_users ADD COLUMN \`${column}\` ${definition}`); } } await pool.query(`UPDATE h5_users SET slug = username WHERE slug IS NULL OR slug = ''`); if (!(await indexExists(pool, 'h5_users', 'uq_h5_users_slug'))) { await pool.query(`ALTER TABLE h5_users ADD UNIQUE KEY uq_h5_users_slug (slug)`); } if (!(await indexExists(pool, 'h5_users', 'uq_h5_users_email'))) { await pool.query(`ALTER TABLE h5_users ADD UNIQUE KEY uq_h5_users_email (email)`); } const assetForeignKeys = [ { table: 'h5_assets', constraint: 'fk_h5_asset_category', column: 'category_id', referencedTable: 'h5_space_categories', deleteRule: 'CASCADE', }, { table: 'h5_assets', constraint: 'fk_h5_asset_parent', column: 'parent_id', referencedTable: 'h5_assets', deleteRule: 'SET NULL', }, { table: 'h5_asset_versions', constraint: 'fk_h5_asset_version_user', column: 'created_by', referencedTable: 'h5_users', deleteRule: 'CASCADE', }, { table: 'h5_upload_sessions', constraint: 'fk_h5_upload_category', column: 'category_id', referencedTable: 'h5_space_categories', deleteRule: 'CASCADE', }, { table: 'h5_page_versions', constraint: 'fk_h5_page_version_content', column: 'content_asset_id', referencedTable: 'h5_assets', deleteRule: 'CASCADE', }, ]; for (const foreignKey of assetForeignKeys) { await ensureForeignKeyDeleteRule(pool, foreignKey); } await pool.query( `ALTER TABLE h5_assets MODIFY source_type ENUM('upload', 'chat', 'agent', 'template', 'generated', 'workspace') NOT NULL DEFAULT 'upload'`, ); const llmColumns = [ ['provider_kind', "ENUM('builtin','custom') NOT NULL DEFAULT 'builtin' AFTER provider_id"], ['api_url', 'VARCHAR(512) NULL AFTER provider_kind'], ['base_path', 'VARCHAR(256) NULL AFTER api_url'], ['models_json', 'TEXT NULL AFTER base_path'], ['goosed_provider_id', 'VARCHAR(64) NULL AFTER models_json'], ['engine', "VARCHAR(32) NOT NULL DEFAULT 'openai' AFTER goosed_provider_id"], ['relay_provider', 'VARCHAR(64) NULL AFTER engine'], ['is_vision_selected', 'TINYINT(1) NOT NULL DEFAULT 0 AFTER is_selected'], ]; for (const [column, definition] of llmColumns) { if (!(await columnExists(pool, 'h5_llm_provider_keys', column))) { await pool.query(`ALTER TABLE h5_llm_provider_keys ADD COLUMN \`${column}\` ${definition}`); } } await pool.query(` CREATE TABLE IF NOT EXISTS h5_llm_executor_bindings ( id CHAR(36) PRIMARY KEY, executor ENUM('goose', 'aider', 'openhands') NOT NULL, purpose VARCHAR(32) NOT NULL DEFAULT 'default', provider_key_id CHAR(36) NULL, model VARCHAR(128) NOT NULL, enabled TINYINT(1) NOT NULL DEFAULT 1, created_at BIGINT NOT NULL, updated_at BIGINT NOT NULL, UNIQUE KEY uq_h5_llm_executor_binding (executor, purpose), KEY idx_h5_llm_executor_provider (provider_key_id), CONSTRAINT fk_h5_llm_executor_provider FOREIGN KEY (provider_key_id) REFERENCES h5_llm_provider_keys(id) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); const publishColumns = [ ['plaza_view_count', 'BIGINT NOT NULL DEFAULT 0'], ['plaza_like_count', 'BIGINT NOT NULL DEFAULT 0'], ]; for (const [column, definition] of publishColumns) { if (!(await columnExists(pool, 'h5_publish_records', column))) { await pool.query(`ALTER TABLE h5_publish_records ADD COLUMN \`${column}\` ${definition}`); } } const plazaUserColumns = [ ['plaza_post_count', 'INT UNSIGNED NOT NULL DEFAULT 0'], ['plaza_follower_count', 'INT UNSIGNED NOT NULL DEFAULT 0'], ['plaza_following_count', 'INT UNSIGNED NOT NULL DEFAULT 0'], ['plaza_verified', 'TINYINT(1) NOT NULL DEFAULT 0'], ['plaza_post_banned', 'TINYINT(1) NOT NULL DEFAULT 0'], ['plaza_comment_banned', 'TINYINT(1) NOT NULL DEFAULT 0'], [ 'ops_role', "ENUM('none','reviewer','editor','ops_admin') NOT NULL DEFAULT 'none'", ], ]; for (const [column, definition] of plazaUserColumns) { if (!(await columnExists(pool, 'h5_users', column))) { await pool.query(`ALTER TABLE h5_users ADD COLUMN \`${column}\` ${definition}`); } } if (!(await columnExists(pool, 'h5_users', 'signup_source'))) { await pool.query( `ALTER TABLE h5_users ADD COLUMN signup_source VARCHAR(32) NULL DEFAULT 'password' AFTER workspace_root`, ); } if (!(await columnExists(pool, 'h5_user_sessions', 'goosed_node'))) { await pool.query( `ALTER TABLE h5_user_sessions ADD COLUMN goosed_node TINYINT UNSIGNED NOT NULL DEFAULT 0`, ); } const oauthStateColumns = [ ['intent', "VARCHAR(16) NOT NULL DEFAULT 'login' AFTER utm_campaign"], ['bind_user_id', 'CHAR(36) NULL AFTER intent'], ['auth_mode', "VARCHAR(8) NOT NULL DEFAULT 'mp' AFTER bind_user_id"], ['status', "VARCHAR(16) NOT NULL DEFAULT 'pending' AFTER auth_mode"], ['result_kind', 'VARCHAR(32) NULL AFTER status'], ['result_token', 'VARCHAR(512) NULL AFTER result_kind'], ['result_message', 'VARCHAR(255) NULL AFTER result_token'], ]; for (const [column, definition] of oauthStateColumns) { if (!(await columnExists(pool, 'h5_wechat_oauth_states', column))) { await pool.query( `ALTER TABLE h5_wechat_oauth_states ADD COLUMN \`${column}\` ${definition}`, ); } } await pool.query(` CREATE TABLE IF NOT EXISTS h5_wechat_pending_binds ( token VARCHAR(64) PRIMARY KEY, app_id VARCHAR(32) NOT NULL, openid VARCHAR(64) NOT NULL, unionid VARCHAR(64) NULL, nickname VARCHAR(128) NULL, avatar_url VARCHAR(512) NULL, return_to VARCHAR(512) NULL, utm_source VARCHAR(64) NULL, utm_medium VARCHAR(64) NULL, utm_campaign VARCHAR(64) NULL, expires_at BIGINT NOT NULL, created_at BIGINT NOT NULL, KEY idx_wechat_pending_expires (expires_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); await pool.query(` CREATE TABLE IF NOT EXISTS h5_wechat_mp_messages ( app_id VARCHAR(32) NOT NULL, openid VARCHAR(64) NOT NULL, msg_id VARCHAR(128) NOT NULL, status ENUM('processing', 'done', 'failed') NOT NULL DEFAULT 'processing', agent_session_id VARCHAR(128) NULL, created_at BIGINT NOT NULL, updated_at BIGINT NOT NULL, PRIMARY KEY (app_id, openid, msg_id), KEY idx_wechat_mp_messages_updated (updated_at), KEY idx_wechat_mp_messages_session (agent_session_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); await pool.query(` CREATE TABLE IF NOT EXISTS h5_wechat_mp_message_details ( id CHAR(36) PRIMARY KEY, app_id VARCHAR(32) NOT NULL, openid VARCHAR(64) NOT NULL, user_id CHAR(36) NULL, msg_id VARCHAR(128) NULL, msg_type VARCHAR(32) NOT NULL, display_text TEXT NULL, agent_text TEXT NULL, media_id VARCHAR(256) NULL, media_url TEXT NULL, media_public_url TEXT NULL, media_format VARCHAR(64) NULL, location_lat DECIMAL(10,7) NULL, location_lng DECIMAL(10,7) NULL, location_label VARCHAR(255) NULL, link_url TEXT NULL, link_title VARCHAR(255) NULL, raw_xml_hash CHAR(40) NULL, raw_json JSON NULL, created_at BIGINT NOT NULL, KEY idx_wechat_mp_message_details_user (app_id, openid, created_at), KEY idx_wechat_mp_message_details_msg (msg_id), KEY idx_wechat_mp_message_details_type (msg_type, created_at), CONSTRAINT fk_wechat_mp_message_details_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); await pool.query(` CREATE TABLE IF NOT EXISTS h5_schedule_items ( id CHAR(36) PRIMARY KEY, user_id CHAR(36) NOT NULL, kind ENUM('task', 'event') NOT NULL, title VARCHAR(255) NOT NULL, description TEXT NULL, status ENUM('active', 'completed', 'cancelled', 'deleted') NOT NULL DEFAULT 'active', start_at BIGINT NULL, end_at BIGINT NULL, due_at BIGINT NULL, all_day TINYINT(1) NOT NULL DEFAULT 0, timezone VARCHAR(64) NOT NULL DEFAULT 'Asia/Shanghai', location VARCHAR(255) NULL, source_channel ENUM('h5', 'wechat', 'agent', 'api') NOT NULL DEFAULT 'agent', source_session_id VARCHAR(128) NULL, source_message_id VARCHAR(128) NULL, source_text TEXT NULL, metadata_json JSON NULL, created_at BIGINT NOT NULL, updated_at BIGINT NOT NULL, deleted_at BIGINT NULL, KEY idx_schedule_user_time (user_id, status, start_at, due_at), KEY idx_schedule_user_updated (user_id, updated_at), CONSTRAINT fk_schedule_item_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); await pool.query(` CREATE TABLE IF NOT EXISTS h5_schedule_reminders ( id CHAR(36) PRIMARY KEY, user_id CHAR(36) NOT NULL, item_id CHAR(36) NOT NULL, remind_at BIGINT NOT NULL, offset_minutes INT NULL, channel ENUM('wechat', 'in_app') NOT NULL DEFAULT 'wechat', status ENUM('pending', 'locked', 'sent', 'failed', 'cancelled') NOT NULL DEFAULT 'pending', attempts INT NOT NULL DEFAULT 0, last_error VARCHAR(500) NULL, locked_until BIGINT NULL, sent_at BIGINT NULL, created_at BIGINT NOT NULL, updated_at BIGINT NOT NULL, UNIQUE KEY uq_schedule_item_remind_at (item_id, remind_at, channel), KEY idx_reminder_due (status, remind_at), KEY idx_reminder_user (user_id, status, remind_at), CONSTRAINT fk_schedule_reminder_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE, CONSTRAINT fk_schedule_reminder_item FOREIGN KEY (item_id) REFERENCES h5_schedule_items(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); await pool.query(` CREATE TABLE IF NOT EXISTS h5_schedule_digest_subscriptions ( id CHAR(36) PRIMARY KEY, user_id CHAR(36) NOT NULL, digest_type ENUM('todo_day') NOT NULL DEFAULT 'todo_day', hour TINYINT UNSIGNED NOT NULL, minute TINYINT UNSIGNED NOT NULL DEFAULT 0, timezone VARCHAR(64) NOT NULL DEFAULT 'Asia/Shanghai', channel ENUM('wechat', 'in_app') NOT NULL DEFAULT 'wechat', status ENUM('active', 'locked', 'failed', 'cancelled') NOT NULL DEFAULT 'active', next_run_at BIGINT NOT NULL, last_run_at BIGINT NULL, attempts INT NOT NULL DEFAULT 0, locked_until BIGINT NULL, last_error VARCHAR(500) NULL, source_channel ENUM('h5', 'wechat', 'agent', 'api') NOT NULL DEFAULT 'agent', source_session_id VARCHAR(128) NULL, source_message_id VARCHAR(128) NULL, source_text TEXT NULL, created_at BIGINT NOT NULL, updated_at BIGINT NOT NULL, UNIQUE KEY uq_schedule_digest_user_type_channel (user_id, digest_type, channel), KEY idx_schedule_digest_due (status, next_run_at), CONSTRAINT fk_schedule_digest_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); await pool.query(` CREATE TABLE IF NOT EXISTS h5_balance_alert_subscriptions ( id CHAR(36) PRIMARY KEY, user_id CHAR(36) NOT NULL, threshold_cents BIGINT NOT NULL, channel ENUM('wechat', 'in_app') NOT NULL DEFAULT 'wechat', status ENUM('active', 'locked', 'failed', 'cancelled') NOT NULL DEFAULT 'active', next_run_at BIGINT NOT NULL, last_run_at BIGINT NULL, last_notified_balance_cents BIGINT NULL, attempts INT NOT NULL DEFAULT 0, locked_until BIGINT NULL, last_error VARCHAR(500) NULL, source_channel ENUM('h5', 'wechat', 'agent', 'api') NOT NULL DEFAULT 'agent', source_session_id VARCHAR(128) NULL, source_message_id VARCHAR(128) NULL, source_text TEXT NULL, created_at BIGINT NOT NULL, updated_at BIGINT NOT NULL, UNIQUE KEY uq_balance_alert_user_channel (user_id, channel), KEY idx_balance_alert_due (status, next_run_at), KEY idx_balance_alert_user (user_id, status, next_run_at), CONSTRAINT fk_balance_alert_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); await pool.query(` CREATE TABLE IF NOT EXISTS h5_schedule_delivery_logs ( id CHAR(36) PRIMARY KEY, reminder_id CHAR(36) NULL, subscription_id CHAR(36) NULL, user_id CHAR(36) NOT NULL, channel ENUM('wechat', 'in_app') NOT NULL, status ENUM('success', 'failed') NOT NULL, provider_message_id VARCHAR(128) NULL, error_code VARCHAR(64) NULL, error_message VARCHAR(500) NULL, created_at BIGINT NOT NULL, KEY idx_delivery_reminder (reminder_id, created_at), KEY idx_delivery_subscription (subscription_id, created_at), KEY idx_delivery_user (user_id, created_at), CONSTRAINT fk_schedule_delivery_reminder FOREIGN KEY (reminder_id) REFERENCES h5_schedule_reminders(id) ON DELETE CASCADE, CONSTRAINT fk_schedule_delivery_subscription FOREIGN KEY (subscription_id) REFERENCES h5_schedule_digest_subscriptions(id) ON DELETE CASCADE, CONSTRAINT fk_schedule_delivery_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); await pool.query(` CREATE TABLE IF NOT EXISTS h5_user_notifications ( id CHAR(36) PRIMARY KEY, user_id CHAR(36) NOT NULL, channel ENUM('web', 'wechat') NOT NULL DEFAULT 'web', notification_type VARCHAR(64) NOT NULL, title VARCHAR(255) NOT NULL, body TEXT NOT NULL, data_json JSON NULL, status ENUM('unread', 'read') NOT NULL DEFAULT 'unread', read_at BIGINT NULL, created_at BIGINT NOT NULL, updated_at BIGINT NOT NULL, KEY idx_user_notifications_user_status_created (user_id, status, created_at), KEY idx_user_notifications_user_created (user_id, created_at), CONSTRAINT fk_user_notifications_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); await pool.query( `ALTER TABLE h5_payment_orders MODIFY pay_mode ENUM('native', 'h5', 'jsapi') NOT NULL DEFAULT 'native'`, ); // Session snapshot cache table (additive, no existing-data impact). // Rollback: DROP TABLE h5_session_snapshots — safe, no dependants. await pool.query(` CREATE TABLE IF NOT EXISTS h5_session_snapshots ( agent_session_id VARCHAR(128) NOT NULL PRIMARY KEY, user_id CHAR(36) NOT NULL, name VARCHAR(512) NOT NULL DEFAULT '', working_dir VARCHAR(1024) NOT NULL DEFAULT '', created_at_str VARCHAR(64) NOT NULL DEFAULT '', updated_at_str VARCHAR(64) NOT NULL DEFAULT '', user_set_name TINYINT(1) NOT NULL DEFAULT 0, recipe_json TEXT NULL, synced_msg_count INT NOT NULL DEFAULT 0, source_updated_at VARCHAR(64) NOT NULL DEFAULT '', messages_json LONGTEXT NOT NULL, synced_at BIGINT NOT NULL, KEY idx_h5_snapshots_user (user_id), KEY idx_h5_snapshots_synced (synced_at), CONSTRAINT fk_h5_snapshot_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); // Subscription plan table for tiered monthly billing. // Rollback: DROP TABLE h5_subscriptions — safe, no dependants. await pool.query(` CREATE TABLE IF NOT EXISTS h5_subscriptions ( id CHAR(36) PRIMARY KEY, user_id CHAR(36) NOT NULL, plan_type VARCHAR(32) NOT NULL DEFAULT 'free', status ENUM('active', 'expired', 'cancelled') NOT NULL DEFAULT 'active', period_tokens_limit BIGINT NOT NULL DEFAULT 0, period_tokens_used BIGINT NOT NULL DEFAULT 0, period_start BIGINT NOT NULL, period_end BIGINT NOT NULL, expires_at BIGINT NOT NULL, overage_rate DECIMAL(4,2) NOT NULL DEFAULT 1.00, operator_id CHAR(36) NULL, note VARCHAR(512) NULL, created_at BIGINT NOT NULL, updated_at BIGINT NOT NULL, KEY idx_h5_sub_user_status (user_id, status), KEY idx_h5_sub_expires (expires_at, status), CONSTRAINT fk_h5_sub_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); // Back-fill free subscriptions for existing users who pre-date the subscription system. // Idempotent: only inserts where no active subscription exists. await pool.query(` INSERT INTO h5_subscriptions (id, user_id, plan_type, status, period_tokens_limit, period_tokens_used, period_start, period_end, expires_at, overage_rate, operator_id, note, created_at, updated_at) SELECT UUID(), u.id, 'free', 'active', 150000, 0, UNIX_TIMESTAMP() * 1000, (UNIX_TIMESTAMP() + 30 * 86400) * 1000, (UNIX_TIMESTAMP() + 30 * 86400) * 1000, 1.00, NULL, '系统迁移补建免费套餐', UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000 FROM h5_users u WHERE NOT EXISTS ( SELECT 1 FROM h5_subscriptions s WHERE s.user_id = u.id AND s.status = 'active' ) `); } export async function initSchema(pool) { const schemaPath = path.join(__dirname, 'schema.sql'); const sql = fs.readFileSync(schemaPath, 'utf8'); for (const statement of sql.split(';')) { const trimmed = statement.trim(); if (trimmed) { await pool.query(trimmed); } } await migrateSchema(pool); await ensureDefaultSpaces(pool, { quotaBytes: Number(process.env.MINDSPACE_FREE_QUOTA_BYTES ?? 5 * 1024 * 1024), }); }