Add shared update guard and reliable site deletion count

This commit is contained in:
MrPiglr 2025-12-22 18:28:49 -07:00
parent cf620a3e24
commit cd39dc06c5

View file

@ -1,4 +1,12 @@
import { type Profile, type Project, type ChatMessage } from "../shared/schema.js"; import {
type Profile,
type Project,
type ChatMessage,
type AethexSite,
type Application,
type Achievement,
type AethexAlert,
} from "../shared/schema.js";
import { supabase } from "./supabase.js"; import { supabase } from "./supabase.js";
export interface IStorage { export interface IStorage {
@ -14,21 +22,24 @@ export interface IStorage {
getProject(id: string): Promise<Project | undefined>; getProject(id: string): Promise<Project | undefined>;
// Sites // Sites
getSites(): Promise<any[]>; getSites(): Promise<AethexSite[]>;
createSite(site: Partial<AethexSite>): Promise<AethexSite>;
updateSite(id: string, updates: Partial<AethexSite>): Promise<AethexSite>;
deleteSite(id: string): Promise<boolean>;
// Auth Logs // Auth Logs
getAuthLogs(): Promise<any[]>; getAuthLogs(): Promise<any[]>;
// Achievements // Achievements
getAchievements(): Promise<any[]>; getAchievements(): Promise<Achievement[]>;
// Applications // Applications
getApplications(): Promise<any[]>; getApplications(): Promise<Application[]>;
updateApplication(id: string, updates: any): Promise<any>; updateApplication(id: string, updates: Partial<Application>): Promise<Application>;
// Alerts // Alerts
getAlerts(): Promise<any[]>; getAlerts(): Promise<AethexAlert[]>;
updateAlert(id: string, updates: any): Promise<any>; updateAlert(id: string, updates: Partial<AethexAlert>): Promise<AethexAlert>;
// Notifications (for WebSocket) // Notifications (for WebSocket)
getNotifications(): Promise<any[]>; getNotifications(): Promise<any[]>;
@ -50,38 +61,54 @@ export interface IStorage {
} }
export class SupabaseStorage implements IStorage { export class SupabaseStorage implements IStorage {
// Create a new site private filterDefined<T extends Record<string, any>>(updates: Partial<T>): Partial<T> {
async createSite(site: any): Promise<any> { return Object.fromEntries(
const { data, error } = await supabase Object.entries(updates).filter(([, value]) => value !== undefined)
.from('aethex_sites') ) as Partial<T>;
.insert(site) }
.select()
.single();
if (error) throw new Error(error.message);
return data;
}
// Update a site private ensureUpdates<T extends Record<string, any>>(updates: Partial<T>, entity: string): asserts updates is Partial<T> & Record<string, any> {
async updateSite(id: string, updates: any): Promise<any> { if (Object.keys(updates).length === 0) {
const { data, error } = await supabase throw new Error(`No ${entity} fields provided for update`);
.from('aethex_sites')
.update({ ...updates, updated_at: new Date().toISOString() })
.eq('id', id)
.select()
.single();
if (error) throw new Error(error.message);
return data;
} }
}
// Delete a site // Create a new site
async deleteSite(id: string): Promise<boolean> { async createSite(site: Partial<AethexSite>): Promise<AethexSite> {
const { error, count } = await supabase const cleanSite = this.filterDefined<AethexSite>(site);
.from('aethex_sites') const { data, error } = await supabase
.delete({ count: 'exact' }) .from('aethex_sites')
.eq('id', id); .insert(cleanSite)
if (error) throw new Error(error.message); .select()
return (count ?? 0) > 0; .single();
} if (error) throw new Error(error.message);
return data as AethexSite;
}
// Update a site
async updateSite(id: string, updates: Partial<AethexSite>): Promise<AethexSite> {
const cleanUpdates = this.filterDefined<AethexSite>(updates);
this.ensureUpdates(cleanUpdates, 'site');
const { data, error } = await supabase
.from('aethex_sites')
.update({ ...cleanUpdates, updated_at: new Date().toISOString() })
.eq('id', id)
.select()
.single();
if (error) throw new Error(error.message);
return data as AethexSite;
}
// Delete a site
async deleteSite(id: string): Promise<boolean> {
const { error, data } = await supabase
.from('aethex_sites')
.delete()
.eq('id', id)
.select('id');
if (error) throw new Error(error.message);
return (data?.length ?? 0) > 0;
}
async getProfiles(): Promise<Profile[]> { async getProfiles(): Promise<Profile[]> {
const { data, error } = await supabase const { data, error } = await supabase
@ -116,9 +143,11 @@ export class SupabaseStorage implements IStorage {
} }
async updateProfile(id: string, updates: Partial<Profile>): Promise<Profile | undefined> { async updateProfile(id: string, updates: Partial<Profile>): Promise<Profile | undefined> {
const cleanUpdates = this.filterDefined<Profile>(updates);
this.ensureUpdates(cleanUpdates, 'profile');
const { data, error } = await supabase const { data, error } = await supabase
.from('profiles') .from('profiles')
.update({ ...updates, updated_at: new Date().toISOString() }) .update({ ...cleanUpdates, updated_at: new Date().toISOString() })
.eq('id', id) .eq('id', id)
.select() .select()
.single(); .single();
@ -127,6 +156,16 @@ export class SupabaseStorage implements IStorage {
return data as Profile; return data as Profile;
} }
async getLeadershipProfiles(): Promise<Profile[]> {
const { data, error } = await supabase
.from('profiles')
.select('*')
.in('role', ['oversee', 'admin']);
if (error || !data) return [];
return data as Profile[];
}
async getProjects(): Promise<Project[]> { async getProjects(): Promise<Project[]> {
const { data, error } = await supabase const { data, error } = await supabase
.from('projects') .from('projects')
@ -148,14 +187,14 @@ export class SupabaseStorage implements IStorage {
return data as Project; return data as Project;
} }
async getSites(): Promise<any[]> { async getSites(): Promise<AethexSite[]> {
const { data, error } = await supabase const { data, error } = await supabase
.from('aethex_sites') .from('aethex_sites')
.select('*') .select('*')
.order('last_check', { ascending: false }); .order('last_check', { ascending: false });
if (error) return []; if (error || !data) return [];
return data || []; return data as AethexSite[];
} }
async getAuthLogs(): Promise<any[]> { async getAuthLogs(): Promise<any[]> {
@ -169,42 +208,45 @@ export class SupabaseStorage implements IStorage {
return data || []; return data || [];
} }
async getAchievements(): Promise<any[]> { async getAchievements(): Promise<Achievement[]> {
const { data, error } = await supabase const { data, error } = await supabase
.from('achievements') .from('achievements')
.select('*') .select('*')
.order('name', { ascending: true }); .order('name', { ascending: true });
if (error) return []; if (error || !data) return [];
return data || []; return data as Achievement[];
} }
async getApplications(): Promise<any[]> { async getApplications(): Promise<Application[]> {
const { data, error } = await supabase const { data, error } = await supabase
.from('applications') .from('applications')
.select('*') .select('*')
.order('submitted_at', { ascending: false }); .order('submitted_at', { ascending: false });
if (error) return []; if (error || !data) return [];
return data || []; return data as Application[];
} }
async getAlerts(): Promise<any[]> { async getAlerts(): Promise<AethexAlert[]> {
const { data, error } = await supabase const { data, error } = await supabase
.from('aethex_alerts') .from('aethex_alerts')
.select('*') .select('*')
.order('created_at', { ascending: false }) .order('created_at', { ascending: false })
.limit(50); .limit(50);
if (error) return []; if (error || !data) return [];
return data || []; return data as AethexAlert[];
} }
async updateAlert(id: string, updates: any): Promise<any> { async updateAlert(id: string, updates: Partial<AethexAlert>): Promise<AethexAlert> {
const updateData: any = {}; const updateData = this.filterDefined<AethexAlert>({
if ('is_resolved' in updates) { message: updates.message,
updateData.is_resolved = updates.is_resolved; severity: updates.severity,
} is_resolved: updates.is_resolved,
resolved_at: updates.resolved_at,
});
this.ensureUpdates(updateData, 'alert');
const { data, error } = await supabase const { data, error } = await supabase
.from('aethex_alerts') .from('aethex_alerts')
@ -217,14 +259,15 @@ export class SupabaseStorage implements IStorage {
console.error('Update alert error:', error); console.error('Update alert error:', error);
throw new Error(error.message); throw new Error(error.message);
} }
return data; return data as AethexAlert;
} }
async updateApplication(id: string, updates: any): Promise<any> { async updateApplication(id: string, updates: Partial<Application>): Promise<Application> {
const updateData: any = {}; const updateData = this.filterDefined<Application>({
if ('status' in updates) { status: updates.status,
updateData.status = updates.status; response_message: updates.response_message,
} });
this.ensureUpdates(updateData, 'application');
const { data, error } = await supabase const { data, error } = await supabase
.from('applications') .from('applications')
@ -237,7 +280,7 @@ export class SupabaseStorage implements IStorage {
console.error('Update application error:', error); console.error('Update application error:', error);
throw new Error(error.message); throw new Error(error.message);
} }
return data; return data as Application;
} }
// Chat Messages (AI memory) // Chat Messages (AI memory)