Refactor moderation queue query

This commit is contained in:
gpt-engineer-app[bot]
2025-10-29 13:09:45 +00:00
parent 7b6dfc4741
commit 3e0c4db0a1

View File

@@ -45,11 +45,13 @@ export interface FetchSubmissionsResult {
* *
* @param supabase - Supabase client instance * @param supabase - Supabase client instance
* @param config - Query configuration * @param config - Query configuration
* @param skipModeratorFilter - Skip the moderator access control filter
* @returns Configured Supabase query builder * @returns Configured Supabase query builder
*/ */
export function buildSubmissionQuery( export function buildSubmissionQuery(
supabase: SupabaseClient, supabase: SupabaseClient,
config: QueryConfig config: QueryConfig,
skipModeratorFilter = false
) { ) {
const { entityFilter, statusFilter, tab, userId, isAdmin, isSuperuser } = config; 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 // CRM-style claim filtering: moderators only see unclaimed OR self-assigned submissions
// Admins see all 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(); 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( 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'); countQuery = countQuery.neq('submission_type', 'photo');
} }
// Note: Count query not used for non-admin users (multi-query approach handles count)
if (!isAdmin && !isSuperuser) { if (!isAdmin && !isSuperuser) {
const now = new Date().toISOString(); const now = new Date().toISOString();
// Fixed: Add null check for locked_until to prevent 400 error
countQuery = countQuery.or( 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 config: QueryConfig
): Promise<FetchSubmissionsResult> { ): Promise<FetchSubmissionsResult> {
try { 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 countQuery = buildCountQuery(supabase, config);
const { count, error: countError } = await countQuery; const { count, error: countError } = await countQuery;
@@ -238,8 +248,8 @@ export async function fetchSubmissions(
// Build main query with pagination // Build main query with pagination
const query = buildSubmissionQuery(supabase, config); const query = buildSubmissionQuery(supabase, config);
const startIndex = (config.currentPage - 1) * config.pageSize; const startIndex = (currentPage - 1) * pageSize;
const endIndex = startIndex + config.pageSize - 1; const endIndex = startIndex + pageSize - 1;
const paginatedQuery = query.range(startIndex, endIndex); const paginatedQuery = query.range(startIndex, endIndex);
// Execute query // Execute query
@@ -249,19 +259,130 @@ export async function fetchSubmissions(
throw submissionsError; throw submissionsError;
} }
// Enrich submissions with type field for UI conditional logic // Enrich submissions with type field for UI conditional logic
const enrichedSubmissions = (submissions || []).map(sub => ({ const enrichedSubmissions = (submissions || []).map(sub => ({
...sub, ...sub,
type: 'content_submission' as const, type: 'content_submission' as const,
})); }));
return { return {
submissions: enrichedSubmissions, submissions: enrichedSubmissions,
totalCount: count || 0, totalCount: count || 0,
}; };
} catch (error: unknown) {
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) { } catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
// Use logger instead of console.error for consistent error tracking
return { return {
submissions: [], submissions: [],
totalCount: 0, totalCount: 0,
@@ -327,9 +448,10 @@ export async function getQueueStats(
.from('content_submissions') .from('content_submissions')
.select('status, escalated'); .select('status, escalated');
// Apply access control // Apply access control using simple OR filter
if (!isAdmin && !isSuperuser) { if (!isAdmin && !isSuperuser) {
const now = new Date().toISOString(); const now = new Date().toISOString();
// Show: unclaimed items OR items with expired locks OR my items
statsQuery = statsQuery.or( statsQuery = statsQuery.or(
`assigned_to.is.null,locked_until.lt.${now},assigned_to.eq.${userId}` `assigned_to.is.null,locked_until.lt.${now},assigned_to.eq.${userId}`
); );