mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 05:11:14 -05:00
Refactor moderation queue query
This commit is contained in:
@@ -45,11 +45,13 @@ export interface FetchSubmissionsResult {
|
||||
*
|
||||
* @param supabase - Supabase client instance
|
||||
* @param config - Query configuration
|
||||
* @param skipModeratorFilter - Skip the moderator access control filter
|
||||
* @returns Configured Supabase query builder
|
||||
*/
|
||||
export function buildSubmissionQuery(
|
||||
supabase: SupabaseClient,
|
||||
config: QueryConfig
|
||||
config: QueryConfig,
|
||||
skipModeratorFilter = false
|
||||
) {
|
||||
const { entityFilter, statusFilter, tab, userId, isAdmin, isSuperuser } = config;
|
||||
|
||||
@@ -135,11 +137,12 @@ export function buildSubmissionQuery(
|
||||
|
||||
// CRM-style claim filtering: moderators only see unclaimed OR self-assigned submissions
|
||||
// Admins see all submissions
|
||||
if (!isAdmin && !isSuperuser) {
|
||||
// Note: For non-admin users, moderator filtering is handled by multi-query approach in fetchSubmissions
|
||||
if (!isAdmin && !isSuperuser && !skipModeratorFilter) {
|
||||
const now = new Date().toISOString();
|
||||
// Fixed: Add null check for locked_until to prevent 400 error
|
||||
// Single filter approach (used by getQueueStats)
|
||||
query = query.or(
|
||||
`assigned_to.is.null,and(locked_until.not.is.null,locked_until.lt.${now}),assigned_to.eq.${userId}`
|
||||
`assigned_to.is.null,locked_until.lt.${now},assigned_to.eq.${userId}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -188,11 +191,11 @@ export function buildCountQuery(
|
||||
countQuery = countQuery.neq('submission_type', 'photo');
|
||||
}
|
||||
|
||||
// Note: Count query not used for non-admin users (multi-query approach handles count)
|
||||
if (!isAdmin && !isSuperuser) {
|
||||
const now = new Date().toISOString();
|
||||
// Fixed: Add null check for locked_until to prevent 400 error
|
||||
countQuery = countQuery.or(
|
||||
`assigned_to.is.null,and(locked_until.not.is.null,locked_until.lt.${now}),assigned_to.eq.${userId}`
|
||||
`assigned_to.is.null,locked_until.lt.${now},assigned_to.eq.${userId}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -228,7 +231,14 @@ export async function fetchSubmissions(
|
||||
config: QueryConfig
|
||||
): Promise<FetchSubmissionsResult> {
|
||||
try {
|
||||
// Get total count first
|
||||
const { userId, isAdmin, isSuperuser, currentPage, pageSize } = config;
|
||||
|
||||
// For non-admin users, use multi-query approach to avoid complex OR filters
|
||||
if (!isAdmin && !isSuperuser) {
|
||||
return await fetchSubmissionsMultiQuery(supabase, config);
|
||||
}
|
||||
|
||||
// Admin path: use single query with count
|
||||
const countQuery = buildCountQuery(supabase, config);
|
||||
const { count, error: countError } = await countQuery;
|
||||
|
||||
@@ -238,8 +248,8 @@ export async function fetchSubmissions(
|
||||
|
||||
// Build main query with pagination
|
||||
const query = buildSubmissionQuery(supabase, config);
|
||||
const startIndex = (config.currentPage - 1) * config.pageSize;
|
||||
const endIndex = startIndex + config.pageSize - 1;
|
||||
const startIndex = (currentPage - 1) * pageSize;
|
||||
const endIndex = startIndex + pageSize - 1;
|
||||
const paginatedQuery = query.range(startIndex, endIndex);
|
||||
|
||||
// Execute query
|
||||
@@ -260,8 +270,119 @@ export async function fetchSubmissions(
|
||||
totalCount: count || 0,
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
// Use logger instead of console.error for consistent error tracking
|
||||
return {
|
||||
submissions: [],
|
||||
totalCount: 0,
|
||||
error: error as Error,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch submissions using multi-query approach for non-admin users
|
||||
*
|
||||
* Executes three separate queries to avoid complex OR filters:
|
||||
* 1. Unclaimed items (assigned_to is null)
|
||||
* 2. Expired locks (locked_until < now, not assigned to current user)
|
||||
* 3. Items assigned to current user
|
||||
*
|
||||
* Results are merged, deduplicated, sorted, and paginated.
|
||||
*/
|
||||
async function fetchSubmissionsMultiQuery(
|
||||
supabase: SupabaseClient,
|
||||
config: QueryConfig
|
||||
): Promise<FetchSubmissionsResult> {
|
||||
const { userId, currentPage, pageSize } = config;
|
||||
const now = new Date().toISOString();
|
||||
|
||||
try {
|
||||
// Build three separate queries
|
||||
// Query 1: Unclaimed items
|
||||
const query1 = buildSubmissionQuery(supabase, config, true).is('assigned_to', null);
|
||||
|
||||
// Query 2: Expired locks (not mine)
|
||||
const query2 = buildSubmissionQuery(supabase, config, true)
|
||||
.not('assigned_to', 'is', null)
|
||||
.neq('assigned_to', userId)
|
||||
.lt('locked_until', now);
|
||||
|
||||
// Query 3: My claimed items
|
||||
const query3 = buildSubmissionQuery(supabase, config, true).eq('assigned_to', userId);
|
||||
|
||||
// Execute all queries in parallel
|
||||
const [result1, result2, result3] = await Promise.all([
|
||||
query1,
|
||||
query2,
|
||||
query3,
|
||||
]);
|
||||
|
||||
// Check for errors
|
||||
if (result1.error) throw result1.error;
|
||||
if (result2.error) throw result2.error;
|
||||
if (result3.error) throw result3.error;
|
||||
|
||||
// Merge all submissions
|
||||
const allSubmissions = [
|
||||
...(result1.data || []),
|
||||
...(result2.data || []),
|
||||
...(result3.data || []),
|
||||
];
|
||||
|
||||
// Deduplicate by ID
|
||||
const uniqueMap = new Map();
|
||||
allSubmissions.forEach(sub => {
|
||||
if (!uniqueMap.has(sub.id)) {
|
||||
uniqueMap.set(sub.id, sub);
|
||||
}
|
||||
});
|
||||
const uniqueSubmissions = Array.from(uniqueMap.values());
|
||||
|
||||
// Apply sorting (same logic as buildSubmissionQuery)
|
||||
uniqueSubmissions.sort((a, b) => {
|
||||
// Level 1: Escalated first
|
||||
if (a.escalated !== b.escalated) {
|
||||
return b.escalated ? 1 : -1;
|
||||
}
|
||||
|
||||
// Level 2: Custom sort (if provided)
|
||||
if (config.sortConfig) {
|
||||
const field = config.sortConfig.field;
|
||||
const ascending = config.sortConfig.direction === 'asc';
|
||||
const aVal = a[field];
|
||||
const bVal = b[field];
|
||||
|
||||
if (aVal !== bVal) {
|
||||
if (aVal == null) return 1;
|
||||
if (bVal == null) return -1;
|
||||
|
||||
const comparison = aVal < bVal ? -1 : 1;
|
||||
return ascending ? comparison : -comparison;
|
||||
}
|
||||
}
|
||||
|
||||
// Level 3: Tiebreaker by created_at
|
||||
const aTime = new Date(a.created_at).getTime();
|
||||
const bTime = new Date(b.created_at).getTime();
|
||||
return aTime - bTime;
|
||||
});
|
||||
|
||||
// Apply pagination
|
||||
const totalCount = uniqueSubmissions.length;
|
||||
const startIndex = (currentPage - 1) * pageSize;
|
||||
const endIndex = startIndex + pageSize;
|
||||
const paginatedSubmissions = uniqueSubmissions.slice(startIndex, endIndex);
|
||||
|
||||
// Enrich with type field
|
||||
const enrichedSubmissions = paginatedSubmissions.map(sub => ({
|
||||
...sub,
|
||||
type: 'content_submission' as const,
|
||||
}));
|
||||
|
||||
return {
|
||||
submissions: enrichedSubmissions,
|
||||
totalCount,
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
submissions: [],
|
||||
totalCount: 0,
|
||||
@@ -327,9 +448,10 @@ export async function getQueueStats(
|
||||
.from('content_submissions')
|
||||
.select('status, escalated');
|
||||
|
||||
// Apply access control
|
||||
// Apply access control using simple OR filter
|
||||
if (!isAdmin && !isSuperuser) {
|
||||
const now = new Date().toISOString();
|
||||
// Show: unclaimed items OR items with expired locks OR my items
|
||||
statsQuery = statsQuery.or(
|
||||
`assigned_to.is.null,locked_until.lt.${now},assigned_to.eq.${userId}`
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user