Files
finance/src/server.ts
GitHub Copilot d8678e68ed Refactor transaction components and styles
- Updated imports to use absolute paths for types and utilities.
- Enhanced TransactionTable component with mobile responsiveness and card-based layout.
- Improved loading and error handling in transaction fetching logic.
- Refactored transaction update API to streamline validation and data preparation.
- Added new styles for Radix UI components and improved global styles for better mobile experience.
- Implemented collapsible sections and improved button interactions in the UI.
- Updated tests to reflect changes in component structure and imports.
2025-05-07 17:10:21 -04:00

271 lines
8.3 KiB
TypeScript

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<ZodTypeProvider>(); // 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<BankAccount> => {
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<BankAccount> => {
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<BankAccount[]> => {
// 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<BankAccount> => {
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();