'use server' import { prisma } from './db' import { revalidatePath } from 'next/cache' import { auth } from '@/auth' import { validateListTitle, validateItemTitle, validateColor, validateDateString, validateTags, sanitizeString, } from './validation' // Retry utility for database operations async function retryOperation( operation: () => Promise, maxRetries: number = 3, delayMs: number = 1000 ): Promise { let lastError: Error | undefined for (let attempt = 0; attempt < maxRetries; attempt++) { try { return await operation() } catch (error) { lastError = error as Error // Don't retry on validation errors or not found errors if ( error instanceof Error && (error.message.includes('Unique constraint') || error.message.includes('Record to update not found') || error.message.includes('Record to delete does not exist')) ) { throw error } // Wait before retrying (exponential backoff) if (attempt < maxRetries - 1) { await new Promise(resolve => setTimeout(resolve, delayMs * Math.pow(2, attempt))) } } } throw lastError } export async function getLists() { try { // Get current user session const session = await auth() if (!session?.user?.id) { throw new Error('Unauthorized: Please log in to view lists') } const userId = session.user.id // Get lists where user is owner or collaborator return await retryOperation(() => prisma.list.findMany({ where: { OR: [ { ownerId: userId }, { collaborators: { some: { userId: userId, }, }, }, ], }, include: { items: true, owner: { select: { id: true, name: true, email: true, }, }, collaborators: { include: { user: { select: { id: true, name: true, email: true, }, }, }, }, }, orderBy: { updatedAt: 'desc', }, }) ) } catch (error) { console.error('Error fetching lists:', error) if (error instanceof Error && error.message.includes('Unauthorized')) { throw error } throw new Error('Failed to load lists. Please check your connection and try again.') } } export async function createList(title: string, color: string = '#3B82F6') { try { // Get current user session const session = await auth() if (!session?.user?.id) { throw new Error('Unauthorized: Please log in to create lists') } const userId = session.user.id // Validate and sanitize input const titleResult = validateListTitle(title) if (!titleResult.isValid) { throw new Error(titleResult.error) } const colorResult = validateColor(color) if (!colorResult.isValid) { throw new Error(colorResult.error) } const sanitizedTitle = sanitizeString(title) const list = await retryOperation(() => prisma.list.create({ data: { title: sanitizedTitle, color, ownerId: userId, }, }) ) revalidatePath('/') return list } catch (error) { console.error('Error creating list:', error) if (error instanceof Error && (error.message.includes('required') || error.message.includes('must be') || error.message.includes('Unauthorized'))) { throw error } throw new Error('Failed to create list. Please try again.') } } export async function getListWithItems(listId: string) { try { if (!listId) { throw new Error('List ID is required') } // Get current user session const session = await auth() if (!session?.user?.id) { throw new Error('Unauthorized: Please log in to view lists') } const userId = session.user.id const list = await retryOperation(() => prisma.list.findFirst({ where: { id: listId, OR: [ { ownerId: userId }, { collaborators: { some: { userId: userId, }, }, }, ], }, include: { items: { orderBy: { createdAt: 'desc', }, }, owner: { select: { id: true, name: true, email: true, }, }, collaborators: { include: { user: { select: { id: true, name: true, email: true, }, }, }, }, }, }) ) if (!list) { throw new Error('List not found or access denied') } return list } catch (error) { console.error('Error fetching list:', error) if (error instanceof Error && (error.message === 'List not found or access denied' || error.message.includes('Unauthorized'))) { throw error } throw new Error('Failed to load list. Please try again.') } } export async function getItem(itemId: string) { try { if (!itemId) { throw new Error('Item ID is required') } // Get current user session const session = await auth() if (!session?.user?.id) { throw new Error('Unauthorized: Please log in to view items') } const userId = session.user.id const item = await retryOperation(() => prisma.item.findUnique({ where: { id: itemId }, include: { list: { include: { owner: { select: { id: true, name: true, email: true, }, }, collaborators: { include: { user: { select: { id: true, name: true, email: true, }, }, }, }, }, }, }, }) ) if (!item) { throw new Error('Item not found') } // Check if user has access to the parent list const hasAccess = item.list.ownerId === userId || item.list.collaborators.some(c => c.userId === userId) if (!hasAccess) { throw new Error('You do not have permission to view this item') } return item } catch (error) { console.error('Error fetching item:', error) if (error instanceof Error && (error.message === 'Item not found' || error.message.includes('Unauthorized') || error.message.includes('permission'))) { throw error } throw new Error('Failed to load item. Please try again.') } } export async function createItem( listId: string, title: string, description?: string, metadata?: { priority?: 'low' | 'medium' | 'high' dueDate?: Date | null tags?: string[] notes?: string | null } ) { try { // Validate input if (!listId) { throw new Error('List ID is required') } // Get current user session const session = await auth() if (!session?.user?.id) { throw new Error('Unauthorized: Please log in to create items') } const userId = session.user.id // Check if user has access to the list (owner or editor) const list = await retryOperation(() => prisma.list.findFirst({ where: { id: listId, OR: [ { ownerId: userId }, { collaborators: { some: { userId: userId, permission: 'editor', }, }, }, ], }, }) ) if (!list) { throw new Error('List not found or you do not have permission to add items') } const titleResult = validateItemTitle(title) if (!titleResult.isValid) { throw new Error(titleResult.error) } // Validate tags if provided if (metadata?.tags) { const tagsResult = validateTags(metadata.tags) if (!tagsResult.isValid) { throw new Error(tagsResult.error) } } // Sanitize inputs const sanitizedTitle = sanitizeString(title) const sanitizedDescription = description ? sanitizeString(description) : null const sanitizedNotes = metadata?.notes ? sanitizeString(metadata.notes) : null const sanitizedTags = metadata?.tags ? metadata.tags.map(sanitizeString) : undefined const item = await retryOperation(() => prisma.item.create({ data: { title: sanitizedTitle, description: sanitizedDescription, listId, ...(metadata?.priority && { priority: metadata.priority }), ...(metadata?.dueDate !== undefined && { dueDate: metadata.dueDate }), ...(sanitizedTags && { tags: sanitizedTags }), ...(sanitizedNotes !== undefined && { notes: sanitizedNotes }), }, }) ) revalidatePath(`/list/${listId}`) return item } catch (error) { console.error('Error creating item:', error) if (error instanceof Error && (error.message.includes('required') || error.message.includes('must be') || error.message.includes('Invalid') || error.message.includes('Unauthorized') || error.message.includes('permission'))) { throw error } throw new Error('Failed to create item. Please try again.') } } export async function toggleItem(itemId: string) { try { if (!itemId) { throw new Error('Item ID is required') } // Get current user session const session = await auth() if (!session?.user?.id) { throw new Error('Unauthorized: Please log in to update items') } const userId = session.user.id const item = await retryOperation(() => prisma.item.findUnique({ where: { id: itemId }, include: { list: true }, }) ) if (!item) { throw new Error('Item not found') } // Check if user has access to the parent list (owner or editor) const hasAccess = await retryOperation(() => prisma.list.findFirst({ where: { id: item.listId, OR: [ { ownerId: userId }, { collaborators: { some: { userId: userId, permission: 'editor', }, }, }, ], }, }) ) if (!hasAccess) { throw new Error('You do not have permission to update this item') } const updated = await retryOperation(() => prisma.item.update({ where: { id: itemId }, data: { completed: !item.completed }, }) ) revalidatePath(`/list/${item.listId}`) return updated } catch (error) { console.error('Error toggling item:', error) if (error instanceof Error && (error.message === 'Item not found' || error.message.includes('Unauthorized') || error.message.includes('permission'))) { throw error } throw new Error('Failed to update item. Please try again.') } } export async function updateItem( itemId: string, data: { title?: string description?: string | null completed?: boolean priority?: 'low' | 'medium' | 'high' dueDate?: Date | null tags?: string[] notes?: string | null } ) { try { if (!itemId) { throw new Error('Item ID is required') } // Get current user session const session = await auth() if (!session?.user?.id) { throw new Error('Unauthorized: Please log in to update items') } const userId = session.user.id // Validate and sanitize title if provided if (data.title !== undefined) { const titleResult = validateItemTitle(data.title) if (!titleResult.isValid) { throw new Error(titleResult.error) } data.title = sanitizeString(data.title) } // Sanitize description if provided if (data.description !== undefined && data.description !== null) { data.description = sanitizeString(data.description) || null } // Validate tags if provided if (data.tags !== undefined) { const tagsResult = validateTags(data.tags) if (!tagsResult.isValid) { throw new Error(tagsResult.error) } data.tags = data.tags.map(sanitizeString) } // Sanitize notes if provided if (data.notes !== undefined && data.notes !== null) { data.notes = sanitizeString(data.notes) || null } const item = await retryOperation(() => prisma.item.findUnique({ where: { id: itemId }, include: { list: true }, }) ) if (!item) { throw new Error('Item not found') } // Check if user has access to the parent list (owner or editor) const hasAccess = await retryOperation(() => prisma.list.findFirst({ where: { id: item.listId, OR: [ { ownerId: userId }, { collaborators: { some: { userId: userId, permission: 'editor', }, }, }, ], }, }) ) if (!hasAccess) { throw new Error('You do not have permission to update this item') } const updated = await retryOperation(() => prisma.item.update({ where: { id: itemId }, data, }) ) revalidatePath(`/list/${item.listId}`) return updated } catch (error) { console.error('Error updating item:', error) if (error instanceof Error && (error.message.includes('not found') || error.message.includes('required') || error.message.includes('cannot be') || error.message.includes('Invalid') || error.message.includes('Unauthorized') || error.message.includes('permission'))) { throw error } throw new Error('Failed to update item. Please try again.') } } export async function deleteItem(itemId: string) { try { if (!itemId) { throw new Error('Item ID is required') } // Get current user session const session = await auth() if (!session?.user?.id) { throw new Error('Unauthorized: Please log in to delete items') } const userId = session.user.id const item = await retryOperation(() => prisma.item.findUnique({ where: { id: itemId }, include: { list: true }, }) ) if (!item) { throw new Error('Item not found') } // Check if user has access to the parent list (owner or editor) const hasAccess = await retryOperation(() => prisma.list.findFirst({ where: { id: item.listId, OR: [ { ownerId: userId }, { collaborators: { some: { userId: userId, permission: 'editor', }, }, }, ], }, }) ) if (!hasAccess) { throw new Error('You do not have permission to delete this item') } await retryOperation(() => prisma.item.delete({ where: { id: itemId }, }) ) revalidatePath(`/list/${item.listId}`) return { success: true } } catch (error) { console.error('Error deleting item:', error) if (error instanceof Error && (error.message === 'Item not found' || error.message.includes('Unauthorized') || error.message.includes('permission'))) { throw error } throw new Error('Failed to delete item. Please try again.') } } export async function deleteList(listId: string) { try { if (!listId) { throw new Error('List ID is required') } // Get current user session const session = await auth() if (!session?.user?.id) { throw new Error('Unauthorized: Please log in to delete lists') } const userId = session.user.id // Check if list exists and user is the owner (only owners can delete) const list = await retryOperation(() => prisma.list.findFirst({ where: { id: listId, ownerId: userId, }, }) ) if (!list) { throw new Error('List not found or you do not have permission to delete it') } await retryOperation(() => prisma.list.delete({ where: { id: listId }, }) ) revalidatePath('/') return { success: true } } catch (error) { console.error('Error deleting list:', error) if (error instanceof Error && (error.message.includes('not found') || error.message.includes('Unauthorized') || error.message.includes('permission'))) { throw error } throw new Error('Failed to delete list. Please try again.') } } export async function updateList( listId: string, data: { title?: string color?: string } ) { try { if (!listId) { throw new Error('List ID is required') } // Get current user session const session = await auth() if (!session?.user?.id) { throw new Error('Unauthorized: Please log in to update lists') } const userId = session.user.id // Check if user has access to the list (owner or editor) const list = await retryOperation(() => prisma.list.findFirst({ where: { id: listId, OR: [ { ownerId: userId }, { collaborators: { some: { userId: userId, permission: 'editor', }, }, }, ], }, }) ) if (!list) { throw new Error('List not found or you do not have permission to update it') } // Validate and sanitize title if provided if (data.title !== undefined) { const titleResult = validateListTitle(data.title) if (!titleResult.isValid) { throw new Error(titleResult.error) } data.title = sanitizeString(data.title) } // Validate color if provided if (data.color !== undefined) { const colorResult = validateColor(data.color) if (!colorResult.isValid) { throw new Error(colorResult.error) } } const updated = await retryOperation(() => prisma.list.update({ where: { id: listId }, data, }) ) revalidatePath('/') revalidatePath(`/list/${listId}`) return updated } catch (error) { console.error('Error updating list:', error) if (error instanceof Error && (error.message.includes('not found') || error.message.includes('required') || error.message.includes('cannot be') || error.message.includes('Invalid') || error.message.includes('Unauthorized') || error.message.includes('permission'))) { throw error } throw new Error('Failed to update list. Please try again.') } } export async function searchItems(query: string, listId?: string) { try { if (!query || !query.trim()) { return [] } // Get current user session const session = await auth() if (!session?.user?.id) { throw new Error('Unauthorized: Please log in to search items') } const userId = session.user.id const searchTerm = query.toLowerCase() // Build the where clause const whereClause: any = { OR: [ { title: { contains: query, mode: 'insensitive' as const, }, }, { description: { contains: query, mode: 'insensitive' as const, }, }, { notes: { contains: query, mode: 'insensitive' as const, }, }, ], list: { OR: [ { ownerId: userId }, { collaborators: { some: { userId: userId, }, }, }, ], }, } // Add list scope if specified if (listId) { whereClause.listId = listId } const items = await retryOperation(() => prisma.item.findMany({ where: whereClause, include: { list: true, }, orderBy: { updatedAt: 'desc', }, }) ) // Also search in tags (stored as JSON) const allItemsWhere: any = { list: { OR: [ { ownerId: userId }, { collaborators: { some: { userId: userId, }, }, }, ], }, } if (listId) { allItemsWhere.listId = listId } const allItems = await retryOperation(() => prisma.item.findMany({ where: allItemsWhere, include: { list: true }, }) ) const tagMatches = allItems.filter((item) => { const tags = Array.isArray(item.tags) ? item.tags : [] return tags.some((tag) => typeof tag === 'string' && tag.toLowerCase().includes(searchTerm) ) }) // Combine results and remove duplicates const combinedResults = [...items, ...tagMatches] const uniqueResults = Array.from( new Map(combinedResults.map((item) => [item.id, item])).values() ) return uniqueResults } catch (error) { console.error('Error searching items:', error) if (error instanceof Error && error.message.includes('Unauthorized')) { throw error } throw new Error('Failed to search items. Please try again.') } } export async function getItemsByFilter( listId: string | null, filters: { priority?: 'low' | 'medium' | 'high' | null completed?: boolean | null tags?: string[] dueDateRange?: { start: Date | null; end: Date | null } sortBy?: 'createdAt' | 'dueDate' | 'priority' | 'title' sortOrder?: 'asc' | 'desc' page?: number pageSize?: number } = {} ) { try { // Get current user session const session = await auth() if (!session?.user?.id) { throw new Error('Unauthorized: Please log in to filter items') } const userId = session.user.id // Build the where clause with compound filters (AND logic) const whereClause: any = { list: { OR: [ { ownerId: userId }, { collaborators: { some: { userId: userId, }, }, }, ], }, } // Add list scope if specified if (listId) { whereClause.listId = listId } // Filter by priority if (filters.priority) { whereClause.priority = filters.priority } // Filter by completed status if (filters.completed !== undefined && filters.completed !== null) { whereClause.completed = filters.completed } // Filter by due date range if (filters.dueDateRange) { const { start, end } = filters.dueDateRange if (start || end) { whereClause.dueDate = {} if (start) { whereClause.dueDate.gte = start } if (end) { whereClause.dueDate.lte = end } } } // Pagination parameters const page = filters.page || 1 const pageSize = filters.pageSize || 50 const skip = (page - 1) * pageSize // Fetch items from database with pagination const [items, totalCount] = await retryOperation(() => Promise.all([ prisma.item.findMany({ where: whereClause, include: { list: true, }, skip, take: pageSize, }), prisma.item.count({ where: whereClause, }), ]) ) // Filter by tags (client-side since tags are stored as JSON) let filteredItems = items if (filters.tags && filters.tags.length > 0) { filteredItems = items.filter((item) => { const itemTags = Array.isArray(item.tags) ? item.tags : [] return filters.tags!.every((filterTag) => itemTags.some((itemTag) => typeof itemTag === 'string' && itemTag === filterTag) ) }) } // Sort items const sortBy = filters.sortBy || 'createdAt' const sortOrder = filters.sortOrder || 'desc' const sortedItems = [...filteredItems].sort((a, b) => { let comparison = 0 switch (sortBy) { case 'createdAt': comparison = a.createdAt.getTime() - b.createdAt.getTime() break case 'dueDate': // Handle null due dates - place at end if (a.dueDate === null && b.dueDate === null) { comparison = 0 } else if (a.dueDate === null) { comparison = 1 // a goes after b } else if (b.dueDate === null) { comparison = -1 // b goes after a } else { comparison = a.dueDate.getTime() - b.dueDate.getTime() } break case 'priority': const priorityOrder = { low: 1, medium: 2, high: 3 } const aPriority = priorityOrder[a.priority as keyof typeof priorityOrder] || 2 const bPriority = priorityOrder[b.priority as keyof typeof priorityOrder] || 2 comparison = aPriority - bPriority break case 'title': comparison = a.title.localeCompare(b.title) break } return sortOrder === 'asc' ? comparison : -comparison }) return { items: sortedItems, pagination: { page, pageSize, totalCount, totalPages: Math.ceil(totalCount / pageSize), hasMore: page * pageSize < totalCount, }, } } catch (error) { console.error('Error filtering items:', error) if (error instanceof Error && error.message.includes('Unauthorized')) { throw error } throw new Error('Failed to filter items. Please try again.') } }