import { env } from "cloudflare:workers"; import migrationSql from "../drizzle/0000_condemned_legion.sql?raw"; import claimMigrationSql from "../drizzle/0001_mushy_vance_astro.sql?raw"; import claimOwnershipMigrationSql from "../drizzle/0002_wild_ravenous.sql?raw"; import provisioningMigrationSql from "../drizzle/0003_medical_rocket_racer.sql?raw"; import provisioningFenceMigrationSql from "../drizzle/0004_loud_prodigy.sql?raw"; import feedbackMigrationSql from "../drizzle/0005_pale_corsair.sql?raw"; import serviceIncidentMigrationSql from "../drizzle/0006_clever_shocker.sql?raw"; import accountLifecycleMigrationSql from "../drizzle/0007_zippy_nomad.sql?raw"; import provisionerLivenessMigrationSql from "../drizzle/0008_far_justice.sql?raw"; import maintenanceJobMigrationSql from "../drizzle/0009_flat_robbie_robertson.sql?raw"; import betaAccessRequestMigrationSql from "../drizzle/0010_windy_toxin.sql?raw"; import betaAccessRequestTimeIndexMigrationSql from "../drizzle/0011_next_thunderball.sql?raw"; import sharedRelayControlPlaneMigrationSql from "../drizzle/0012_shared_relay_control_plane.sql?raw"; import relayMigrationFencingMigrationSql from "../drizzle/0013_relay_migration_fencing.sql?raw"; import relayTenantPurgeMigrationSql from "../drizzle/0014_relay_tenant_purge.sql?raw"; import phoneHandoffIdempotencyMigrationSql from "../drizzle/0015_phone_handoff_idempotency.sql?raw"; import phoneHandoffActivationMigrationSql from "../drizzle/0016_phone_handoff_activation.sql?raw"; import provisioningInvariantMigrationSql from "../drizzle/9000_provisioning_invariants.sql?raw"; import provisioningSlugMigrationSql from "../drizzle/9001_provisioning_slug_backfill.sql?raw"; import readyCredentialReconciliationMigrationSql from "../drizzle/9002_ready_credential_reconciliation.sql?raw"; import { LAUNCH_GATE_SEEDS } from "./launch-gates.ts"; let schemaPromise: Promise | null = null; function getDatabase(): D1Database { if (!env.DB) { throw new Error("Cloudflare D1 binding `DB` is unavailable."); } return env.DB; } function migrationStatements(sql: string): string[] { return sql .split("--> statement-breakpoint") .map((statement) => statement.trim()) .filter(Boolean); } async function applyTrackedMigration( db: D1Database, id: string, sql: string, ): Promise { const applied = await db .prepare("SELECT id FROM cloud_schema_migrations WHERE id = ?") .bind(id) .first<{ id: string }>(); if (applied) return; const now = new Date().toISOString(); try { await db.batch([ ...migrationStatements(sql).map((statement) => db.prepare(statement)), db .prepare( `INSERT INTO cloud_schema_migrations (id, applied_at) VALUES (?, ?)`, ) .bind(id, now), ]); } catch (error) { const racedMigration = await db .prepare("SELECT id FROM cloud_schema_migrations WHERE id = ?") .bind(id) .first<{ id: string }>(); if (!racedMigration) throw error; } } async function migrateSchema(db: D1Database): Promise { let accountsTable = await db .prepare( "SELECT name FROM sqlite_schema WHERE type = 'table' AND name = 'accounts'", ) .first<{ name: string }>(); if (!accountsTable) { try { await db.batch( migrationStatements(migrationSql).map((statement) => db.prepare(statement)), ); } catch (error) { accountsTable = await db .prepare( "SELECT name FROM sqlite_schema WHERE type = 'table' AND name = 'accounts'", ) .first<{ name: string }>(); if (!accountsTable) throw error; } } const ledgerTable = await db .prepare( "SELECT name FROM sqlite_schema WHERE type = 'table' AND name = 'cloud_schema_migrations'", ) .first<{ name: string }>(); const claimMigration = ledgerTable ? await db .prepare("SELECT id FROM cloud_schema_migrations WHERE id = ?") .bind("0001_mushy_vance_astro") .first<{ id: string }>() : null; if (!claimMigration) { const statements = migrationStatements(claimMigrationSql); const applicableStatements = ledgerTable ? statements.slice(1) : statements; const now = new Date().toISOString(); try { await db.batch([ ...applicableStatements.map((statement) => db.prepare(statement)), db .prepare( `INSERT OR IGNORE INTO cloud_schema_migrations (id, applied_at) VALUES ('0000_condemned_legion', ?), ('0001_mushy_vance_astro', ?)`, ) .bind(now, now), ]); } catch (error) { const racedMigration = await db .prepare("SELECT id FROM cloud_schema_migrations WHERE id = ?") .bind("0001_mushy_vance_astro") .first<{ id: string }>(); if (!racedMigration) throw error; } } await applyTrackedMigration( db, "0002_wild_ravenous", claimOwnershipMigrationSql, ); await applyTrackedMigration( db, "0003_medical_rocket_racer", provisioningMigrationSql, ); await applyTrackedMigration( db, "0004_loud_prodigy", provisioningFenceMigrationSql, ); await applyTrackedMigration(db, "0005_pale_corsair", feedbackMigrationSql); await applyTrackedMigration( db, "0006_clever_shocker", serviceIncidentMigrationSql, ); await applyTrackedMigration( db, "0007_zippy_nomad", accountLifecycleMigrationSql, ); await applyTrackedMigration( db, "0008_far_justice", provisionerLivenessMigrationSql, ); await applyTrackedMigration( db, "0009_flat_robbie_robertson", maintenanceJobMigrationSql, ); await applyTrackedMigration( db, "0010_windy_toxin", betaAccessRequestMigrationSql, ); await applyTrackedMigration( db, "0011_next_thunderball", betaAccessRequestTimeIndexMigrationSql, ); await applyTrackedMigration( db, "9000_provisioning_invariants", provisioningInvariantMigrationSql, ); await applyTrackedMigration( db, "9001_provisioning_slug_backfill", provisioningSlugMigrationSql, ); await applyTrackedMigration( db, "9002_ready_credential_reconciliation", readyCredentialReconciliationMigrationSql, ); await applyTrackedMigration( db, "0012_shared_relay_control_plane", sharedRelayControlPlaneMigrationSql, ); await applyTrackedMigration( db, "0013_relay_migration_fencing", relayMigrationFencingMigrationSql, ); await applyTrackedMigration( db, "0014_relay_tenant_purge", relayTenantPurgeMigrationSql, ); await applyTrackedMigration( db, "0015_phone_handoff_idempotency", phoneHandoffIdempotencyMigrationSql, ); await applyTrackedMigration( db, "0016_phone_handoff_activation", phoneHandoffActivationMigrationSql, ); await db.prepare("PRAGMA optimize").run(); } async function seedCatalogAndGates(db: D1Database): Promise { const now = new Date().toISOString(); const gateStatements = LAUNCH_GATE_SEEDS.map(([key, priority, category, title]) => db .prepare( `INSERT OR IGNORE INTO launch_gates (key, priority, category, title, status, notes, updated_at) VALUES (?, ?, ?, ?, 'blocked', '', ?)`, ) .bind(key, priority, category, title, now), ); const gateReconciliationStatements = LAUNCH_GATE_SEEDS.map( ([key, priority, category, title]) => db .prepare( `UPDATE launch_gates SET priority = ?, category = ?, title = ?, updated_at = ? WHERE key = ? AND (priority <> ? OR category <> ? OR title <> ?)`, ) .bind(priority, category, title, now, key, priority, category, title), ); const gateReconciliationAuditStatements = LAUNCH_GATE_SEEDS.map( ([key, priority, category, title]) => db .prepare( `INSERT OR IGNORE INTO audit_events (id, actor_id, action, target_type, target_id, reason, before_json, after_json, correlation_id, created_at) SELECT 'audit_reclassify_' || key || '_free_beta_v1', 'system:bootstrap', 'launch_gate.reclassified', 'launch_gate', key, '免费公测门禁与未来收费门禁分离', json_object('priority', priority, 'category', category, 'title', title), json_object('priority', ?, 'category', ?, 'title', ?), 'corr_reclassify_' || key || '_free_beta_v1', ? FROM launch_gates WHERE key = ? AND (priority <> ? OR category <> ? OR title <> ?)`, ) .bind(priority, category, title, now, key, priority, category, title), ); await db.batch([ db .prepare( `INSERT OR IGNORE INTO price_versions (id, product_code, billing_period, unit_slots, amount_minor, currency, tax_mode, quote_ttl_seconds, status, effective_from, created_by) VALUES (?, 'host_slot', 'month', 1, 1000, 'CNY', 'undecided', 900, 'retired', ?, 'system:seed')`, ) .bind("price_host_month_v1", now), db .prepare( `INSERT OR IGNORE INTO price_versions (id, product_code, billing_period, unit_slots, amount_minor, currency, tax_mode, quote_ttl_seconds, status, effective_from, created_by) VALUES (?, 'host_slot', 'year', 1, 10000, 'CNY', 'undecided', 900, 'retired', ?, 'system:seed')`, ) .bind("price_host_year_v1", now), db .prepare( `INSERT OR IGNORE INTO beta_programs (id, state, capacity_slots, starts_at, ends_at, grace_days, created_by) VALUES (?, 'active', NULL, ?, NULL, 0, 'system:seed')`, ) .bind("beta_public_v1", now), ...gateStatements, ...gateReconciliationAuditStatements, ...gateReconciliationStatements, db .prepare( `INSERT OR IGNORE INTO audit_events (id, actor_id, action, target_type, target_id, reason, before_json, after_json, correlation_id, created_at) SELECT 'audit_defer_paid_catalog_v1', 'system:bootstrap', 'price_catalog.retired', 'price_catalog', 'host_slot', '免费公测阶段暂缓收费决策,保留历史价格但撤销发布状态', '{"status":"published"}', '{"status":"retired"}', 'corr_defer_paid_catalog_v1', ? WHERE EXISTS ( SELECT 1 FROM price_versions WHERE product_code = 'host_slot' AND status = 'published' )`, ) .bind(now), db.prepare( `UPDATE price_versions SET status = 'retired' WHERE product_code = 'host_slot' AND status = 'published'`, ), db .prepare( `INSERT OR IGNORE INTO audit_events (id, actor_id, action, target_type, target_id, reason, before_json, after_json, correlation_id, created_at) SELECT 'audit_repair_p0_' || key, 'system:bootstrap', 'launch_gate.repaired', 'launch_gate', key, '旧版 P0 状态缺少有效证据或使用了不适用,启动时恢复为阻止', '{"status":"legacy_invalid"}', '{"status":"blocked"}', 'corr_repair_p0_' || key, ? FROM launch_gates WHERE priority = 'P0' AND ( status = 'not_applicable' OR (status = 'passed' AND ( trim(COALESCE(owner, '')) = '' OR trim(COALESCE(notes, '')) = '' OR evidence_url IS NULL OR evidence_url NOT LIKE 'https://%' )) )`, ) .bind(now), db .prepare( `UPDATE launch_gates SET status = 'blocked', reviewed_at = ?, updated_at = ? WHERE priority = 'P0' AND ( status = 'not_applicable' OR (status = 'passed' AND ( trim(COALESCE(owner, '')) = '' OR trim(COALESCE(notes, '')) = '' OR evidence_url IS NULL OR evidence_url NOT LIKE 'https://%' )) )`, ) .bind(now, now), ]); } export async function ensureDatabase(): Promise { if (!schemaPromise) { schemaPromise = (async () => { const db = getDatabase(); await migrateSchema(db); await seedCatalogAndGates(db); })().catch((error) => { schemaPromise = null; throw error; }); } await schemaPromise; } export function getD1(): D1Database { return getDatabase(); }