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 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}`
);