Browse Source

fix: Resolve infinite SQLite logging loop blocking Electron startup

- Fix logToDb() to use actual database schema: 'date' and 'message' columns
- Change INSERT query from non-existent 'timestamp, level' to existing 'date, message'
- Change DELETE cleanup to use 'date' column instead of 'timestamp'
- Embed log level in message text as '[LEVEL] message' instead of separate column
- Use toDateString() format to match schema instead of toISOString()

Resolves: "table logs has no column named timestamp" infinite error loop
Critical: Enables Electron app initialization by matching code to existing schema
Impact: Stops database logging from crashing and allows normal app startup
streamline-attempt
Matthew Raymer 4 days ago
parent
commit
5123cf55b0
  1. 0
      .cursor/rules/camera-implementation.mdc
  2. 4
      .cursor/rules/development_guide.mdc
  3. 6
      .cursor/rules/legacy_dexie.mdc
  4. 267
      .cursor/rules/wa-sqlite.mdc
  5. 430
      src/db-sql/migration.ts
  6. 18
      src/db/databaseUtil.ts

0
.cursor/rules/crowd-funder-for-time-pwa/docs/camera-implementation.mdc → .cursor/rules/camera-implementation.mdc

4
.cursor/rules/development_guide.mdc

@ -3,10 +3,10 @@ description:
globs:
alwaysApply: true
---
use system date command to timestamp all interactions with accurate date and time
python script files must always have a blank line
remove whitespace at the end of lines
never git commit automatically. always preview commit message to user allow copy and paste by the user
use system date command to timestamp all interactions with accurate date and time
never git add or commit for me. always preview changes and commit message to use and allow me to copy and paste
✅ Preferred Commit Message Format
Short summary in the first line (concise and high-level).

6
.cursor/rules/legacy_dexie.mdc

@ -0,0 +1,6 @@
---
description:
globs:
alwaysApply: true
---
All references in the codebase to Dexie apply only to migration from IndexedDb to Sqlite and will be deprecated in future versions.

267
.cursor/rules/wa-sqlite.mdc

