Refactor log_request_metadata function

This commit is contained in:
gpt-engineer-app[bot]
2025-11-03 20:58:52 +00:00
parent 50e560f7cd
commit 19b1451f32
11 changed files with 992 additions and 63 deletions

View File

@@ -191,3 +191,68 @@ export async function readItemChangeFields(
return acc;
}, {} as Record<string, { old_value: string | null; new_value: string | null }>);
}
/**
* Write profile change fields to relational table
* Replaces JSONB profile_audit_log.changes column
*/
export async function writeProfileChangeFields(
auditLogId: string,
changes: Record<string, { old_value?: unknown; new_value?: unknown }>
): Promise<void> {
if (!changes || Object.keys(changes).length === 0) return;
const entries = Object.entries(changes).map(([fieldName, change]) => ({
audit_log_id: auditLogId,
field_name: fieldName,
old_value: change.old_value !== undefined
? (typeof change.old_value === 'object' ? JSON.stringify(change.old_value) : String(change.old_value))
: null,
new_value: change.new_value !== undefined
? (typeof change.new_value === 'object' ? JSON.stringify(change.new_value) : String(change.new_value))
: null,
}));
const { error } = await supabase
.from('profile_change_fields')
.insert(entries);
if (error) {
logger.error('Failed to write profile change fields', { error, auditLogId });
throw error;
}
}
/**
* Write conflict detail fields to relational table
* Replaces JSONB conflict_resolutions.conflict_details column
*/
export async function writeConflictDetailFields(
conflictResolutionId: string,
conflictData: Record<string, unknown>
): Promise<void> {
if (!conflictData || Object.keys(conflictData).length === 0) return;
const entries = Object.entries(conflictData).map(([fieldName, value]) => ({
conflict_resolution_id: conflictResolutionId,
field_name: fieldName,
conflicting_value_1: typeof value === 'object' && value !== null && 'v1' in value
? String((value as any).v1)
: null,
conflicting_value_2: typeof value === 'object' && value !== null && 'v2' in value
? String((value as any).v2)
: null,
resolved_value: typeof value === 'object' && value !== null && 'resolved' in value
? String((value as any).resolved)
: null,
}));
const { error } = await supabase
.from('conflict_detail_fields')
.insert(entries);
if (error) {
logger.error('Failed to write conflict detail fields', { error, conflictResolutionId });
throw error;
}
}

View File

@@ -237,20 +237,36 @@ class NotificationService {
throw dbError;
}
// Create audit log entry
// DOCUMENTED EXCEPTION: profile_audit_log.changes column accepts JSONB
// We validate the preferences structure with Zod before this point
// Safe because the payload is constructed type-safely earlier in the function
await supabase.from('profile_audit_log').insert([{
user_id: userId,
changed_by: userId,
action: 'notification_preferences_updated',
changes: {
previous: previousPrefs || null,
updated: validated,
timestamp: new Date().toISOString()
}
}]);
// Create audit log entry using relational tables
const { data: auditLog, error: auditError } = await supabase
.from('profile_audit_log')
.insert([{
user_id: userId,
changed_by: userId,
action: 'notification_preferences_updated',
changes: {}, // Empty placeholder - actual changes stored in profile_change_fields table
}])
.select('id')
.single();
if (!auditError && auditLog) {
// Write changes to relational profile_change_fields table
const { writeProfileChangeFields } = await import('./auditHelpers');
await writeProfileChangeFields(auditLog.id, {
email_notifications: {
old_value: previousPrefs?.channel_preferences,
new_value: validated.channelPreferences,
},
workflow_preferences: {
old_value: previousPrefs?.workflow_preferences,
new_value: validated.workflowPreferences,
},
frequency_settings: {
old_value: previousPrefs?.frequency_settings,
new_value: validated.frequencySettings,
},
});
}
logger.info('Notification preferences updated', {
action: 'update_notification_preferences',

View File

@@ -0,0 +1,81 @@
/**
* Submission Metadata Service
* Handles reading/writing submission metadata to relational tables
* Replaces content_submissions.content JSONB column
*/
import { supabase } from '@/integrations/supabase/client';
import { logger } from './logger';
export interface SubmissionMetadataInsert {
submission_id: string;
metadata_key: string;
metadata_value: string;
value_type?: 'string' | 'number' | 'boolean' | 'date' | 'url' | 'json';
display_order?: number;
}
/**
* Write submission metadata to relational table
*/
export async function writeSubmissionMetadata(
submissionId: string,
metadata: Record<string, unknown>
): Promise<void> {
if (!metadata || Object.keys(metadata).length === 0) return;
const entries: SubmissionMetadataInsert[] = Object.entries(metadata).map(([key, value], index) => ({
submission_id: submissionId,
metadata_key: key,
metadata_value: typeof value === 'object' ? JSON.stringify(value) : String(value),
value_type: inferValueType(value),
display_order: index,
}));
const { error } = await supabase
.from('submission_metadata')
.insert(entries);
if (error) {
logger.error('Failed to write submission metadata', { error, submissionId });
throw error;
}
}
/**
* Read submission metadata from relational table
* Returns as key-value object for backward compatibility
*/
export async function readSubmissionMetadata(
submissionId: string
): Promise<Record<string, string>> {
const { data, error } = await supabase
.from('submission_metadata')
.select('metadata_key, metadata_value')
.eq('submission_id', submissionId)
.order('display_order');
if (error) {
logger.error('Failed to read submission metadata', { error, submissionId });
return {};
}
return data.reduce((acc, row) => {
acc[row.metadata_key] = row.metadata_value;
return acc;
}, {} as Record<string, string>);
}
/**
* Infer value type for metadata storage
*/
function inferValueType(value: unknown): 'string' | 'number' | 'boolean' | 'date' | 'url' | 'json' {
if (typeof value === 'number') return 'number';
if (typeof value === 'boolean') return 'boolean';
if (typeof value === 'object') return 'json';
if (typeof value === 'string') {
if (value.startsWith('http://') || value.startsWith('https://')) return 'url';
if (/^\d{4}-\d{2}-\d{2}/.test(value)) return 'date';
}
return 'string';
}