feat(cardav): add Migration 30 for CardDAV contacts schema

Add comprehensive database schema for CardDAV multi-account contacts sync:

New tables:
- cardav_accounts: Store CardDAV server credentials
- cardav_addressbook_selection: Per-account addressbook enable/disable
- contact_phones: Multiple phone numbers per contact with labels
- contact_emails: Multiple email addresses per contact with labels
- contact_addresses: Multiple postal addresses per contact with labels

Extended contacts table with 9 new columns:
- organization, job_title, birthday, website, photo, nickname
- cardav_account_id (FK to cardav_accounts, ON DELETE SET NULL)
- cardav_uid (vCard UID from server)
- cardav_addressbook_url (source addressbook URL)

Features:
- UNIQUE constraints on (cardav_url, username) and (account_id, addressbook_url)
- CASCADE delete for addressbook selection and contact sub-tables
- Performance indices for cardav_uid and email lookups
- Support for manual contacts (NULL cardav_account_id)
- is_primary flag for phone/email/address selection

Test coverage:
- 23 comprehensive tests verify all tables, constraints, indices
- FK CASCADE delete behavior validated
- UNIQUE constraints enforced
- Data integrity scenarios tested

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Ulas Kalayci
2026-05-04 10:47:16 +02:00
parent 6cc72676c6
commit 18310dbfe5
4 changed files with 574 additions and 3 deletions
+101
View File
@@ -1075,6 +1075,107 @@ const MIGRATIONS = [
db.exec(`CREATE UNIQUE INDEX idx_calendar_sub_extid ON calendar_events (subscription_id, external_calendar_id)`);
},
},
{
version: 30,
description: 'CardDAV multi-account contacts sync',
up: `
-- ========================================
-- CardDAV Accounts
-- ========================================
CREATE TABLE cardav_accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
cardav_url TEXT NOT NULL,
username TEXT NOT NULL,
password TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
last_sync TEXT,
UNIQUE(cardav_url, username)
);
-- ========================================
-- CardDAV Addressbook Selection
-- ========================================
CREATE TABLE cardav_addressbook_selection (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER NOT NULL,
addressbook_url TEXT NOT NULL,
addressbook_name TEXT NOT NULL,
enabled INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
UNIQUE(account_id, addressbook_url),
FOREIGN KEY(account_id) REFERENCES cardav_accounts(id) ON DELETE CASCADE
);
CREATE INDEX idx_cardav_addressbook_account
ON cardav_addressbook_selection(account_id, enabled);
-- ========================================
-- Extend Contacts Table for CardDAV
-- ========================================
ALTER TABLE contacts ADD COLUMN organization TEXT;
ALTER TABLE contacts ADD COLUMN job_title TEXT;
ALTER TABLE contacts ADD COLUMN birthday TEXT;
ALTER TABLE contacts ADD COLUMN website TEXT;
ALTER TABLE contacts ADD COLUMN photo TEXT;
ALTER TABLE contacts ADD COLUMN nickname TEXT;
ALTER TABLE contacts ADD COLUMN cardav_account_id INTEGER
REFERENCES cardav_accounts(id) ON DELETE SET NULL;
ALTER TABLE contacts ADD COLUMN cardav_uid TEXT;
ALTER TABLE contacts ADD COLUMN cardav_addressbook_url TEXT;
CREATE INDEX idx_contacts_cardav_uid ON contacts(cardav_uid);
CREATE INDEX idx_contacts_email ON contacts(email);
-- ========================================
-- Contact Phones (Multiple per Contact)
-- ========================================
CREATE TABLE contact_phones (
id INTEGER PRIMARY KEY AUTOINCREMENT,
contact_id INTEGER NOT NULL,
label TEXT,
value TEXT NOT NULL,
is_primary INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY(contact_id) REFERENCES contacts(id) ON DELETE CASCADE
);
CREATE INDEX idx_contact_phones_contact ON contact_phones(contact_id);
CREATE INDEX idx_contact_phones_value ON contact_phones(value);
-- ========================================
-- Contact Emails (Multiple per Contact)
-- ========================================
CREATE TABLE contact_emails (
id INTEGER PRIMARY KEY AUTOINCREMENT,
contact_id INTEGER NOT NULL,
label TEXT,
value TEXT NOT NULL,
is_primary INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY(contact_id) REFERENCES contacts(id) ON DELETE CASCADE
);
CREATE INDEX idx_contact_emails_contact ON contact_emails(contact_id);
CREATE INDEX idx_contact_emails_value ON contact_emails(value);
-- ========================================
-- Contact Addresses (Multiple per Contact)
-- ========================================
CREATE TABLE contact_addresses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
contact_id INTEGER NOT NULL,
label TEXT,
street TEXT,
city TEXT,
state TEXT,
postal_code TEXT,
country TEXT,
is_primary INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY(contact_id) REFERENCES contacts(id) ON DELETE CASCADE
);
CREATE INDEX idx_contact_addresses_contact ON contact_addresses(contact_id);
`,
},
];
/**