Files
MoonTVPlus/scripts/init-turso.js
T
mtvpls-turso 8e464c463b feat: add Turso (libSQL) storage support for EdgeOne deployment
- Add TursoAdapter implementing DatabaseAdapter interface using @libsql/client
- Add 'turso' storage type to db.ts storage selector (reuses D1Storage)
- Add init-turso.js script for database migration and admin initialization
- Add @libsql/client dependency to package.json
- Update next.config.js to exclude @libsql/client from client-side bundle
- Update edgeone-deploy.yml workflow with TURSO_URL and TURSO_TOKEN secrets
- Update edgeone-build.mjs runtime env keys to include Turso variables
- Update .env.edgeone.example with Turso deployment instructions
- Update README.md with Turso as recommended storage for EdgeOne

Turso provides free SQLite-compatible cloud database (libSQL) with:
- 500 databases, 9GB storage, 1B row reads/month on free tier
- HTTP API compatible with edge/serverless environments
- Full SQLite syntax compatibility, reuses existing migrations
2026-07-11 17:04:27 +00:00

206 lines
6.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Turso (libSQL) 数据库初始化脚本
*
* 创建数据库表结构并初始化默认管理员用户
* 复用 SQLite 迁移文件(Turso 基于 libSQL,完全兼容 SQLite 语法)
*
* 使用方式:
* 1. 安装依赖:pnpm install
* 2. 设置环境变量:
* export TURSO_URL=libsql://your-database.turso.io
* export TURSO_TOKEN=your-auth-token
* export USERNAME=admin
* export PASSWORD=your-password
* 3. 运行:node scripts/init-turso.js
*/
const { createClient } = require('@libsql/client');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const MIGRATIONS_DIR = path.join(__dirname, '../migrations');
const MIGRATION_BASELINE_CUTOFF = '008_web_push_notifications.sql';
function hashPassword(password) {
return crypto.createHash('sha256').update(password).digest('hex');
}
function splitSqlStatements(sql) {
const withoutLineComments = sql
.split('\n')
.filter((line) => !line.trim().startsWith('--'))
.join('\n');
return withoutLineComments
.split(';')
.map((statement) => statement.trim())
.filter((statement) => statement.length > 0);
}
function getMigrationFiles() {
if (!fs.existsSync(MIGRATIONS_DIR)) {
throw new Error(`Migrations directory not found: ${MIGRATIONS_DIR}`);
}
return fs
.readdirSync(MIGRATIONS_DIR)
.filter((file) => file.endsWith('.sql'))
.sort((a, b) => a.localeCompare(b));
}
function isIgnorableMigrationError(error) {
const message =
error instanceof Error ? error.message : String(error || '');
return (
(message.includes('table') && message.includes('already exists')) ||
(message.includes('index') && message.includes('already exists')) ||
message.includes('duplicate column name')
);
}
async function tableExists(client, tableName) {
const result = await client.execute({
sql: "SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?",
args: [tableName],
});
return result.rows.length > 0;
}
async function ensureMigrationTable(client) {
await client.execute(`
CREATE TABLE IF NOT EXISTS schema_migrations (
filename TEXT PRIMARY KEY,
applied_at INTEGER NOT NULL
)
`);
}
async function getAppliedMigrations(client) {
const result = await client.execute('SELECT filename FROM schema_migrations');
return new Set(result.rows.map((row) => row.filename));
}
async function markMigrationApplied(client, filename) {
await client.execute({
sql: 'INSERT OR IGNORE INTO schema_migrations (filename, applied_at) VALUES (?, ?)',
args: [filename, Date.now()],
});
}
async function seedExistingMigrationBaseline(
client,
migrationFiles,
hadExistingSchema
) {
const applied = await getAppliedMigrations(client);
if (!hadExistingSchema || applied.size > 0) return;
for (const file of migrationFiles) {
if (file.localeCompare(MIGRATION_BASELINE_CUTOFF) < 0) {
await markMigrationApplied(client, file);
}
}
}
async function runMigrations(client) {
const migrationFiles = getMigrationFiles();
const hadExistingSchema = await tableExists(client, 'users');
await ensureMigrationTable(client);
await seedExistingMigrationBaseline(client, migrationFiles, hadExistingSchema);
for (const file of migrationFiles) {
const applied = await getAppliedMigrations(client);
if (applied.has(file)) {
console.log(`⏭️ Migration already applied: ${file}`);
continue;
}
const migrationPath = path.join(MIGRATIONS_DIR, file);
const sql = fs.readFileSync(migrationPath, 'utf8');
const statements = splitSqlStatements(sql);
console.log(`▶️ Applying migration: ${file}`);
for (const statement of statements) {
try {
await client.execute(statement);
} catch (error) {
if (isIgnorableMigrationError(error)) {
console.log(`⏭️ Statement skipped in ${file}: ${error.message}`);
continue;
}
throw error;
}
}
await markMigrationApplied(client, file);
console.log(`✅ Migration applied: ${file}`);
}
}
async function ensureDefaultAdmin(client) {
const username = process.env.USERNAME || 'admin';
const password = process.env.PASSWORD || '123456789';
const passwordHash = hashPassword(password);
const existingUser = await client.execute({
sql: 'SELECT username FROM users WHERE username = ? LIMIT 1',
args: [username],
});
if (existingUser.rows.length > 0) {
console.log(`️ Admin user already exists: ${username}`);
return;
}
await client.execute({
sql: `INSERT INTO users (
username, password_hash, role, created_at,
playrecord_migrated, favorite_migrated, skip_migrated
) VALUES (?, ?, 'owner', ?, 1, 1, 1)`,
args: [username, passwordHash, Date.now()],
});
console.log(`✅ Default admin user created: ${username}`);
}
async function initTursoDatabase() {
const tursoUrl = process.env.TURSO_URL;
const tursoToken = process.env.TURSO_TOKEN;
if (!tursoUrl || !tursoToken) {
console.error(
'❌ TURSO_URL and TURSO_TOKEN environment variables must be set'
);
console.error('');
console.error('Example:');
console.error(' export TURSO_URL=libsql://your-database.turso.io');
console.error(' export TURSO_TOKEN=your-auth-token');
process.exit(1);
}
const client = createClient({
url: tursoUrl,
authToken: tursoToken,
});
console.log('📦 Initializing Turso (libSQL) database...');
console.log('🔗 Database URL:', tursoUrl);
try {
await runMigrations(client);
await ensureDefaultAdmin(client);
console.log('');
console.log('🎉 Turso database is ready!');
console.log('');
console.log('Next steps:');
console.log('1. Set NEXT_PUBLIC_STORAGE_TYPE=turso in your environment');
console.log('2. Set TURSO_URL and TURSO_TOKEN environment variables');
console.log('3. Deploy your application');
} catch (err) {
console.error('❌ Turso initialization failed:', err);
process.exit(1);
}
}
initTursoDatabase();