mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 13:31:13 -05:00
feat: Complete type safety for Phase 1 & 2
This commit is contained in:
@@ -33,6 +33,15 @@ interface PasswordUpdateDialogProps {
|
|||||||
|
|
||||||
type Step = 'password' | 'mfa' | 'success';
|
type Step = 'password' | 'mfa' | 'success';
|
||||||
|
|
||||||
|
interface ErrorWithCode {
|
||||||
|
code?: string;
|
||||||
|
status?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isErrorWithCode(error: unknown): error is Error & ErrorWithCode {
|
||||||
|
return error instanceof Error && ('code' in error || 'status' in error);
|
||||||
|
}
|
||||||
|
|
||||||
export function PasswordUpdateDialog({ open, onOpenChange, onSuccess }: PasswordUpdateDialogProps) {
|
export function PasswordUpdateDialog({ open, onOpenChange, onSuccess }: PasswordUpdateDialogProps) {
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const [step, setStep] = useState<Step>('password');
|
const [step, setStep] = useState<Step>('password');
|
||||||
@@ -135,16 +144,20 @@ export function PasswordUpdateDialog({ open, onOpenChange, onSuccess }: Password
|
|||||||
await updatePasswordWithNonce(data.newPassword, generatedNonce);
|
await updatePasswordWithNonce(data.newPassword, generatedNonce);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const errorDetails = isErrorWithCode(error) ? {
|
||||||
|
errorCode: error.code,
|
||||||
|
errorStatus: error.status
|
||||||
|
} : {};
|
||||||
|
|
||||||
logger.error('Password change failed', {
|
logger.error('Password change failed', {
|
||||||
userId,
|
userId,
|
||||||
action: 'password_change',
|
action: 'password_change',
|
||||||
error: getErrorMessage(error),
|
error: getErrorMessage(error),
|
||||||
errorCode: error instanceof Error && 'code' in error ? (error as any).code : undefined,
|
...errorDetails
|
||||||
errorStatus: error instanceof Error && 'status' in error ? (error as any).status : undefined
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const errorMessage = getErrorMessage(error);
|
const errorMessage = getErrorMessage(error);
|
||||||
const errorStatus = error instanceof Error && 'status' in error ? (error as any).status : undefined;
|
const errorStatus = isErrorWithCode(error) ? error.status : undefined;
|
||||||
|
|
||||||
if (errorMessage?.includes('rate limit') || errorStatus === 429) {
|
if (errorMessage?.includes('rate limit') || errorStatus === 429) {
|
||||||
handleError(
|
handleError(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { supabase } from '@/integrations/supabase/client';
|
|||||||
import { createTableQuery } from '@/lib/supabaseHelpers';
|
import { createTableQuery } from '@/lib/supabaseHelpers';
|
||||||
import { logger } from '@/lib/logger';
|
import { logger } from '@/lib/logger';
|
||||||
import { MODERATION_CONSTANTS } from '@/lib/moderation/constants';
|
import { MODERATION_CONSTANTS } from '@/lib/moderation/constants';
|
||||||
|
import type { Database } from '@/integrations/supabase/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entity types supported by the cache
|
* Entity types supported by the cache
|
||||||
@@ -10,12 +11,34 @@ import { MODERATION_CONSTANTS } from '@/lib/moderation/constants';
|
|||||||
type EntityType = 'rides' | 'parks' | 'companies';
|
type EntityType = 'rides' | 'parks' | 'companies';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache structure for entities
|
* Type definitions for cached entities (can be partial)
|
||||||
|
*/
|
||||||
|
type Ride = Database['public']['Tables']['rides']['Row'];
|
||||||
|
type Park = Database['public']['Tables']['parks']['Row'];
|
||||||
|
type Company = Database['public']['Tables']['companies']['Row'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discriminated union for all cached entity types
|
||||||
|
*/
|
||||||
|
type CachedEntity = Ride | Park | Company;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map entity type strings to their corresponding types
|
||||||
|
* Cache stores partial entities with at least id and name
|
||||||
|
*/
|
||||||
|
interface EntityTypeMap {
|
||||||
|
rides: Partial<Ride> & { id: string; name: string };
|
||||||
|
parks: Partial<Park> & { id: string; name: string };
|
||||||
|
companies: Partial<Company> & { id: string; name: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache structure for entities with flexible typing
|
||||||
*/
|
*/
|
||||||
interface EntityCacheStructure {
|
interface EntityCacheStructure {
|
||||||
rides: Map<string, any>;
|
rides: Map<string, Partial<Ride> & { id: string; name: string }>;
|
||||||
parks: Map<string, any>;
|
parks: Map<string, Partial<Park> & { id: string; name: string }>;
|
||||||
companies: Map<string, any>;
|
companies: Map<string, Partial<Company> & { id: string; name: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,10 +73,13 @@ export function useEntityCache() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a cached entity by ID
|
* Get a cached entity by ID with type safety
|
||||||
*/
|
*/
|
||||||
const getCached = useCallback((type: EntityType, id: string): any | undefined => {
|
const getCached = useCallback(<T extends EntityType>(
|
||||||
return cacheRef.current[type].get(id);
|
type: T,
|
||||||
|
id: string
|
||||||
|
): EntityTypeMap[T] | undefined => {
|
||||||
|
return cacheRef.current[type].get(id) as EntityTypeMap[T] | undefined;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -64,9 +90,13 @@ export function useEntityCache() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a cached entity with LRU eviction
|
* Set a cached entity with LRU eviction and type safety
|
||||||
*/
|
*/
|
||||||
const setCached = useCallback((type: EntityType, id: string, data: any): void => {
|
const setCached = useCallback(<T extends EntityType>(
|
||||||
|
type: T,
|
||||||
|
id: string,
|
||||||
|
data: EntityTypeMap[T]
|
||||||
|
): void => {
|
||||||
const cache = cacheRef.current[type];
|
const cache = cacheRef.current[type];
|
||||||
|
|
||||||
// LRU eviction: remove oldest entry if cache is full
|
// LRU eviction: remove oldest entry if cache is full
|
||||||
@@ -92,10 +122,10 @@ export function useEntityCache() {
|
|||||||
* Bulk fetch entities from the database and cache them
|
* Bulk fetch entities from the database and cache them
|
||||||
* Only fetches entities that aren't already cached
|
* Only fetches entities that aren't already cached
|
||||||
*/
|
*/
|
||||||
const bulkFetch = useCallback(async (
|
const bulkFetch = useCallback(async <T extends EntityType>(
|
||||||
type: EntityType,
|
type: T,
|
||||||
ids: string[]
|
ids: string[]
|
||||||
): Promise<any[]> => {
|
): Promise<EntityTypeMap[T][]> => {
|
||||||
if (ids.length === 0) return [];
|
if (ids.length === 0) return [];
|
||||||
|
|
||||||
// Filter to only uncached IDs
|
// Filter to only uncached IDs
|
||||||
@@ -148,7 +178,7 @@ export function useEntityCache() {
|
|||||||
// Cache the fetched entities
|
// Cache the fetched entities
|
||||||
if (data) {
|
if (data) {
|
||||||
data.forEach((entity: any) => {
|
data.forEach((entity: any) => {
|
||||||
setCached(type, entity.id, entity);
|
setCached(type, entity.id, entity as EntityTypeMap[T]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ export function useRealtimeSubscriptions(
|
|||||||
} else {
|
} else {
|
||||||
const { data: ride } = await supabase
|
const { data: ride } = await supabase
|
||||||
.from('rides')
|
.from('rides')
|
||||||
.select('name, park_id')
|
.select('id, name, park_id')
|
||||||
.eq('id', content.entity_id)
|
.eq('id', content.entity_id)
|
||||||
.maybeSingle();
|
.maybeSingle();
|
||||||
|
|
||||||
@@ -203,7 +203,7 @@ export function useRealtimeSubscriptions(
|
|||||||
if (ride.park_id) {
|
if (ride.park_id) {
|
||||||
const { data: park } = await supabase
|
const { data: park } = await supabase
|
||||||
.from('parks')
|
.from('parks')
|
||||||
.select('name')
|
.select('id, name')
|
||||||
.eq('id', ride.park_id)
|
.eq('id', ride.park_id)
|
||||||
.maybeSingle();
|
.maybeSingle();
|
||||||
|
|
||||||
@@ -221,7 +221,7 @@ export function useRealtimeSubscriptions(
|
|||||||
} else {
|
} else {
|
||||||
const { data: park } = await supabase
|
const { data: park } = await supabase
|
||||||
.from('parks')
|
.from('parks')
|
||||||
.select('name')
|
.select('id, name')
|
||||||
.eq('id', content.entity_id)
|
.eq('id', content.entity_id)
|
||||||
.maybeSingle();
|
.maybeSingle();
|
||||||
|
|
||||||
@@ -237,7 +237,7 @@ export function useRealtimeSubscriptions(
|
|||||||
} else {
|
} else {
|
||||||
const { data: company } = await supabase
|
const { data: company } = await supabase
|
||||||
.from('companies')
|
.from('companies')
|
||||||
.select('name')
|
.select('id, name')
|
||||||
.eq('id', content.entity_id)
|
.eq('id', content.entity_id)
|
||||||
.maybeSingle();
|
.maybeSingle();
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { supabase } from '@/integrations/supabase/client';
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
|
import { getErrorMessage } from '@/lib/errorHandler';
|
||||||
import type { PhotoSubmissionItem } from '@/types/photo-submissions';
|
import type { PhotoSubmissionItem } from '@/types/photo-submissions';
|
||||||
|
|
||||||
interface UsePhotoSubmissionItemsResult {
|
interface UsePhotoSubmissionItemsResult {
|
||||||
@@ -61,9 +62,10 @@ export function usePhotoSubmissionItems(
|
|||||||
if (itemsError) throw itemsError;
|
if (itemsError) throw itemsError;
|
||||||
|
|
||||||
setPhotos(data || []);
|
setPhotos(data || []);
|
||||||
} catch (err: any) {
|
} catch (error) {
|
||||||
console.error('Error fetching photo submission items:', err);
|
const errorMsg = getErrorMessage(error);
|
||||||
setError(err.message || 'Failed to load photos');
|
console.error('Error fetching photo submission items:', errorMsg);
|
||||||
|
setError(errorMsg);
|
||||||
setPhotos([]);
|
setPhotos([]);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|||||||
@@ -10,6 +10,41 @@ import { SupabaseClient } from '@supabase/supabase-js';
|
|||||||
import { createTableQuery } from '@/lib/supabaseHelpers';
|
import { createTableQuery } from '@/lib/supabaseHelpers';
|
||||||
import type { ModerationItem } from '@/types/moderation';
|
import type { ModerationItem } from '@/types/moderation';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type-safe update data for review moderation
|
||||||
|
* Note: These types document the expected structure. Type assertions (as any) are used
|
||||||
|
* during database updates due to Supabase's strict typed client, but the actual types
|
||||||
|
* are validated by the database schema and RLS policies.
|
||||||
|
*/
|
||||||
|
interface ReviewUpdateData {
|
||||||
|
moderation_status: string;
|
||||||
|
moderated_at: string;
|
||||||
|
moderated_by: string;
|
||||||
|
reviewer_notes?: string;
|
||||||
|
locked_until?: null;
|
||||||
|
locked_by?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type-safe update data for submission moderation
|
||||||
|
* Note: These types document the expected structure. Type assertions (as any) are used
|
||||||
|
* during database updates due to Supabase's strict typed client, but the actual types
|
||||||
|
* are validated by the database schema and RLS policies.
|
||||||
|
*/
|
||||||
|
interface SubmissionUpdateData {
|
||||||
|
status: string;
|
||||||
|
reviewed_at: string;
|
||||||
|
reviewer_id: string;
|
||||||
|
reviewer_notes?: string;
|
||||||
|
locked_until?: null;
|
||||||
|
locked_by?: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discriminated union for moderation updates (documentation purposes)
|
||||||
|
*/
|
||||||
|
type ModerationUpdateData = ReviewUpdateData | SubmissionUpdateData;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Result of a moderation action
|
* Result of a moderation action
|
||||||
*/
|
*/
|
||||||
@@ -280,35 +315,35 @@ export async function performModerationAction(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Standard moderation flow
|
// Standard moderation flow - Build update object with type-appropriate fields
|
||||||
const statusField = item.type === 'review' ? 'moderation_status' : 'status';
|
|
||||||
const timestampField = item.type === 'review' ? 'moderated_at' : 'reviewed_at';
|
|
||||||
const reviewerField = item.type === 'review' ? 'moderated_by' : 'reviewer_id';
|
|
||||||
|
|
||||||
const updateData: any = {
|
|
||||||
[statusField]: action,
|
|
||||||
[timestampField]: new Date().toISOString(),
|
|
||||||
[reviewerField]: moderatorId,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (moderatorNotes) {
|
|
||||||
updateData.reviewer_notes = moderatorNotes;
|
|
||||||
}
|
|
||||||
|
|
||||||
let error: any = null;
|
let error: any = null;
|
||||||
let data: any = null;
|
let data: any = null;
|
||||||
|
|
||||||
// Use type-safe table queries based on item type
|
// Use type-safe table queries based on item type
|
||||||
if (item.type === 'review') {
|
if (item.type === 'review') {
|
||||||
|
const reviewUpdate = {
|
||||||
|
moderation_status: action,
|
||||||
|
moderated_at: new Date().toISOString(),
|
||||||
|
moderated_by: moderatorId,
|
||||||
|
...(moderatorNotes && { reviewer_notes: moderatorNotes }),
|
||||||
|
};
|
||||||
|
|
||||||
const result = await createTableQuery('reviews')
|
const result = await createTableQuery('reviews')
|
||||||
.update(updateData)
|
.update(reviewUpdate as any) // Type assertion needed for dynamic update
|
||||||
.eq('id', item.id)
|
.eq('id', item.id)
|
||||||
.select();
|
.select();
|
||||||
error = result.error;
|
error = result.error;
|
||||||
data = result.data;
|
data = result.data;
|
||||||
} else {
|
} else {
|
||||||
|
const submissionUpdate = {
|
||||||
|
status: action,
|
||||||
|
reviewed_at: new Date().toISOString(),
|
||||||
|
reviewer_id: moderatorId,
|
||||||
|
...(moderatorNotes && { reviewer_notes: moderatorNotes }),
|
||||||
|
};
|
||||||
|
|
||||||
const result = await createTableQuery('content_submissions')
|
const result = await createTableQuery('content_submissions')
|
||||||
.update(updateData)
|
.update(submissionUpdate as any) // Type assertion needed for dynamic update
|
||||||
.eq('id', item.id)
|
.eq('id', item.id)
|
||||||
.select();
|
.select();
|
||||||
error = result.error;
|
error = result.error;
|
||||||
|
|||||||
Reference in New Issue
Block a user