From 8a41239e1b04770944cadf3d3c45e9e1dd562e3f Mon Sep 17 00:00:00 2001 From: Akhun Akhand Date: Fri, 5 Dec 2025 09:15:30 +0000 Subject: [PATCH 1/5] feat: Optimize getProjects with pagination and reduced selects --- src/actions/projects/getProjects.ts | 40 ++++++++++++++++++----------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/src/actions/projects/getProjects.ts b/src/actions/projects/getProjects.ts index 6523a0d..0cc5d80 100644 --- a/src/actions/projects/getProjects.ts +++ b/src/actions/projects/getProjects.ts @@ -6,21 +6,31 @@ interface GetProjectsParams { userId: string; search?: string; type?: ProjectType; + page?: number; + perPage?: number; } -export async function getProjects({ userId, search, type }: GetProjectsParams): Promise> { - const projects: Project[] = await prisma.project.findMany({ - where: { - userId, - ...(search && { - title: { - contains: search, - mode: 'insensitive' - } - }), - ...(type && { type }) - }, - orderBy: { createdAt: 'desc' } - }); - return success(projects); +export async function getProjects({ userId, search, type, page = 1, perPage = 10 }: GetProjectsParams): Promise> { + const skip = (page - 1) * perPage; + const whereClause = { + userId, + ...(search && { + title: { + contains: search, + mode: 'insensitive' + } + }), + ...(type && { type }) + }; + + const [projects, total] = await prisma.$transaction([ + prisma.project.findMany({ + skip, + take: perPage, + where: whereClause, + orderBy: { createdAt: 'desc' } + }), + prisma.project.count({ where: whereClause }) + ]); + return success({ projects, total }); } From 07df49a5f72a7614a6f6dbee677b7784ea9f87da Mon Sep 17 00:00:00 2001 From: Akhun Akhand Date: Fri, 5 Dec 2025 09:31:56 +0000 Subject: [PATCH 2/5] Fix: Add input validation for project pagination and optimize Prisma queries --- src/actions/projects/getProjects.ts | 18 +++++++++++++++--- src/app/(general)/page.tsx | 5 ++--- .../pages/home/HomePageContainer.tsx | 5 +++-- src/lib/export.ts | 6 +++--- 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/actions/projects/getProjects.ts b/src/actions/projects/getProjects.ts index 0cc5d80..3a44b5c 100644 --- a/src/actions/projects/getProjects.ts +++ b/src/actions/projects/getProjects.ts @@ -10,7 +10,11 @@ interface GetProjectsParams { perPage?: number; } -export async function getProjects({ userId, search, type, page = 1, perPage = 10 }: GetProjectsParams): Promise> { +export async function getProjects({ userId, search, type, page: rawPage, perPage: rawPerPage }: GetProjectsParams): Promise> { + const MAX_PER_PAGE = 100; + const page = Math.max(1, Math.floor(rawPage ?? 1)); + const perPage = Math.min(MAX_PER_PAGE, Math.max(1, Math.floor(rawPerPage ?? 20))); + const skip = (page - 1) * perPage; const whereClause = { userId, @@ -23,12 +27,20 @@ export async function getProjects({ userId, search, type, page = 1, perPage = 10 ...(type && { type }) }; - const [projects, total] = await prisma.$transaction([ + const [projects, total] = await Promise.all([ prisma.project.findMany({ skip, take: perPage, where: whereClause, - orderBy: { createdAt: 'desc' } + orderBy: { createdAt: 'desc' }, + select: { + id: true, + title: true, + createdAt: true, + updatedAt: true, + type: true, + userId: true, + }, }), prisma.project.count({ where: whereClause }) ]); diff --git a/src/app/(general)/page.tsx b/src/app/(general)/page.tsx index eef4ded..37838ab 100644 --- a/src/app/(general)/page.tsx +++ b/src/app/(general)/page.tsx @@ -16,7 +16,7 @@ export default async function HomePage() { redirect('/signin'); } - const [projectsError, projectsResult] = await safeAwait(getProjects(userId)); + const [projectsError, projectsResult] = await safeAwait(getProjects({ userId })); const [analyticsError, analyticsResult] = await safeAwait(getProjectAnalytics()); if (projectsError) { @@ -27,7 +27,6 @@ export default async function HomePage() { return ; } - const initialProjects = result.data; + const { projects: initialProjects, total } = projectsResult.data; return ; -} diff --git a/src/components/pages/home/HomePageContainer.tsx b/src/components/pages/home/HomePageContainer.tsx index c3edd8a..4638cb0 100644 --- a/src/components/pages/home/HomePageContainer.tsx +++ b/src/components/pages/home/HomePageContainer.tsx @@ -18,9 +18,10 @@ export function HomePageContainer({ initialProjects, userId }: HomePageContainer useEffect(() => { const loadProjects = async () => { - const result = await fetchProjects(userId); + const result = await fetchProjects({ userId }); if (result.success) { - setProjects(result.data); + const { projects: fetchedProjects, total } = result.data; + setProjects(fetchedProjects); } else { // Handle error, maybe show a toast or alert console.error('Failed to fetch projects:', result.error); diff --git a/src/lib/export.ts b/src/lib/export.ts index fe1a55d..e8ed3da 100644 --- a/src/lib/export.ts +++ b/src/lib/export.ts @@ -1,13 +1,13 @@ import { getProjects } from '@/actions/projects/getProjects'; -export async function exportProjects(format: 'csv' | 'pdf') { - const result = await getProjects(); +export async function exportProjects(userId: string, format: 'csv' | 'pdf') { + const result = await getProjects({ userId }); if (!result.success) { throw new Error('Failed to fetch projects for export.'); } - const projects = result.data; + const { projects, total } = result.data; if (format === 'csv') { const csvContent = From 1e62c30211c3a15d65b8664fccd6ad1d2df6a4ea Mon Sep 17 00:00:00 2001 From: Akhun Akhand Date: Fri, 5 Dec 2025 09:44:55 +0000 Subject: [PATCH 3/5] Fix project pagination, export all projects, and improve type safety --- src/actions/projects/getProjects.ts | 15 +++++++++--- src/lib/export.ts | 38 +++++++++++++++++++++++------ 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/src/actions/projects/getProjects.ts b/src/actions/projects/getProjects.ts index 3a44b5c..0c97fac 100644 --- a/src/actions/projects/getProjects.ts +++ b/src/actions/projects/getProjects.ts @@ -2,6 +2,15 @@ import { Project, ProjectType } from '@/generated/prisma'; import { prisma } from '@/lib/prisma'; import { Result, success } from '@/lib/result'; +export interface ProjectListItem { + id: string; + title: string; + createdAt: Date; + updatedAt: Date; + type: ProjectType; + userId: string; +} + interface GetProjectsParams { userId: string; search?: string; @@ -10,10 +19,10 @@ interface GetProjectsParams { perPage?: number; } -export async function getProjects({ userId, search, type, page: rawPage, perPage: rawPerPage }: GetProjectsParams): Promise> { +export async function getProjects({ userId, search, type, page: rawPage, perPage: rawPerPage }: GetProjectsParams): Promise> { const MAX_PER_PAGE = 100; - const page = Math.max(1, Math.floor(rawPage ?? 1)); - const perPage = Math.min(MAX_PER_PAGE, Math.max(1, Math.floor(rawPerPage ?? 20))); + const page = Math.max(1, Math.floor(Number.isFinite(rawPage) ? rawPage : 1)); + const perPage = Math.min(MAX_PER_PAGE, Math.max(1, Math.floor(Number.isFinite(rawPerPage) ? rawPerPage : 20))); const skip = (page - 1) * perPage; const whereClause = { diff --git a/src/lib/export.ts b/src/lib/export.ts index e8ed3da..6a9a364 100644 --- a/src/lib/export.ts +++ b/src/lib/export.ts @@ -1,18 +1,42 @@ -import { getProjects } from '@/actions/projects/getProjects'; +import { getProjects, ProjectListItem } from '@/actions/projects/getProjects'; export async function exportProjects(userId: string, format: 'csv' | 'pdf') { - const result = await getProjects({ userId }); + let allProjects: ProjectListItem[] = []; + let page = 1; + const perPage = 100; - if (!result.success) { - throw new Error('Failed to fetch projects for export.'); - } + let total = 0; + let fetchedCount = 0; + + do { + const result = await getProjects({ userId, page, perPage }); + + if (!result.success) { + throw new Error('Failed to fetch projects for export.'); + } + + allProjects = allProjects.concat(result.data.projects); + total = result.data.total; + fetchedCount += result.data.projects.length; + page++; + } while (fetchedCount < total); - const { projects, total } = result.data; if (format === 'csv') { + if (allProjects.length === 0) { + const csvContent = 'data:text/csv;charset=utf-8,'; + const encodedUri = encodeURI(csvContent); + const link = document.createElement('a'); + link.setAttribute('href', encodedUri); + link.setAttribute('download', 'projects.csv'); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + return; + } const csvContent = 'data:text/csv;charset=utf-8,' + - [Object.keys(projects[0]).join(','), ...projects.map((project) => Object.values(project).join(','))].join('\n'); + [Object.keys(allProjects[0]).join(','), ...allProjects.map((project) => Object.values(project).join(','))].join('\n'); const encodedUri = encodeURI(csvContent); const link = document.createElement('a'); link.setAttribute('href', encodedUri); From 7aad7ca63a9ba96224df383750a3a60488af3993 Mon Sep 17 00:00:00 2001 From: Akhun Akhand Date: Fri, 5 Dec 2025 10:38:08 +0000 Subject: [PATCH 4/5] Fix: Improve pagination type safety and robust CSV export --- src/actions/projects/getProjects.ts | 4 +-- src/lib/export.ts | 40 +++++++++++++++++++---------- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/src/actions/projects/getProjects.ts b/src/actions/projects/getProjects.ts index 0c97fac..9dc92a5 100644 --- a/src/actions/projects/getProjects.ts +++ b/src/actions/projects/getProjects.ts @@ -21,8 +21,8 @@ interface GetProjectsParams { export async function getProjects({ userId, search, type, page: rawPage, perPage: rawPerPage }: GetProjectsParams): Promise> { const MAX_PER_PAGE = 100; - const page = Math.max(1, Math.floor(Number.isFinite(rawPage) ? rawPage : 1)); - const perPage = Math.min(MAX_PER_PAGE, Math.max(1, Math.floor(Number.isFinite(rawPerPage) ? rawPerPage : 20))); + const page = Math.max(1, Math.floor(typeof rawPage === 'number' && Number.isFinite(rawPage) ? rawPage : 1)); + const perPage = Math.min(MAX_PER_PAGE, Math.max(1, Math.floor(typeof rawPerPage === 'number' && Number.isFinite(rawPerPage) ? rawPerPage : 20))); const skip = (page - 1) * perPage; const whereClause = { diff --git a/src/lib/export.ts b/src/lib/export.ts index 6a9a364..7c0ff55 100644 --- a/src/lib/export.ts +++ b/src/lib/export.ts @@ -1,9 +1,25 @@ import { getProjects, ProjectListItem } from '@/actions/projects/getProjects'; +function escapeCsvValue(value: any): string { + let stringValue = String(value); + + if (value instanceof Date) { + stringValue = value.toISOString(); + } + + stringValue = stringValue.replace(/"/g, '""'); + + if (stringValue.startsWith('=') || stringValue.startsWith('+') || stringValue.startsWith('-') || stringValue.startsWith('@')) { + stringValue = ''' + stringValue; + } + + return `"${stringValue}"`; +} + export async function exportProjects(userId: string, format: 'csv' | 'pdf') { let allProjects: ProjectListItem[] = []; let page = 1; - const perPage = 100; + const perPage = 100; let total = 0; let fetchedCount = 0; @@ -23,20 +39,18 @@ export async function exportProjects(userId: string, format: 'csv' | 'pdf') { if (format === 'csv') { - if (allProjects.length === 0) { - const csvContent = 'data:text/csv;charset=utf-8,'; - const encodedUri = encodeURI(csvContent); - const link = document.createElement('a'); - link.setAttribute('href', encodedUri); - link.setAttribute('download', 'projects.csv'); - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - return; - } + const headers = allProjects.length > 0 ? Object.keys(allProjects[0]) : ['id', 'title', 'createdAt', 'updatedAt', 'type', 'userId']; // Fallback headers for empty projects + const escapedHeaders = headers.map(escapeCsvValue).join(','); + + const csvRows = allProjects.map(project => { + const rowValues = headers.map(header => project[header as keyof ProjectListItem]); + return rowValues.map(escapeCsvValue).join(','); + }); + const csvContent = 'data:text/csv;charset=utf-8,' + - [Object.keys(allProjects[0]).join(','), ...allProjects.map((project) => Object.values(project).join(','))].join('\n'); + [escapedHeaders, ...csvRows].join('\n'); + const encodedUri = encodeURI(csvContent); const link = document.createElement('a'); link.setAttribute('href', encodedUri); From 0fc8f164b64a4d022935716ffde903abc8e3046d Mon Sep 17 00:00:00 2001 From: Akhun Akhand Date: Fri, 5 Dec 2025 10:54:47 +0000 Subject: [PATCH 5/5] Fix: Resolve unterminated string literal in export.ts --- src/lib/export.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/export.ts b/src/lib/export.ts index 7c0ff55..9d68965 100644 --- a/src/lib/export.ts +++ b/src/lib/export.ts @@ -10,7 +10,7 @@ function escapeCsvValue(value: any): string { stringValue = stringValue.replace(/"/g, '""'); if (stringValue.startsWith('=') || stringValue.startsWith('+') || stringValue.startsWith('-') || stringValue.startsWith('@')) { - stringValue = ''' + stringValue; + stringValue = "'" + stringValue; } return `"${stringValue}"`;