From ac4bd9839a5959a6773d16a0938af0834d8d90a8 Mon Sep 17 00:00:00 2001 From: rome-xi Date: Tue, 1 Sep 2026 12:43:05 +0000 Subject: [PATCH] fix(setup): sync fresh D1 bootstrap schema with migrations 0013/0021/0022 Signed-off-by: rome-xi --- src/lib/setup/migration.ts | 10 +++- tests/setup-bootstrap-schema.test.mjs | 68 +++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 tests/setup-bootstrap-schema.test.mjs diff --git a/src/lib/setup/migration.ts b/src/lib/setup/migration.ts index b5819e4..c2ffc75 100644 --- a/src/lib/setup/migration.ts +++ b/src/lib/setup/migration.ts @@ -12,6 +12,7 @@ const MIGRATION_NAMES = [ "0010_hard_squirrel_girl.sql", "0011_add_folder_colors.sql", "0012_add_app_settings.sql", + "0013_add_license_settings.sql", "0014_add_account_permissions.sql", "0015_add_forwarding_email.sql", "0016_add_message_snooze.sql", @@ -19,6 +20,8 @@ const MIGRATION_NAMES = [ "0018_add_mailbox_domain_aliases.sql", "0019_merge_message_bodies.sql", "0020_add_calendar_templates_schedule.sql", + "0021_add_mailbox_signature.sql", + "0022_add_mailbox_auto_reply.sql", ]; const INITIAL_SCHEMA_SQL = ` @@ -27,8 +30,11 @@ CREATE INDEX IF NOT EXISTS users_created_by_idx ON users(created_by_user_id); CREATE TABLE IF NOT EXISTS domains (id text PRIMARY KEY NOT NULL, user_id text NOT NULL REFERENCES users(id) ON DELETE cascade, hostname text NOT NULL, zone_id text NOT NULL, status text DEFAULT 'pending' NOT NULL, routing_status text, sending_subdomain_tag text, sending_enabled integer DEFAULT false NOT NULL, routing_enabled integer DEFAULT false NOT NULL, created_at integer NOT NULL); CREATE UNIQUE INDEX IF NOT EXISTS domains_hostname_idx ON domains(hostname); CREATE INDEX IF NOT EXISTS domains_user_idx ON domains(user_id); -CREATE TABLE IF NOT EXISTS mailboxes (id text PRIMARY KEY NOT NULL, user_id text NOT NULL REFERENCES users(id) ON DELETE cascade, domain_id text NOT NULL REFERENCES domains(id) ON DELETE cascade, local_part text NOT NULL, display_name text, avatar_key text, type text DEFAULT 'personal' NOT NULL, use_all_domains integer DEFAULT true NOT NULL, disabled integer DEFAULT false NOT NULL, created_at integer NOT NULL); +CREATE TABLE IF NOT EXISTS mailboxes (id text PRIMARY KEY NOT NULL, user_id text NOT NULL REFERENCES users(id) ON DELETE cascade, domain_id text NOT NULL REFERENCES domains(id) ON DELETE cascade, local_part text NOT NULL, display_name text, signature text, auto_reply_enabled integer DEFAULT false NOT NULL, auto_reply_subject text DEFAULT 'Out of office' NOT NULL, auto_reply_body text DEFAULT '' NOT NULL, avatar_key text, type text DEFAULT 'personal' NOT NULL, use_all_domains integer DEFAULT true NOT NULL, disabled integer DEFAULT false NOT NULL, created_at integer NOT NULL); CREATE UNIQUE INDEX IF NOT EXISTS mailboxes_address_idx ON mailboxes(domain_id, local_part); +CREATE TABLE IF NOT EXISTS auto_reply_deliveries (id text PRIMARY KEY NOT NULL, mailbox_id text NOT NULL REFERENCES mailboxes(id) ON DELETE cascade, recipient text NOT NULL, sent_at integer NOT NULL); +CREATE UNIQUE INDEX IF NOT EXISTS auto_reply_deliveries_mailbox_recipient_idx ON auto_reply_deliveries(mailbox_id, recipient); +CREATE INDEX IF NOT EXISTS auto_reply_deliveries_sent_idx ON auto_reply_deliveries(sent_at); CREATE TABLE IF NOT EXISTS mailbox_access (id text PRIMARY KEY NOT NULL, mailbox_id text NOT NULL REFERENCES mailboxes(id) ON DELETE cascade, user_id text NOT NULL REFERENCES users(id) ON DELETE cascade, permission text DEFAULT 'read_only' NOT NULL, created_by_user_id text REFERENCES users(id) ON DELETE set null, created_at integer NOT NULL); CREATE UNIQUE INDEX IF NOT EXISTS mailbox_access_mailbox_user_idx ON mailbox_access(mailbox_id, user_id); CREATE INDEX IF NOT EXISTS mailbox_access_user_idx ON mailbox_access(user_id); @@ -67,6 +73,8 @@ CREATE INDEX IF NOT EXISTS backups_created_idx ON backups(created_at); CREATE INDEX IF NOT EXISTS backups_status_idx ON backups(status); CREATE TABLE IF NOT EXISTS app_settings (id text PRIMARY KEY NOT NULL, app_name text DEFAULT 'Mailflare' NOT NULL, icon_key text, updated_at integer NOT NULL); INSERT OR IGNORE INTO app_settings (id, app_name, updated_at) VALUES ('default', 'Mailflare', unixepoch()); +CREATE TABLE IF NOT EXISTS license_settings (id text PRIMARY KEY NOT NULL, instance_id text NOT NULL, instance_url text, license_key_hash text, plan text DEFAULT 'community' NOT NULL, state text DEFAULT 'inactive' NOT NULL, features text DEFAULT '[]' NOT NULL, activated_at integer, validated_at integer, updated_at integer NOT NULL); +CREATE UNIQUE INDEX IF NOT EXISTS license_settings_instance_id_unique ON license_settings(instance_id); CREATE TABLE IF NOT EXISTS d1_migrations (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE, applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL); `; diff --git a/tests/setup-bootstrap-schema.test.mjs b/tests/setup-bootstrap-schema.test.mjs new file mode 100644 index 0000000..109d10c --- /dev/null +++ b/tests/setup-bootstrap-schema.test.mjs @@ -0,0 +1,68 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import test from "node:test"; + +const root = join(dirname(fileURLToPath(import.meta.url)), ".."); +const src = readFileSync(join(root, "src/lib/setup/migration.ts"), "utf8"); + +function migrationNames() { + const match = src.match(/const MIGRATION_NAMES = \[([\s\S]*?)\];/); + assert.ok(match, "MIGRATION_NAMES array not found"); + return [...match[1].matchAll(/"([^"]+)"/g)].map((item) => item[1]); +} + +function initialSchemaSql() { + const match = src.match(/const INITIAL_SCHEMA_SQL = `([\s\S]*?)`;/); + assert.ok(match, "INITIAL_SCHEMA_SQL not found"); + return match[1]; +} + +test("bootstrap records migrations 0013, 0021, and 0022 so later deploys do not re-apply them", () => { + const names = migrationNames(); + for (const name of [ + "0013_add_license_settings.sql", + "0021_add_mailbox_signature.sql", + "0022_add_mailbox_auto_reply.sql", + ]) { + assert.ok(names.includes(name), `MIGRATION_NAMES is missing ${name}`); + } +}); + +test("fresh bootstrap schema accepts the current Drizzle mailbox and license inserts", () => { + const sql = initialSchemaSql(); + const mailboxCreate = sql.match(/CREATE TABLE IF NOT EXISTS mailboxes \(([\s\S]*?)\);/); + assert.ok(mailboxCreate, "mailboxes CREATE TABLE not found"); + assert.match(mailboxCreate[1], /\bsignature\b/); + assert.match(mailboxCreate[1], /\bauto_reply_enabled\b/); + assert.match(mailboxCreate[1], /\bauto_reply_subject\b/); + assert.match(mailboxCreate[1], /\bauto_reply_body\b/); + assert.match(sql, /CREATE TABLE IF NOT EXISTS license_settings \(/); + assert.match(sql, /CREATE TABLE IF NOT EXISTS auto_reply_deliveries \(/); + + const py = ` +import sqlite3, sys +sql = sys.stdin.read() +db = sqlite3.connect(":memory:") +for stmt in sql.split(";"): + s = stmt.strip() + if s: + db.execute(s) +db.execute("INSERT INTO users (id,email,password_hash,name,created_at) VALUES ('u','a@b.c','x','n',1)") +db.execute("INSERT INTO domains (id,user_id,hostname,zone_id,created_at) VALUES ('d','u','ex.com','z',1)") +db.execute(""" +INSERT INTO mailboxes ( + id, user_id, domain_id, local_part, display_name, + signature, auto_reply_enabled, auto_reply_subject, auto_reply_body, created_at +) VALUES ('m','u','d','admin','admin','sig',0,'Out of office','',1) +""") +db.execute("INSERT INTO license_settings (id, instance_id, updated_at) VALUES ('default','inst',1)") +db.execute("INSERT INTO auto_reply_deliveries (id, mailbox_id, recipient, sent_at) VALUES ('ar','m','x@y.z',1)") +print("ok") +`; + const result = spawnSync("python3", ["-c", py], { input: sql, encoding: "utf8" }); + assert.equal(result.status, 0, result.stderr + result.stdout); + assert.match(result.stdout, /^ok$/m); +});