From 88919fa0d303c89e3a88ff0889c61663de8a8dc3 Mon Sep 17 00:00:00 2001 From: paul Date: Mon, 14 Jul 2025 20:44:58 +0200 Subject: [PATCH] fix: critical boolean compatibility for PostgreSQL/SQLite Fixed boolean value handling differences between databases: - SQLite stores booleans as 0/1, PostgreSQL as true/false - Add formatBoolean() calls to critical queries that were failing Files fixed: - adminEvents.js: Fixed status filters and archive queries - adminDashboard.js: Fixed active/archived event counts - expirationChecker.js: Fixed expiration checking queries - dbCompat.js: Updated to avoid circular dependency Added migration 024 to: - Enable foreign keys for SQLite (PRAGMA foreign_keys = ON) - Document boolean compatibility requirements This fixes queries returning 0 results in SQLite when checking boolean columns like is_active, is_archived. Critical for proper event management and expiration handling. Note: 23 more boolean comparisons remain to be fixed in other files. --- .../024_fix_boolean_compatibility.js | 29 +++++++++++++++++++ backend/src/routes/adminDashboard.js | 11 +++---- backend/src/routes/adminEvents.js | 13 +++++---- backend/src/services/expirationChecker.js | 9 +++--- backend/src/utils/dbCompat.js | 9 ++++-- 5 files changed, 53 insertions(+), 18 deletions(-) create mode 100644 backend/migrations/024_fix_boolean_compatibility.js diff --git a/backend/migrations/024_fix_boolean_compatibility.js b/backend/migrations/024_fix_boolean_compatibility.js new file mode 100644 index 0000000..d31147a --- /dev/null +++ b/backend/migrations/024_fix_boolean_compatibility.js @@ -0,0 +1,29 @@ +/** + * Fix boolean compatibility issues between PostgreSQL and SQLite + * This migration updates the database configuration and existing data + */ + +exports.up = async function(knex) { + const isPostgres = knex.client.config.client === 'pg'; + + if (!isPostgres) { + // Enable foreign keys for SQLite + await knex.raw('PRAGMA foreign_keys = ON'); + + // Note: SQLite stores booleans as 0/1 + // No data migration needed as Knex handles this automatically + // But queries must use formatBoolean() helper + + console.log('SQLite boolean compatibility check:'); + console.log('- SQLite stores booleans as 0/1'); + console.log('- All boolean comparisons should use formatBoolean() helper'); + console.log('- Foreign keys enabled'); + } + + return Promise.resolve(); +}; + +exports.down = async function(knex) { + // No rollback needed + return Promise.resolve(); +}; \ No newline at end of file diff --git a/backend/src/routes/adminDashboard.js b/backend/src/routes/adminDashboard.js index b3456c6..3817acf 100644 --- a/backend/src/routes/adminDashboard.js +++ b/backend/src/routes/adminDashboard.js @@ -2,6 +2,7 @@ const express = require('express'); const { db } = require('../database/db'); const { adminAuth } = require('../middleware/auth-enhanced-v2'); const { sanitizeDays, addDateRangeCondition } = require('../utils/sqlSecurity'); +const { formatBoolean } = require('../utils/dbCompat'); const router = express.Router(); // Get dashboard statistics @@ -9,8 +10,8 @@ router.get('/stats', adminAuth, async (req, res) => { try { // Get active events count const activeEvents = await db('events') - .where('is_active', true) - .where('is_archived', false) + .where('is_active', formatBoolean(true)) + .where('is_archived', formatBoolean(false)) .count('id as count') .first(); @@ -20,8 +21,8 @@ router.get('/stats', adminAuth, async (req, res) => { const now = new Date(); const expiringEvents = await db('events') - .where('is_active', true) - .where('is_archived', false) + .where('is_active', formatBoolean(true)) + .where('is_archived', formatBoolean(false)) .where('expires_at', '<=', sevenDaysFromNow.toISOString()) .where('expires_at', '>', now.toISOString()) .count('id as count') @@ -56,7 +57,7 @@ router.get('/stats', adminAuth, async (req, res) => { // Get archived events count const archivedEvents = await db('events') - .where('is_archived', true) + .where('is_archived', formatBoolean(true)) .count('id as count') .first(); diff --git a/backend/src/routes/adminEvents.js b/backend/src/routes/adminEvents.js index 6ff5ec5..dfc4b83 100644 --- a/backend/src/routes/adminEvents.js +++ b/backend/src/routes/adminEvents.js @@ -11,6 +11,7 @@ const { archiveEvent } = require('../services/archiveService'); const { escapeLikePattern } = require('../utils/sqlSecurity'); const { formatDate } = require('../utils/dateFormatter'); const { validatePasswordInContext, getBcryptRounds } = require('../utils/passwordValidation'); +const { formatBoolean } = require('../utils/dbCompat'); // Create new event router.post('/', adminAuth, [ @@ -183,17 +184,17 @@ router.get('/', adminAuth, async (req, res) => { // Apply status filter if (status === 'active') { - query = query.where('is_active', true).where('is_archived', false); + query = query.where('is_active', formatBoolean(true)).where('is_archived', formatBoolean(false)); } else if (status === 'archived') { - query = query.where('is_archived', true); + query = query.where('is_archived', formatBoolean(true)); } else if (status === 'inactive') { - query = query.where('is_active', false).where('is_archived', false); + query = query.where('is_active', formatBoolean(false)).where('is_archived', formatBoolean(false)); } else if (status === 'expiring') { const sevenDaysFromNow = new Date(); sevenDaysFromNow.setDate(sevenDaysFromNow.getDate() + 7); query = query - .where('is_active', true) - .where('is_archived', false) + .where('is_active', formatBoolean(true)) + .where('is_archived', formatBoolean(false)) .where('expires_at', '<=', sevenDaysFromNow.toISOString()) .where('expires_at', '>', new Date().toISOString()); } @@ -554,7 +555,7 @@ router.post('/bulk-archive', adminAuth, [ // Get all events to archive const events = await db('events') .whereIn('id', eventIds) - .where('is_archived', false); + .where('is_archived', formatBoolean(false)); if (events.length === 0) { return res.status(400).json({ error: 'No valid events found to archive' }); diff --git a/backend/src/services/expirationChecker.js b/backend/src/services/expirationChecker.js index ad7bed1..27ffbe0 100644 --- a/backend/src/services/expirationChecker.js +++ b/backend/src/services/expirationChecker.js @@ -4,6 +4,7 @@ const { archiveEvent } = require('./archiveService'); const { queueEmail } = require('./emailProcessor'); const logger = require('../utils/logger'); const { formatDate } = require('../utils/dateFormatter'); +const { formatBoolean } = require('../utils/dbCompat'); function startExpirationChecker() { // Check every hour for expired events and warnings @@ -21,8 +22,8 @@ async function checkExpirations() { // Check for events needing warning emails const eventsNeedingWarning = await db('events') - .where('is_active', true) - .where('is_archived', false) + .where('is_active', formatBoolean(true)) + .where('is_archived', formatBoolean(false)) .where('expires_at', '<=', warningDate) .where('expires_at', '>', now); @@ -40,8 +41,8 @@ async function checkExpirations() { // Check for expired events const expiredEvents = await db('events') - .where('is_active', true) - .where('is_archived', false) + .where('is_active', formatBoolean(true)) + .where('is_archived', formatBoolean(false)) .where('expires_at', '<=', now); for (const event of expiredEvents) { diff --git a/backend/src/utils/dbCompat.js b/backend/src/utils/dbCompat.js index 2bf28f8..2e28033 100644 --- a/backend/src/utils/dbCompat.js +++ b/backend/src/utils/dbCompat.js @@ -3,7 +3,8 @@ * Handles differences between PostgreSQL and SQLite */ -const { db } = require('../database/db'); +// Note: Requiring db here creates circular dependency +// db should be passed as parameter or required where needed /** * Get database client type @@ -58,10 +59,11 @@ function addDays(date, days) { /** * Get date extraction SQL that works on both databases + * @param {object} db - Knex database instance * @param {string} column - Column name * @returns {object} Knex raw query */ -function dateExtractSQL(column) { +function dateExtractSQL(db, column) { if (isPostgreSQL()) { return db.raw(`DATE(${column})`); } else { @@ -72,10 +74,11 @@ function dateExtractSQL(column) { /** * Get database size query + * @param {object} db - Knex database instance * @param {string} dbName - Database name * @returns {Promise} Size in bytes */ -async function getDatabaseSize(dbName) { +async function getDatabaseSize(db, dbName) { if (isPostgreSQL()) { const result = await db.raw('SELECT pg_database_size(?) as size', [dbName]); return result.rows[0]?.size || 0;