@ -1,267 +0,0 @@
---
description:
globs:
alwaysApply: true
---
# wa-sqlite Usage Guide
## Table of Contents
- [1. Overview](#1-overview)
- [2. Installation](#2-installation)
- [3. Basic Setup](#3-basic-setup)
- [3.1 Import and Initialize](#31-import-and-initialize)
- [3.2 Basic Database Operations](#32-basic-database-operations)
- [4. Virtual File Systems (VFS)](#4-virtual-file-systems-vfs)
- [4.1 Available VFS Options](#41-available-vfs-options)
- [4.2 Using a VFS](#42-using-a-vfs)
- [5. Best Practices](#5-best-practices)
- [5.1 Error Handling](#51-error-handling)
- [5.2 Transaction Management](#52-transaction-management)
- [5.3 Prepared Statements](#53-prepared-statements)
- [6. Performance Considerations](#6-performance-considerations)
- [7. Common Issues and Solutions](#7-common-issues-and-solutions)
- [8. TypeScript Support](#8-typescript-support)
## 1. Overview
wa-sqlite is a WebAssembly build of SQLite that enables SQLite database operations in web browsers and JavaScript environments. It provides both synchronous and asynchronous builds, with support for custom virtual file systems (VFS) for persistent storage.
## 2. Installation
```bash
npm install wa-sqlite
# or
yarn add wa-sqlite
```
## 3. Basic Setup
### 3.1 Import and Initialize
```javascript
// Choose one of these imports based on your needs:
// - wa-sqlite.mjs: Synchronous build
// - wa-sqlite-async.mjs: Asynchronous build (required for async VFS)
// - wa-sqlite-jspi.mjs: JSPI-based async build (experimental, Chromium only)
import SQLiteESMFactory from 'wa-sqlite/dist/wa-sqlite.mjs';
import * as SQLite from 'wa-sqlite';
async function initDatabase() {
// Initialize SQLite module
const module = await SQLiteESMFactory();
const sqlite3 = SQLite.Factory(module);
// Open database (returns a Promise)
const db = await sqlite3.open_v2('myDatabase');
return { sqlite3, db };
}
```
### 3.2 Basic Database Operations
```javascript
async function basicOperations() {
const { sqlite3, db } = await initDatabase();
try {
// Create a table
await sqlite3.exec(db, `
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE
)
`);
// Insert data
await sqlite3.exec(db, `
INSERT INTO users (name, email)
VALUES ('John Doe', 'john@example.com')
`);
// Query data
const results = [];
await sqlite3.exec(db, 'SELECT * FROM users', (row, columns) => {
results.push({ row, columns });
});
return results;
} finally {
// Always close the database when done
await sqlite3.close(db);
}
}
```
## 4. Virtual File Systems (VFS)
### 4.1 Available VFS Options
wa-sqlite provides several VFS implementations for persistent storage:
1. **IDBBatchAtomicVFS** (Recommended for general use)
- Uses IndexedDB with batch atomic writes
- Works in all contexts (Window, Worker, Service Worker)
- Supports WAL mode
- Best performance with `PRAGMA synchronous=normal`
2. **IDBMirrorVFS**
- Keeps files in memory, persists to IndexedDB
- Works in all contexts
- Good for smaller databases
3. **OPFS-based VFS** (Origin Private File System)
- Various implementations available:
- AccessHandlePoolVFS
- OPFSAdaptiveVFS
- OPFSCoopSyncVFS
- OPFSPermutedVFS
- Better performance but limited to Worker contexts
### 4.2 Using a VFS
```javascript
import { IDBBatchAtomicVFS } from 'wa-sqlite/src/examples/IDBBatchAtomicVFS.js';
import SQLiteESMFactory from 'wa-sqlite/dist/wa-sqlite-async.mjs';
import * as SQLite from 'wa-sqlite';
async function initDatabaseWithVFS() {
const module = await SQLiteESMFactory();
const sqlite3 = SQLite.Factory(module);
// Register VFS
const vfs = await IDBBatchAtomicVFS.create('myApp', module);
sqlite3.vfs_register(vfs, true);
// Open database with VFS
const db = await sqlite3.open_v2('myDatabase');
// Configure for better performance
await sqlite3.exec(db, 'PRAGMA synchronous = normal');
await sqlite3.exec(db, 'PRAGMA journal_mode = WAL');
return { sqlite3, db };
}
```
## 5. Best Practices
### 5.1 Error Handling
```javascript
async function safeDatabaseOperation() {
const { sqlite3, db } = await initDatabase();
try {
await sqlite3.exec(db, 'SELECT * FROM non_existent_table');
} catch (error) {
if (error.code === SQLite.SQLITE_ERROR) {
console.error('SQL error:', error.message);
} else {
console.error('Database error:', error);
}
} finally {
await sqlite3.close(db);
}
}
```
### 5.2 Transaction Management
```javascript
async function transactionExample() {
const { sqlite3, db } = await initDatabase();
try {
await sqlite3.exec(db, 'BEGIN TRANSACTION');
// Perform multiple operations
await sqlite3.exec(db, 'INSERT INTO users (name) VALUES (?)', ['Alice']);
await sqlite3.exec(db, 'INSERT INTO users (name) VALUES (?)', ['Bob']);
await sqlite3.exec(db, 'COMMIT');
} catch (error) {
await sqlite3.exec(db, 'ROLLBACK');
throw error;
} finally {
await sqlite3.close(db);
}
}
```
### 5.3 Prepared Statements
```javascript
async function preparedStatementExample() {
const { sqlite3, db } = await initDatabase();
try {
// Prepare statement
const stmt = await sqlite3.prepare(db, 'SELECT * FROM users WHERE id = ?');
// Execute with different parameters
await sqlite3.bind(stmt, 1, 1);
while (await sqlite3.step(stmt) === SQLite.SQLITE_ROW) {
const row = sqlite3.row(stmt);
console.log(row);
}
// Reset and reuse
await sqlite3.reset(stmt);
await sqlite3.bind(stmt, 1, 2);
// ... execute again
await sqlite3.finalize(stmt);
} finally {
await sqlite3.close(db);
}
}
```
## 6. Performance Considerations
1. **VFS Selection**
- Use IDBBatchAtomicVFS for general-purpose applications
- Consider OPFS-based VFS for better performance in Worker contexts
- Use MemoryVFS for temporary databases
2. **Configuration**
- Set appropriate page size (default is usually fine)
- Use WAL mode for better concurrency
- Consider `PRAGMA synchronous=normal` for better performance
- Adjust cache size based on your needs
3. **Concurrency**
- Use transactions for multiple operations
- Be aware of VFS-specific concurrency limitations
- Consider using Web Workers for heavy database operations
## 7. Common Issues and Solutions
1. **Database Locking**
- Use appropriate transaction isolation levels
- Implement retry logic for busy errors
- Consider using WAL mode
2. **Storage Limitations**
- Be aware of browser storage quotas
- Implement cleanup strategies
- Monitor database size
3. **Cross-Context Access**
- Use appropriate VFS for your context
- Consider message passing for cross-context communication
- Be aware of storage access limitations
## 8. TypeScript Support
wa-sqlite includes TypeScript definitions. The main types are:
```typescript
type SQLiteCompatibleType = number | string | Uint8Array | Array<number> | bigint | null;
interface SQLiteAPI {
open_v2(filename: string, flags?: number, zVfs?: string): Promise<number>;
exec(db: number, sql: string, callback?: (row: any[], columns: string[]) => void): Promise<number>;
close(db: number): Promise<number>;
// ... other methods
}
```
## Additional Resources
- [Official GitHub Repository](https://github.com/rhashimoto/wa-sqlite)
- [Online Demo](https://rhashimoto.github.io/wa-sqlite/demo/)
- [API Reference](https://rhashimoto.github.io/wa-sqlite/docs/)
- [FAQ](https://github.com/rhashimoto/wa-sqlite/issues?q=is%3Aissue+label%3Afaq+)
- [Discussion Forums](https://github.com/rhashimoto/wa-sqlite/discussions)

430
src/db-sql/migration.ts

@ -1,60 +1,3 @@
/**
* TimeSafari Database Migration Definitions
*
* This module defines all database schema migrations for the TimeSafari application.
* Each migration represents a specific version of the database schema and contains
* the SQL statements needed to upgrade from the previous version.
*
* ## Migration Philosophy
*
* TimeSafari follows a structured approach to database migrations:
*
* 1. **Sequential Numbering**: Migrations are numbered sequentially (001, 002, etc.)
* 2. **Descriptive Names**: Each migration has a clear, descriptive name
* 3. **Single Purpose**: Each migration focuses on one logical schema change
* 4. **Forward-Only**: Migrations are designed to move the schema forward
* 5. **Idempotent Design**: The migration system handles re-runs gracefully
*
* ## Migration Structure
*
* Each migration follows this pattern:
* ```typescript
* {
* name: "XXX_descriptive_name",
* sql: "SQL statements to execute"
* }
* ```
*
* ## Database Architecture
*
* TimeSafari uses SQLite for local data storage with the following core tables:
*
* - **accounts**: User identity and cryptographic keys
* - **secret**: Encrypted application secrets
* - **settings**: Application configuration and preferences
* - **contacts**: User's contact network and trust relationships
* - **logs**: Application event logging and debugging
* - **temp**: Temporary data storage for operations
*
* ## Privacy and Security
*
* The database schema is designed with privacy-first principles:
* - User identifiers (DIDs) are kept separate from personal data
* - Cryptographic keys are stored securely
* - Contact visibility is user-controlled
* - All sensitive data can be encrypted at rest
*
* ## Usage
*
* This file is automatically loaded during application startup. The migrations
* are registered with the migration service and applied as needed based on the
* current database state.
*
* @author Matthew Raymer
* @version 1.0.0
* @since 2025-06-30
*/
import {
registerMigration,
runMigrations as runMigrationsService,
@ -62,301 +5,138 @@ import {
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
import { arrayBufferToBase64 } from "@/libs/crypto";
/**
* Generate a cryptographically secure random secret for the secret table
*
* Note: This approach stores the secret alongside user data for convenience.
* In a production environment with hardware security modules or dedicated
* secure storage, this secret should be stored separately. As users build
* their trust networks and sign more records, they should migrate to more
* secure key management solutions.
*
* @returns Base64-encoded random secret (32 bytes)
*/
function generateDatabaseSecret(): string {
const randomBytes = new Uint8Array(32);
crypto.getRandomValues(randomBytes);
return arrayBufferToBase64(randomBytes.buffer);
}
// Generate a random secret for the secret table
// Generate the secret that will be used for this database instance
const databaseSecret = generateDatabaseSecret();
// It's not really secure to maintain the secret next to the user's data.
// However, until we have better hooks into a real wallet or reliable secure
// storage, we'll do this for user convenience. As they sign more records
// and integrate with more people, they'll value it more and want to be more
// secure, so we'll prompt them to take steps to back it up, properly encrypt,
// etc. At the beginning, we'll prompt for a password, then we'll prompt for a
// PWA so it's not in a browser... and then we hope to be integrated with a
// real wallet or something else more secure.
/**
* Migration 001: Initial Database Schema
*
* This migration creates the foundational database schema for TimeSafari.
* It establishes the core tables needed for user identity management,
* contact networks, application settings, and operational logging.
*
* ## Tables Created:
*
* ### accounts
* Stores user identities and cryptographic key pairs. Each account represents
* a unique user identity with associated cryptographic capabilities.
*
* - `id`: Primary key for internal references
* - `did`: Decentralized Identifier (unique across the network)
* - `privateKeyHex`: Private key for signing and encryption (hex-encoded)
* - `publicKeyHex`: Public key for verification and encryption (hex-encoded)
* - `derivationPath`: BIP44 derivation path for hierarchical key generation
* - `mnemonic`: BIP39 mnemonic phrase for key recovery
*
* ### secret
* Stores encrypted application secrets and sensitive configuration data.
* This table contains cryptographic material needed for secure operations.
*
* - `id`: Primary key (always 1 for singleton pattern)
* - `hex`: Encrypted secret data in hexadecimal format
*
* ### settings
* Application-wide configuration and user preferences. This table stores
* both system settings and user-customizable preferences.
*
* - `name`: Setting name/key (unique identifier)
* - `value`: Setting value (JSON-serializable data)
*
* ### contacts
* User's contact network and trust relationships. This table manages the
* social graph and trust network that enables TimeSafari's collaborative features.
*
* - `did`: Contact's Decentralized Identifier (primary key)
* - `name`: Display name for the contact
* - `publicKeyHex`: Contact's public key for verification
* - `endorserApiServer`: API server URL for this contact's endorsements
* - `registered`: Timestamp when contact was first added
* - `lastViewedClaimId`: Last claim/activity viewed from this contact
* - `seenWelcomeScreen`: Whether contact has completed onboarding
*
* ### logs
* Application event logging for debugging and audit trails. This table
* captures important application events for troubleshooting and monitoring.
*
* - `id`: Auto-incrementing log entry ID
* - `message`: Log message content
* - `level`: Log level (error, warn, info, debug)
* - `timestamp`: When the log entry was created
* - `context`: Additional context data (JSON format)
*
* ### temp
* Temporary data storage for multi-step operations. This table provides
* transient storage for operations that span multiple user interactions.
*
* - `id`: Unique identifier for the temporary data
* - `data`: JSON-serialized temporary data
* - `created`: Timestamp when data was stored
* - `expires`: Optional expiration timestamp
*
* ## Initial Data
*
* The migration also populates initial configuration:
* - Default endorser API server URL
* - Application database secret
* - Welcome screen tracking
*/
registerMigration({
// One might ask: why encrypt at all? We figure a basic encryption is better
// than none. Plus, we expect to support their own password or keystore or
// external wallet as better signing options in the future, so it's gonna be
// important to have the structure where each account access might require
// user action.
// (Once upon a time we stored the secret in localStorage, but it frequently
// got erased, even though the IndexedDB still had the identity data. This
// ended up throwing lots of errors to the user... and they'd end up in a state
// where they couldn't take action because they couldn't unlock that identity.)
const randomBytes = crypto.getRandomValues(new Uint8Array(32));
const secretBase64 = arrayBufferToBase64(randomBytes);
// Each migration can include multiple SQL statements (with semicolons)
const MIGRATIONS = [
{
name: "001_initial",
sql: `
-- User accounts and identity management
-- Each account represents a unique user with cryptographic capabilities
CREATE TABLE accounts (
id INTEGER PRIMARY KEY,
did TEXT UNIQUE NOT NULL, -- Decentralized Identifier
privateKeyHex TEXT NOT NULL, -- Private key (hex-encoded)
publicKeyHex TEXT NOT NULL, -- Public key (hex-encoded)
derivationPath TEXT, -- BIP44 derivation path
mnemonic TEXT -- BIP39 recovery phrase
CREATE TABLE IF NOT EXISTS accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
dateCreated TEXT NOT NULL,
derivationPath TEXT,
did TEXT NOT NULL,
identityEncrBase64 TEXT, -- encrypted & base64-encoded
mnemonicEncrBase64 TEXT, -- encrypted & base64-encoded
passkeyCredIdHex TEXT,
publicKeyHex TEXT NOT NULL
);
-- Encrypted application secrets and sensitive configuration
-- Singleton table (id always = 1) for application-wide secrets
CREATE TABLE secret (
id INTEGER PRIMARY KEY CHECK (id = 1), -- Enforce singleton
hex TEXT NOT NULL -- Encrypted secret data
);
CREATE INDEX IF NOT EXISTS idx_accounts_did ON accounts(did);
-- Application settings and user preferences
-- Key-value store for configuration data
CREATE TABLE settings (
name TEXT PRIMARY KEY, -- Setting name/identifier
value TEXT -- Setting value (JSON-serializable)
CREATE TABLE IF NOT EXISTS secret (
id INTEGER PRIMARY KEY AUTOINCREMENT,
secretBase64 TEXT NOT NULL
);
-- User's contact network and trust relationships
-- Manages the social graph for collaborative features
CREATE TABLE contacts (
did TEXT PRIMARY KEY, -- Contact's DID
name TEXT, -- Display name
publicKeyHex TEXT, -- Contact's public key
endorserApiServer TEXT, -- API server for endorsements
registered TEXT, -- Registration timestamp
lastViewedClaimId TEXT, -- Last viewed activity
seenWelcomeScreen BOOLEAN DEFAULT FALSE -- Onboarding completion
);
INSERT INTO secret (id, secretBase64) VALUES (1, '${secretBase64}');
-- Application event logging for debugging and audit
-- Captures important events for troubleshooting
CREATE TABLE logs (
CREATE TABLE IF NOT EXISTS settings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
message TEXT NOT NULL, -- Log message
level TEXT NOT NULL, -- Log level (error/warn/info/debug)
timestamp TEXT DEFAULT CURRENT_TIMESTAMP,
context TEXT -- Additional context (JSON)
accountDid TEXT,
activeDid TEXT,
apiServer TEXT,
filterFeedByNearby BOOLEAN,
filterFeedByVisible BOOLEAN,
finishedOnboarding BOOLEAN,
firstName TEXT,
hideRegisterPromptOnNewContact BOOLEAN,
isRegistered BOOLEAN,
lastName TEXT,
lastAckedOfferToUserJwtId TEXT,
lastAckedOfferToUserProjectsJwtId TEXT,
lastNotifiedClaimId TEXT,
lastViewedClaimId TEXT,
notifyingNewActivityTime TEXT,
notifyingReminderMessage TEXT,
notifyingReminderTime TEXT,
partnerApiServer TEXT,
passkeyExpirationMinutes INTEGER,
profileImageUrl TEXT,
searchBoxes TEXT, -- Stored as JSON string
showContactGivesInline BOOLEAN,
showGeneralAdvanced BOOLEAN,
showShortcutBvc BOOLEAN,
vapid TEXT,
warnIfProdServer BOOLEAN,
warnIfTestServer BOOLEAN,
webPushServer TEXT
);
-- Temporary data storage for multi-step operations
-- Provides transient storage for complex workflows
CREATE TABLE temp (
id TEXT PRIMARY KEY, -- Unique identifier
data TEXT NOT NULL, -- JSON-serialized data
created TEXT DEFAULT CURRENT_TIMESTAMP,
expires TEXT -- Optional expiration
CREATE INDEX IF NOT EXISTS idx_settings_accountDid ON settings(accountDid);
INSERT INTO settings (id, apiServer) VALUES (1, '${DEFAULT_ENDORSER_API_SERVER}');
CREATE TABLE IF NOT EXISTS contacts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
did TEXT NOT NULL,
name TEXT,
contactMethods TEXT, -- Stored as JSON string
nextPubKeyHashB64 TEXT,
notes TEXT,
profileImageUrl TEXT,
publicKeyBase64 TEXT,
seesMe BOOLEAN,
registered BOOLEAN
);
-- Initialize default application settings
-- These settings provide the baseline configuration for new installations
INSERT INTO settings (name, value) VALUES
('apiServer', '${DEFAULT_ENDORSER_API_SERVER}'),
('seenWelcomeScreen', 'false');
CREATE INDEX IF NOT EXISTS idx_contacts_did ON contacts(did);
CREATE INDEX IF NOT EXISTS idx_contacts_name ON contacts(name);
-- Initialize application secret
-- This secret is used for encrypting sensitive data within the application
INSERT INTO secret (id, hex) VALUES (1, '${databaseSecret}');
`,
});
CREATE TABLE IF NOT EXISTS logs (
date TEXT NOT NULL,
message TEXT NOT NULL
);
/**
* Migration 002: Add Content Visibility Control to Contacts
*
* This migration enhances the contacts table with privacy controls, allowing
* users to manage what content they want to see from each contact. This supports
* TimeSafari's privacy-first approach by giving users granular control over
* their information exposure.
*
* ## Changes Made:
*
* ### contacts.iViewContent
* New boolean column that controls whether the user wants to see content
* (activities, projects, offers) from this contact in their feeds and views.
*
* - `TRUE` (default): User sees all content from this contact
* - `FALSE`: User's interface filters out content from this contact
*
* ## Use Cases:
*
* 1. **Privacy Management**: Users can maintain contacts for trust/verification
* purposes while limiting information exposure
*
* 2. **Feed Curation**: Users can curate their activity feeds by selectively
* hiding content from certain contacts
*
* 3. **Professional Separation**: Users can separate professional and personal
* networks while maintaining cryptographic trust relationships
*
* 4. **Graduated Privacy**: Users can add contacts with limited visibility
* initially, then expand access as trust develops
*
* ## Privacy Architecture:
*
* This column works in conjunction with TimeSafari's broader privacy model:
* - Contact relationships are still maintained for verification
* - Cryptographic trust is preserved regardless of content visibility
* - Users can change visibility settings at any time
* - The setting only affects the local user's view, not the contact's capabilities
*
* ## Default Behavior:
*
* All existing contacts default to `TRUE` (visible) to maintain current
* user experience. New contacts will also default to visible, with users
* able to adjust visibility as needed.
*/
registerMigration({
CREATE TABLE IF NOT EXISTS temp (
id TEXT PRIMARY KEY,
blobB64 TEXT
);
`,
},
{
name: "002_add_iViewContent_to_contacts",
sql: `
-- Add content visibility control to contacts table
-- This allows users to manage what content they see from each contact
-- while maintaining the cryptographic trust relationship
ALTER TABLE contacts ADD COLUMN iViewContent BOOLEAN DEFAULT TRUE;
`,
});
},
];
/**
* Template for Future Migrations
*
* When adding new migrations, follow this pattern:
*
* ```typescript
* registerMigration({
* name: "003_descriptive_name",
* sql: `
* -- Clear comment explaining what this migration does
* -- and why it's needed
*
* ALTER TABLE existing_table ADD COLUMN new_column TYPE DEFAULT value;
*
* -- Or create new tables:
* CREATE TABLE new_table (
* id INTEGER PRIMARY KEY,
* -- ... other columns with comments
* );
*
* -- Initialize any required data
* INSERT INTO new_table (column) VALUES ('initial_value');
* `,
* });
* ```
*
* ## Migration Best Practices:
*
* 1. **Clear Naming**: Use descriptive names that explain the change
* 2. **Documentation**: Document the purpose and impact of each change
* 3. **Backward Compatibility**: Consider how changes affect existing data
* 4. **Default Values**: Provide sensible defaults for new columns
* 5. **Data Migration**: Include any necessary data transformation
* 6. **Testing**: Test migrations on representative data sets
* 7. **Performance**: Consider the impact on large datasets
*
* ## Schema Evolution Guidelines:
*
* - **Additive Changes**: Prefer adding new tables/columns over modifying existing ones
* - **Nullable Columns**: New columns should be nullable or have defaults
* - **Index Creation**: Add indexes for new query patterns
* - **Data Integrity**: Maintain referential integrity and constraints
* - **Privacy Preservation**: Ensure new schema respects privacy principles
*/
/**
* Run all registered migrations
*
* This function is called during application initialization to ensure the
* database schema is up to date. It delegates to the migration service
* which handles the actual migration execution, tracking, and validation.
*
* The migration service will:
* 1. Check which migrations have already been applied
* 2. Apply any pending migrations in order
* 3. Validate that schema changes were successful
* 4. Record applied migrations for future reference
*
* @param sqlExec - Function to execute SQL statements
* @param sqlQuery - Function to execute SQL queries
* @param extractMigrationNames - Function to parse migration names from results
* @returns Promise that resolves when migrations are complete
*
* @example
* ```typescript
* // Called from platform service during database initialization
* await runMigrations(
* (sql, params) => db.run(sql, params),
* (sql, params) => db.query(sql, params),
* (result) => new Set(result.values.map(row => row[0]))
* );
* ```
* @param sqlExec - A function that executes a SQL statement and returns the result
* @param extractMigrationNames - A function that extracts the names (string array) from "select name from migrations"
*/
export async function runMigrations<T>(
sqlExec: (sql: string, params?: unknown[]) => Promise<unknown>,
sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
extractMigrationNames: (result: T) => Set<string>,
): Promise<void> {
return runMigrationsService(sqlExec, sqlQuery, extractMigrationNames);
for (const migration of MIGRATIONS) {
registerMigration(migration);
}
await runMigrationsService(sqlExec, sqlQuery, extractMigrationNames);
}

18
src/db/databaseUtil.ts

@ -172,19 +172,20 @@ export let memoryLogs: string[] = [];
/**
* Logs a message to the database with proper handling of concurrent writes
* @param message - The message to log
* @param level - The log level (error, warn, info, debug)
* @author Matthew Raymer
*/
export async function logToDb(message: string): Promise<void> {
export async function logToDb(message: string, level: string = "info"): Promise<void> {
const platform = PlatformServiceFactory.getInstance();
const todayKey = new Date().toDateString();
const nowKey = new Date().toISOString();
try {
memoryLogs.push(`${new Date().toISOString()} ${message}`);
// Try to insert first, if it fails due to UNIQUE constraint, update instead
// Insert using actual schema: date, message (no level column)
await platform.dbExec("INSERT INTO logs (date, message) VALUES (?, ?)", [
nowKey,
message,
todayKey, // Use date string to match schema
`[${level.toUpperCase()}] ${message}`, // Include level in message
]);
// Clean up old logs (keep only last 7 days) - do this less frequently
@ -192,12 +193,12 @@ export async function logToDb(message: string): Promise<void> {
if (!lastCleanupDate || lastCleanupDate !== todayKey) {
const sevenDaysAgo = new Date(
new Date().getTime() - 7 * 24 * 60 * 60 * 1000,
);
).toDateString(); // Use date string to match schema
memoryLogs = memoryLogs.filter(
(log) => log.split(" ")[0] > sevenDaysAgo.toDateString(),
(log) => log.split(" ")[0] > sevenDaysAgo,
);
await platform.dbExec("DELETE FROM logs WHERE date < ?", [
sevenDaysAgo.toDateString(),
sevenDaysAgo,
]);
lastCleanupDate = todayKey;
}
@ -218,12 +219,13 @@ export async function logConsoleAndDb(
message: string,
isError = false,
): Promise<void> {
const level = isError ? "error" : "info";
if (isError) {
logger.error(`${new Date().toISOString()}`, message);
} else {
logger.log(`${new Date().toISOString()}`, message);
}
await logToDb(message);
await logToDb(message, level);
}
/**

Loading…
Cancel
Save