/** * Migration: Add soft delete mechanism to all tables * * Fixes: Most tables use CASCADE delete with no way to recover deleted data * * @type {import('node-pg-migrate').ColumnDefinitions | undefined} */ export const shorthands = undefined; /** * All tables that need soft delete support */ const TABLES_WITH_SOFT_DELETE = [ 'user_profiles', 'user_identities', 'devices', 'inventory_devices', 'raw_events', 'validated_events', 'analysis_results', 'weather_stations', 'weather_observations', 'weather_forecasts', 'camera_devices', 'subscription_plans', 'user_subscriptions', 'subscription_history', 'payment_records', 'device_configurations', 'device_certificates', 'device_security_events', 'device_registrations', ]; /** * @param pgm {import('node-pg-migrate').MigrationBuilder} * @param run {() => void | undefined} * @returns {Promise | void} */ export const up = (pgm) => { console.log('Adding soft delete columns to all tables...'); // Add deleted_at column to all tables TABLES_WITH_SOFT_DELETE.forEach((table) => { console.log(` Adding deleted_at to ${table}...`); pgm.addColumn(table, { deleted_at: { type: 'timestamptz', notNull: false, comment: 'Soft delete timestamp. NULL = not deleted.', }, }); // Create partial index for efficient filtering of non-deleted records pgm.createIndex(table, 'deleted_at', { name: `idx_${table}_deleted_at`, where: 'deleted_at IS NULL', }); }); // Create a helper function for soft delete operations pgm.sql(` -- Function to soft delete a record CREATE OR REPLACE FUNCTION soft_delete() RETURNS TRIGGER AS $$ BEGIN NEW.deleted_at = CURRENT_TIMESTAMP; RETURN NEW; END; $$ LANGUAGE plpgsql; -- Function to check if record is deleted CREATE OR REPLACE FUNCTION is_deleted(deleted_at TIMESTAMPTZ) RETURNS BOOLEAN AS $$ BEGIN RETURN deleted_at IS NOT NULL; END; $$ LANGUAGE plpgsql IMMUTABLE; -- Function to restore a soft-deleted record CREATE OR REPLACE FUNCTION restore_deleted() RETURNS TRIGGER AS $$ BEGIN NEW.deleted_at = NULL; RETURN NEW; END; $$ LANGUAGE plpgsql; `); console.log('Soft delete columns added to all tables successfully.'); }; /** * @param pgm {import('node-pg-migrate').MigrationBuilder} * @param run {() => void | undefined} * @returns {Promise | void} */ export const down = (pgm) => { console.log('Removing soft delete columns from all tables...'); // Drop helper functions pgm.sql(` DROP FUNCTION IF EXISTS soft_delete() CASCADE; DROP FUNCTION IF EXISTS is_deleted(TIMESTAMPTZ) CASCADE; DROP FUNCTION IF EXISTS restore_deleted() CASCADE; `); // Remove deleted_at from all tables in reverse order [...TABLES_WITH_SOFT_DELETE].reverse().forEach((table) => { console.log(` Removing deleted_at from ${table}...`); // Drop index first pgm.dropIndex(table, 'deleted_at', { name: `idx_${table}_deleted_at`, ifExists: true, }); // Drop column pgm.dropColumn(table, 'deleted_at', { ifExists: true, }); }); console.log('Soft delete columns removed from all tables.'); };