import { type BankAccount, PrismaClient } from '@prisma/client'; import dotenv from 'dotenv'; import Fastify, { type FastifyInstance } from 'fastify'; import type { ZodTypeProvider } from 'fastify-type-provider-zod'; import { z } from 'zod'; dotenv.config(); const prisma = new PrismaClient(); // Base schema for common fields, useful for reuse const bankAccountBaseSchema = z.object({ name: z.string().min(1, { message: 'Name cannot be empty' }), bankName: z.string().min(1, { message: 'Bank name cannot be empty' }), accountNumber: z.string().min(1, { message: 'Account number cannot be empty' }), }); // Schema for creating a bank account (all fields required) const createBankAccountSchema = bankAccountBaseSchema; // Schema for request parameters containing an ID const paramsSchema = z.object({ // Use coerce to automatically convert string param to number id: z.coerce.number().int().positive({ message: 'ID must be a positive integer' }), }); // Schema for updating a bank account (all fields optional) const updateBankAccountSchema = bankAccountBaseSchema.partial(); // Makes all fields optional // --- Fastify Server Instance with Zod Type Provider --- const server: FastifyInstance = Fastify({ logger: true, }).withTypeProvider(); // Enable Zod validation and typing // --- API Routes --- const API_PREFIX = '/api/bank-account'; // 1. Create Bank Account server.post( `${API_PREFIX}/create`, { schema: { // Define Zod schema for the request body body: createBankAccountSchema, }, }, async (request, reply): Promise => { try { // request.body is now typed and validated by Zod! const newAccount = await prisma.bankAccount.create({ data: request.body, // Pass validated body directly }); reply.code(201); return newAccount; } catch (error: unknown) { server.log.error(error); if ( typeof error === 'object' && error !== null && 'code' in error && error.code === 'P2002' && 'meta' in error && error.meta?.target?.includes('accountNumber') ) { reply.code(409); const body = createBankAccountSchema.parse(request.body); throw new Error(`Bank account with number ${body.accountNumber} already exists.`); } reply.code(500); throw new Error('Failed to create bank account.'); } }, ); // 2. Update Bank Account server.post( `${API_PREFIX}/update/:id`, { schema: { // Define Zod schemas for params and body params: paramsSchema, body: updateBankAccountSchema, }, }, async (request, reply): Promise => { try { // request.params.id is now a validated number // request.body is now a validated partial object const { id } = request.params; const updateData = request.body; // Prevent updating with an empty object if (Object.keys(updateData).length === 0) { reply.code(400); throw new Error('Request body cannot be empty for update.'); } const updatedAccount = await prisma.bankAccount.update({ where: { id: id }, // Use the validated numeric ID data: updateData, }); return updatedAccount; } catch (error: unknown) { server.log.error(error); if ( typeof error === 'object' && error !== null && 'code' in error && error.code === 'P2025' ) { // Record to update not found reply.code(404); throw new Error(`Bank account with ID ${request.params.id} not found.`); } if ( typeof error === 'object' && error !== null && 'code' in error && error.code === 'P2002' && 'meta' in error && error.meta?.target?.includes('accountNumber') ) { reply.code(409); // Access accountNumber safely as it's optional in update const attemptedNumber = request.body.accountNumber || '(unchanged)'; throw new Error(`Bank account with number ${attemptedNumber} already exists.`); } // Handle Zod validation errors specifically if needed (though Fastify usually does) if (error instanceof z.ZodError) { reply.code(400); throw new Error(`Validation Error: ${error.errors.map((e) => e.message).join(', ')}`); } reply.code(500); throw new Error('Failed to update bank account.'); } }, ); // 3. Delete Bank Account server.delete( `${API_PREFIX}/delete/:id`, { schema: { // Define Zod schema for params params: paramsSchema, }, }, async (request, reply): Promise<{ message: string; deletedAccount: BankAccount }> => { try { // request.params.id is now a validated number const { id } = request.params; const deletedAccount = await prisma.bankAccount.delete({ where: { id: id }, // Use the validated numeric ID }); return { message: `Bank account with ID ${id} deleted successfully.`, deletedAccount }; } catch (error: unknown) { server.log.error(error); if ( typeof error === 'object' && error !== null && 'code' in error && error.code === 'P2025' ) { // Record to delete not found reply.code(404); throw new Error(`Bank account with ID ${request.params.id} not found.`); } // Handle Zod validation errors if (error instanceof z.ZodError) { reply.code(400); throw new Error(`Validation Error: ${error.errors.map((e) => e.message).join(', ')}`); } reply.code(500); throw new Error('Failed to delete bank account.'); } }, ); // 4. Get All Bank Accounts server.get(`${API_PREFIX}/`, async (request, reply): Promise => { // No input validation needed for getting all items usually try { const accounts = await prisma.bankAccount.findMany({ orderBy: { createdAt: 'desc' }, }); return accounts; } catch (error: unknown) { server.log.error(error); reply.code(500); throw new Error('Failed to retrieve bank accounts.'); } }); // Optional: Get Single Bank Account by ID server.get( `${API_PREFIX}/:id`, { schema: { // Define Zod schema for params params: paramsSchema, }, }, async (request, reply): Promise => { try { // request.params.id is now a validated number const { id } = request.params; const account = await prisma.bankAccount.findUnique({ where: { id: id }, // Use the validated numeric ID }); if (!account) { reply.code(404); throw new Error(`Bank account with ID ${id} not found.`); } return account; } catch (error: unknown) { // Handle Zod validation errors (though should be caught by Fastify earlier) if (error instanceof z.ZodError) { reply.code(400); throw new Error(`Validation Error: ${error.errors.map((e) => e.message).join(', ')}`); } // If Prisma throws or other errors occur after validation if (!reply.sent) { // Specific check for Prisma's RecordNotFound (though findUnique returns null, not throws P2025 by default) // The !account check above handles the "not found" case for findUnique server.log.error(error); // Log other unexpected errors reply.code(500); throw new Error('Failed to retrieve bank account.'); } // If reply already sent (e.g., 404), just rethrow the original error throw error; } }, ); // --- Graceful Shutdown --- const gracefulShutdown = async (signal: string) => { console.log(`*^! Received signal ${signal}. Shutting down...`); try { await server.close(); console.log('Fastify server closed.'); await prisma.$disconnect(); console.log('Prisma client disconnected.'); process.exit(0); } catch (err) { console.error('Error during shutdown:', err); process.exit(1); } }; process.on('SIGINT', () => gracefulShutdown('SIGINT')); process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); // --- Start Server (unchanged) --- const start = async () => { try { const host = process.env.API_HOST || '0.0.0.0'; const port = Number.parseInt(process.env.API_PORT || '3000', 10); await server.listen({ port, host }); } catch (err) { server.log.error(err); await prisma.$disconnect(); process.exit(1); } }; start();