Files
thrilltrack-explorer/src/hooks/moderation/useQueueQuery.ts
gpt-engineer-app[bot] b22546e7f2 Add audit trail and filters
Implements audit trail view for item approvals, adds approval date range filtering to moderation queue, and wires up UI and backend components (Approval History page, ItemApprovalHistory component, materialized view-based history, and query/filters integration) to support compliant reporting and time-based moderation filtering.
2025-11-12 14:06:34 +00:00

235 lines
6.6 KiB
TypeScript

/**
* TanStack Query hook for moderation queue data fetching
*
* Wraps the existing fetchSubmissions query builder with React Query
* to provide automatic caching, deduplication, and background refetching.
*/
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { fetchSubmissions, type QueryConfig } from '@/lib/moderation/queries';
import { supabase } from '@/lib/supabaseClient';
import { logger } from '@/lib/logger';
import { getErrorMessage } from '@/lib/errorHandler';
import { MODERATION_CONSTANTS } from '@/lib/moderation/constants';
import { validateModerationItems } from '@/lib/moderation/validation';
import type {
ModerationItem,
EntityFilter,
StatusFilter,
QueueTab,
SortField,
SortDirection
} from '@/types/moderation';
/**
* Get specific, actionable error message based on error type
*/
function getSpecificErrorMessage(error: unknown): string {
// Offline detection
if (!navigator.onLine) {
return 'You appear to be offline. Check your internet connection and try again.';
}
// Timeout
if (error instanceof Error && error.name === 'AbortError') {
return 'Request timed out. The server is taking too long to respond. Please try again.';
}
// Check for Supabase-specific errors
if (typeof error === 'object' && error !== null) {
const err = error as any;
// 500 errors
if (err.status === 500 || err.code === '500') {
return 'Server error occurred. Our team has been notified. Please try again in a few minutes.';
}
// 429 Rate limiting
if (err.status === 429 || err.message?.includes('rate limit')) {
return 'Too many requests. Please wait a moment before trying again.';
}
// Authentication errors
if (err.status === 401 || err.message?.includes('JWT')) {
return 'Your session has expired. Please refresh the page and sign in again.';
}
// Permission errors
if (err.status === 403 || err.message?.includes('permission')) {
return 'You do not have permission to access the moderation queue.';
}
}
// Fallback
return getErrorMessage(error) || 'Failed to load moderation queue. Please try again.';
}
/**
* Configuration for queue query
*/
export interface UseQueueQueryConfig {
/** User making the query */
userId: string | undefined;
/** Whether user is admin */
isAdmin: boolean;
/** Whether user is superuser */
isSuperuser: boolean;
/** Entity filter */
entityFilter: EntityFilter;
/** Status filter */
statusFilter: StatusFilter;
/** Active tab */
tab: QueueTab;
/** Current page */
currentPage: number;
/** Page size */
pageSize: number;
/** Sort configuration */
sortConfig: {
field: SortField;
direction: SortDirection;
};
/** Approval date range filter */
approvalDateRange?: {
from: Date | null;
to: Date | null;
};
/** Whether query is enabled (defaults to true) */
enabled?: boolean;
}
/**
* Return type for useQueueQuery
*/
export interface UseQueueQueryReturn {
/** Queue items */
items: ModerationItem[];
/** Total count of items matching filters */
totalCount: number;
/** Initial loading state (no data yet) */
isLoading: boolean;
/** Background refresh in progress (has data already) */
isRefreshing: boolean;
/** Any error that occurred */
error: Error | null;
/** Manually trigger a refetch */
refetch: () => Promise<any>;
/** Invalidate this query (triggers background refetch) */
invalidate: () => Promise<void>;
}
/**
* Hook to fetch moderation queue data using TanStack Query
*/
export function useQueueQuery(config: UseQueueQueryConfig): UseQueueQueryReturn {
const queryClient = useQueryClient();
// Build query config for fetchSubmissions
const queryConfig: QueryConfig = {
userId: config.userId || '',
isAdmin: config.isAdmin,
isSuperuser: config.isSuperuser,
entityFilter: config.entityFilter,
statusFilter: config.statusFilter,
tab: config.tab,
currentPage: config.currentPage,
pageSize: config.pageSize,
sortConfig: config.sortConfig,
approvalDateRange: config.approvalDateRange,
};
// Create stable query key (TanStack Query uses this for caching/deduplication)
// Include user context to ensure proper cache isolation per user/role
const queryKey = [
'moderation-queue',
config.userId,
config.isAdmin,
config.isSuperuser,
config.entityFilter,
config.statusFilter,
config.tab,
config.currentPage,
config.pageSize,
config.sortConfig.field,
config.sortConfig.direction,
config.approvalDateRange?.from?.toISOString(),
config.approvalDateRange?.to?.toISOString(),
];
// Execute query
const query = useQuery({
queryKey,
queryFn: async () => {
logger.log('🔍 [TanStack Query] Fetching queue data:', queryKey);
// Create timeout controller (30s timeout)
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000);
try {
const result = await fetchSubmissions(supabase, queryConfig);
clearTimeout(timeoutId);
if (result.error) {
const specificMessage = getSpecificErrorMessage(result.error);
// Error already captured in context
throw new Error(specificMessage);
}
// Validate data shape before returning
const validation = validateModerationItems(result.submissions);
if (!validation.success) {
// Invalid data shape
throw new Error(validation.error || 'Invalid data format');
}
logger.log('✅ [TanStack Query] Fetched', validation.data!.length, 'items');
return { ...result, submissions: validation.data! };
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
},
enabled: config.enabled !== false && !!config.userId,
staleTime: MODERATION_CONSTANTS.QUERY_STALE_TIME,
gcTime: MODERATION_CONSTANTS.QUERY_GC_TIME,
retry: MODERATION_CONSTANTS.QUERY_RETRY_COUNT,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
networkMode: 'offlineFirst', // Handle offline gracefully
meta: {
errorMessage: 'Failed to load moderation queue',
},
});
// Invalidate helper
const invalidate = async () => {
await queryClient.invalidateQueries({ queryKey: ['moderation-queue'] });
};
return {
items: query.data?.submissions || [],
totalCount: query.data?.totalCount || 0,
isLoading: query.isLoading,
isRefreshing: query.isFetching && !query.isLoading,
error: query.error as Error | null,
refetch: query.refetch,
invalidate,
};
}