Set up backend API server with Express and Prisma

- Initialize Node.js/Express backend project structure
- Configure TypeScript for backend development
- Set up Prisma ORM with MySQL database schema
- Implement authentication middleware with Clerk
- Create photo upload endpoint with EXIF extraction
- Build report CRUD endpoints (create, read, delete)
- Add file upload handling with Multer
- Configure CORS and security middleware (helmet)
- Set up error handling and validation
- Create development environment configuration

Task 21 complete: Backend foundation ready for report submissions
This commit is contained in:
2025-11-16 17:29:26 +00:00
parent 7dc1ff740b
commit c0ec28c5c9
15 changed files with 3191 additions and 0 deletions

View File

@@ -0,0 +1,168 @@
import prisma from '../config/database';
import { Prisma } from '@prisma/client';
export interface CreateReportData {
userId: string;
latitude: number;
longitude: number;
locationSource: 'exif' | 'gps' | 'manual';
photoUrls: string[];
photoTimestamp?: Date;
litterType: string;
quantity: string;
description?: string;
environmentalConcerns?: Array<{
type: string;
severity: string;
description: string;
}>;
}
export class ReportService {
async createReport(data: CreateReportData) {
const report = await prisma.report.create({
data: {
userId: data.userId,
latitude: new Prisma.Decimal(data.latitude),
longitude: new Prisma.Decimal(data.longitude),
locationSource: data.locationSource,
photoUrls: data.photoUrls,
photoTimestamp: data.photoTimestamp,
litterType: data.litterType,
quantity: data.quantity,
description: data.description,
environmentalConcerns: data.environmentalConcerns
? {
create: data.environmentalConcerns.map((concern) => ({
concernType: concern.type,
severity: concern.severity,
description: concern.description,
})),
}
: undefined,
},
include: {
user: {
select: {
id: true,
username: true,
avatarUrl: true,
},
},
environmentalConcerns: true,
},
});
return report;
}
async getReports(filters?: {
startDate?: Date;
endDate?: Date;
litterTypes?: string[];
quantities?: string[];
verificationStatus?: string[];
bounds?: { north: number; south: number; east: number; west: number };
}) {
const where: any = {};
if (filters?.startDate || filters?.endDate) {
where.createdAt = {};
if (filters.startDate) where.createdAt.gte = filters.startDate;
if (filters.endDate) where.createdAt.lte = filters.endDate;
}
if (filters?.litterTypes?.length) {
where.litterType = { in: filters.litterTypes };
}
if (filters?.quantities?.length) {
where.quantity = { in: filters.quantities };
}
if (filters?.verificationStatus?.length) {
where.verificationStatus = { in: filters.verificationStatus };
}
if (filters?.bounds) {
where.latitude = {
gte: new Prisma.Decimal(filters.bounds.south),
lte: new Prisma.Decimal(filters.bounds.north),
};
where.longitude = {
gte: new Prisma.Decimal(filters.bounds.west),
lte: new Prisma.Decimal(filters.bounds.east),
};
}
const reports = await prisma.report.findMany({
where,
include: {
user: {
select: {
id: true,
username: true,
avatarUrl: true,
},
},
environmentalConcerns: true,
verifications: true,
},
orderBy: {
createdAt: 'desc',
},
});
return reports;
}
async getReportById(id: string) {
const report = await prisma.report.findUnique({
where: { id },
include: {
user: {
select: {
id: true,
username: true,
avatarUrl: true,
},
},
environmentalConcerns: true,
verifications: {
include: {
user: {
select: {
id: true,
username: true,
},
},
},
},
},
});
return report;
}
async deleteReport(id: string, userId: string) {
// Verify ownership
const report = await prisma.report.findUnique({
where: { id },
select: { userId: true },
});
if (!report) {
throw new Error('Report not found');
}
if (report.userId !== userId) {
throw new Error('Unauthorized to delete this report');
}
await prisma.report.delete({
where: { id },
});
}
}
export const reportService = new ReportService();