diff --git a/.github/APP_GOAL.md b/.github/APP_GOAL.md new file mode 100644 index 0000000..bc5a03e --- /dev/null +++ b/.github/APP_GOAL.md @@ -0,0 +1,85 @@ +# Application Goal + +From a high level perspective, I want to build a web application that allows users to manage their financial transactions and institutions. The application should provide a user-friendly interface for users to create, update, delete, and view their financial transactions and institutions. It should also allow users to group their transactions by date or institution for better organization and analysis. +The application should be built using modern web technologies and should be self-hosted. It should also include authentication and authorization features to ensure that users can securely access their data. The application should be designed with scalability in mind, allowing for future enhancements and additional features. +The application should be easy to maintain and extend, with a focus on code quality and best practices. It should also include automated testing and continuous integration/continuous deployment (CI/CD) processes to ensure that changes can be made safely and efficiently. +The application should be well-documented, with clear instructions for installation, configuration, and usage. The documentation should also include information on how to contribute to the project and report issues. +The application should be designed to be user-friendly and accessible, with a focus on providing a positive user experience. It should also include features for data visualization and reporting, allowing users to gain insights into their financial transactions and institutions. +The application should be designed to be responsive and work well on a variety of devices, including desktops, tablets, and smartphones. It should also include features for data import and export, allowing users to easily transfer their data to and from other applications. +The application should be designed to be secure, with a focus on protecting user data and preventing unauthorized access. It should also include features for data backup and recovery, ensuring that users can recover their data in case of loss or corruption. +The application should be designed to be modular and extensible, allowing for the addition of new features and functionality in the future. It should also include features for user feedback and support, allowing users to report issues and request new features. +The application should be designed to be performant, with a focus on minimizing load times and optimizing resource usage. It should also include features for monitoring and logging, allowing developers to track performance and identify issues. + +--- + +## Features + +- User authentication and authorization using OAuth2 +- User profile management +- Financial institution management (create, update, delete, view) +- Financial transaction management (create, update, delete, view) +- Grouping transactions by financial institution +- Grouping transactions by date +- Responsive design for desktop and mobile devices +- Data visualization and reporting features +- Data import and export features +- Data backup and recovery features +- Modular and extensible architecture +- User feedback and support features +- Monitoring and logging features +- Automated testing and CI/CD processes +- Well-documented codebase and user documentation +- Clear instructions for installation, configuration, and usage +- Code quality and best practices +- User-friendly and accessible design +- Performance optimization and resource usage minimization +- Support for recurring transactions and budgeting +- Allow users to set up recurring transactions for regular expenses or income +- Budgeting features to help users track their spending and savings goals + +--- + +## Technical Requirements + +I want to use the following technologies: + +- **Frontend**: React, TypeScript, Tailwind CSS, Zod, Cors +- **Backend**: Node.js, Fastify +- **Database**: PostgreSQL, Prisma +- **Deployment**: Docker, Self-hosted +- **Authentication**: OAuth2 +- **Testing**: Jest, React Testing Library +- **CI/CD**: GitHub Actions +- **Documentation**: Docusaurus + +--- + +## User Stories + +- As a user, I want to be able to create, update, delete, and view - financial institutions. +- As a user, I want to be able to view a list of financial institutions in - the system. +- As a user, I want to be able to create, update, delete, and view - financial transactions. +- As a user, I want to be able to view a list of financial transactions in - the system. +- As a user, I want to be able to view a list of financial transactions - grouped by financial institution. +- As a user, I want to be able to view a list of financial transactions - grouped by date. +- As a user, I want to be able log in using oauth2. +- As a user, I want to be able to log out. +- As a user, I want to be able to view my profile. +- As a user, I want to be able to update my profile. + +--- + +## Out of Scope + +--- + +## Open Questions + +- What specific data visualization and reporting features do you want to include? +- What specific data import and export features do you want to include? +- What specific data backup and recovery features do you want to include? +- What specific user feedback and support features do you want to include? +- What specific monitoring and logging features do you want to include? +- What specific performance optimization and resource usage minimization techniques do you want to include? +- What specific security features do you want to include? +- What specific user experience and accessibility features do you want to include? diff --git a/bank_account.http b/bank_account.http new file mode 100644 index 0000000..4e5077e --- /dev/null +++ b/bank_account.http @@ -0,0 +1,9 @@ +POST /api/bank-account/update/2 HTTP/1.1 +Content-Type: application/json +Host: europa:3050 +Content-Length: 85 + +{"name": "BofA Joint Checking","bankName": "Bank of America","accountNumber": "4581"} + +### + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..76b8633 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,44 @@ +name: finance-api +services: + postgres: + container_name: postgres_container + image: postgres:15 + environment: + POSTGRES_USER: ${POSTGRES_USER:-financeuser} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-changeme} + POSTGRES_DB: ${POSTGRES_DB:-finance} + PGDATA: /data/postgres + volumes: + - postgres_data:/data/postgres + ports: + - "${POSTGRES_PORT}:5432" + networks: + - postgres + restart: unless-stopped + labels: + - diun.enable=true + + pgadmin: + container_name: pgadmin_container + image: dpage/pgadmin4 + environment: + PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-peter@peterwood.dev} + PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-admin} + PGADMIN_CONFIG_SERVER_MODE: 'False' + volumes: + - pgadmin_data:/var/lib/pgadmin + ports: + - "${PGADMIN_PORT:-5050}:80" + networks: + - postgres + restart: unless-stopped + labels: + - diun.enable=true + +networks: + postgres: + driver: bridge + +volumes: + postgres_data: + pgadmin_data: diff --git a/mcp/README.md b/mcp/README.md new file mode 100644 index 0000000..9bc3b5d --- /dev/null +++ b/mcp/README.md @@ -0,0 +1,141 @@ +# Finance MCP Server + +A Model Control Protocol (MCP) server implementation for financial transaction management. + +## Current Implementation + +The server currently provides a basic REST API for managing financial transactions with the following features: + +- FastAPI-based REST API +- In-memory transaction storage +- Basic CRUD operations for transactions +- Data validation using Pydantic models +- Auto-generated API documentation (Swagger UI) + +### Project Structure + +``` +mcp/ +├── mcp_server.py # Main server implementation +├── requirements.txt # Python dependencies +└── start_server.sh # Server startup script +``` + +## Setup Instructions + +1. Install Python dependencies: + ```bash + pip install -r requirements.txt + ``` + +2. Make the start script executable: + ```bash + chmod +x start_server.sh + ``` + +3. Start the server: + ```bash + ./start_server.sh + ``` + +The server will start on `http://localhost:8000` with the following endpoints: +- `/` - Health check endpoint +- `/docs` - Auto-generated API documentation (Swagger UI) +- `/transactions` - Transaction management endpoints + - GET /transactions - List all transactions + - POST /transactions - Create a new transaction + - GET /transactions/{id} - Get a specific transaction + +## API Usage + +### Create a Transaction +```bash +curl -X POST "http://localhost:8000/transactions" \ + -H "Content-Type: application/json" \ + -d '{ + "amount": 100.00, + "description": "Grocery shopping", + "category": "expenses" + }' +``` + +### List All Transactions +```bash +curl "http://localhost:8000/transactions" +``` + +## Recommended Next Steps + +1. **Database Integration** + - Implement PostgreSQL integration using SQLAlchemy + - Add database migrations + - Create proper data models + - Add transaction persistence + +2. **Authentication & Authorization** + - Implement JWT-based authentication + - Add user management + - Role-based access control + - API key authentication for machine-to-machine communication + +3. **Enhanced Features** + - Add transaction categories management + - Implement transaction search and filtering + - Add date range queries + - Support for different currencies + - Transaction metadata support + +4. **Security Enhancements** + - Input validation and sanitization + - Rate limiting + - CORS configuration + - Request logging and monitoring + - SSL/TLS configuration + +5. **Testing** + - Unit tests for models and endpoints + - Integration tests + - Load testing + - API documentation tests + +6. **Monitoring & Logging** + - Structured logging + - Prometheus metrics + - Health check endpoints + - Error tracking + - Performance monitoring + +7. **DevOps** + - Docker containerization + - CI/CD pipeline + - Environment configuration + - Backup strategy + - Deployment documentation + +8. **API Enhancements** + - Pagination + - Sorting options + - Bulk operations + - Webhook support + - Event streaming + +9. **Documentation** + - API documentation + - Development guide + - Deployment guide + - Contributing guidelines + +10. **Compliance & Standards** + - GDPR compliance + - Financial regulations compliance + - API versioning + - Error handling standards + - Audit logging + +## Contributing + +Please read our contributing guidelines before submitting pull requests. + +## License + +[Add your chosen license here] \ No newline at end of file diff --git a/mcp/mcp-issues.md b/mcp/mcp-issues.md new file mode 100644 index 0000000..dd8964d --- /dev/null +++ b/mcp/mcp-issues.md @@ -0,0 +1,230 @@ +# MCP Server Enhancement Issues + +## Priority 1: Core Infrastructure + +### Issue 1: Database Integration with Existing PostgreSQL Setup +**Labels**: `enhancement`, `database`, `priority-high` + +#### Description +Currently, the MCP server uses in-memory storage for transactions. We need to integrate it with the existing PostgreSQL database. + +#### Tasks +- [ ] Add SQLAlchemy to requirements.txt +- [ ] Create database models that align with existing Prisma schema +- [ ] Implement database connection handling +- [ ] Convert in-memory operations to database operations +- [ ] Add database migration system +- [ ] Add connection error handling +- [ ] Add database connection pooling + +#### Technical Details +- Use existing PostgreSQL setup from docker-compose.yml +- Align with existing Prisma schema structure +- Implement proper connection closing and resource cleanup + +#### Dependencies +- SQLAlchemy +- alembic (for migrations) +- psycopg2-binary + +**Estimated time**: 3-4 days + +--- + +### Issue 2: Authentication System Implementation +**Labels**: `enhancement`, `security`, `priority-high` + +#### Description +Implement secure authentication system for the MCP server endpoints. + +#### Tasks +- [ ] Add JWT authentication +- [ ] Create authentication middleware +- [ ] Implement user session management +- [ ] Add API key authentication for machine-to-machine communication +- [ ] Integrate with existing user system from the main application +- [ ] Add rate limiting for authentication attempts +- [ ] Implement secure password handling + +#### Technical Details +- Use Python-JWT for token handling +- Integrate with existing user schema from Prisma +- Implement token refresh mechanism +- Add proper error handling for auth failures + +#### Dependencies +- python-jose[cryptography] +- passlib[bcrypt] +- python-multipart + +#### Security Considerations +- Token expiration +- Secure password hashing +- Protection against brute force attacks + +**Estimated time**: 2-3 days + +--- + +## Priority 2: Feature Enhancements + +### Issue 3: Enhanced Transaction Features +**Labels**: `enhancement`, `feature`, `priority-medium` + +#### Description +Add enhanced features for transaction management and querying. + +#### Tasks +- [ ] Implement transaction categories management +- [ ] Add transaction search functionality + - [ ] Date range filtering + - [ ] Amount range filtering + - [ ] Category filtering + - [ ] Description search +- [ ] Add pagination for transaction listings +- [ ] Implement sorting options +- [ ] Add support for different currencies +- [ ] Implement transaction metadata support + +#### Technical Details +- Add proper indexing for search operations +- Implement efficient pagination +- Add currency conversion support +- Add proper validation for all new fields + +#### API Endpoints to Add +- GET /transactions/search +- GET /categories +- POST /categories +- PUT /transactions/{id}/metadata + +**Estimated time**: 3-4 days + +--- + +## Priority 3: Observability & Quality + +### Issue 4: Monitoring and Logging Setup +**Labels**: `enhancement`, `observability`, `priority-medium` + +#### Description +Implement monitoring and logging systems for better observability. + +#### Tasks +- [ ] Set up structured logging + - [ ] Request logging + - [ ] Error logging + - [ ] Performance metrics +- [ ] Add Prometheus metrics + - [ ] Request counts + - [ ] Response times + - [ ] Error rates +- [ ] Implement detailed health check endpoints +- [ ] Add performance monitoring +- [ ] Set up error tracking + +#### Technical Details +- Use Python logging module with JSON formatter +- Implement OpenTelemetry integration +- Add proper error context collection +- Implement custom metrics for financial operations + +#### Dependencies +- prometheus-client +- opentelemetry-api +- opentelemetry-sdk +- python-json-logger + +**Estimated time**: 2-3 days + +--- + +### Issue 5: Testing Infrastructure +**Labels**: `enhancement`, `testing`, `priority-medium` + +#### Description +Set up complete testing infrastructure for the MCP server. + +#### Tasks +- [ ] Set up unit testing framework +- [ ] Implement integration tests +- [ ] Add API documentation tests +- [ ] Create load testing suite +- [ ] Set up test data fixtures +- [ ] Implement CI pipeline for tests + +#### Test Coverage Areas +- [ ] Transaction operations +- [ ] Authentication +- [ ] Database operations +- [ ] Error handling +- [ ] API endpoints +- [ ] Data validation + +#### Technical Details +- Use pytest for testing +- Implement proper test database handling +- Add mock services for external dependencies +- Set up GitHub Actions for CI + +#### Dependencies +- pytest +- pytest-cov +- pytest-asyncio +- locust (for load testing) + +**Estimated time**: 4-5 days + +--- + +## Priority 4: DevOps & Documentation + +### Issue 6: Docker & Deployment Setup +**Labels**: `enhancement`, `devops`, `priority-low` + +#### Description +Containerize the MCP server and set up deployment infrastructure. + +#### Tasks +- [ ] Create Dockerfile for MCP server +- [ ] Update docker-compose.yml to include MCP service +- [ ] Set up environment configuration +- [ ] Create deployment scripts +- [ ] Implement backup strategy +- [ ] Add health checks for container orchestration + +#### Technical Details +- Multi-stage Docker build +- Environment variable configuration +- Volume management for persistent data +- Container health monitoring + +**Estimated time**: 2-3 days + +--- + +### Issue 7: Documentation Enhancement +**Labels**: `documentation`, `priority-low` + +#### Description +Create comprehensive documentation for the MCP server. + +#### Tasks +- [ ] Create API documentation +- [ ] Write development guide +- [ ] Create deployment guide +- [ ] Add contributing guidelines +- [ ] Document security practices +- [ ] Add troubleshooting guide + +#### Areas to Cover +- Setup instructions +- API endpoints and usage +- Authentication flows +- Database schema +- Configuration options +- Deployment procedures +- Contributing workflow +- Security best practices + +**Estimated time**: 2-3 days \ No newline at end of file diff --git a/mcp/mcp_server.py b/mcp/mcp_server.py new file mode 100644 index 0000000..63ca23f --- /dev/null +++ b/mcp/mcp_server.py @@ -0,0 +1,37 @@ +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel +from datetime import datetime +from typing import List, Optional + +app = FastAPI(title="Finance MCP Server") + +class Transaction(BaseModel): + id: Optional[int] + amount: float + description: str + timestamp: datetime = datetime.now() + category: str + +# In-memory storage (replace with database in production) +transactions: List[Transaction] = [] + +@app.get("/") +async def root(): + return {"status": "running", "service": "Finance MCP Server"} + +@app.get("/transactions") +async def get_transactions(): + return transactions + +@app.post("/transactions") +async def create_transaction(transaction: Transaction): + transaction.id = len(transactions) + 1 + transactions.append(transaction) + return transaction + +@app.get("/transactions/{transaction_id}") +async def get_transaction(transaction_id: int): + for tx in transactions: + if tx.id == transaction_id: + return tx + raise HTTPException(status_code=404, detail="Transaction not found") \ No newline at end of file diff --git a/mcp/requirements.txt b/mcp/requirements.txt new file mode 100644 index 0000000..34d56f8 --- /dev/null +++ b/mcp/requirements.txt @@ -0,0 +1,3 @@ +fastapi==0.68.0 +uvicorn==0.15.0 +pydantic==1.8.2 \ No newline at end of file diff --git a/mcp/start_server.sh b/mcp/start_server.sh new file mode 100644 index 0000000..394c27e --- /dev/null +++ b/mcp/start_server.sh @@ -0,0 +1,2 @@ +#!/bin/bash +uvicorn mcp_server:app --reload --port 8000 \ No newline at end of file diff --git a/overview.md b/overview.md new file mode 100644 index 0000000..13da0c3 --- /dev/null +++ b/overview.md @@ -0,0 +1,413 @@ +Okay, let's set up a skeleton Fastify API project with Prisma to interact with a PostgreSQL database. This structure will provide the requested endpoints. + +**Assumptions:** + +1. You have Node.js and npm (or yarn) installed. +2. You have PostgreSQL installed and running locally. +3. You have a PostgreSQL database created (e.g., `finance_db`). +4. You have a PostgreSQL user with privileges on that database. + +**Project Setup Steps:** + +1. **Create Project Directory & Initialize:** + + ```bash + mkdir finance-api + cd finance-api + npm init -y + ``` + +2. **Install Dependencies:** + + ```bash + # Runtime dependencies + npm install fastify @prisma/client dotenv + + # Development dependencies + npm install --save-dev prisma typescript @types/node ts-node nodemon + # Or if you prefer plain Javascript, skip typescript/ts-node and adjust scripts + ``` + * `fastify`: The web framework. + * `@prisma/client`: The Prisma database client. + * `dotenv`: To load environment variables from a `.env` file. + * `prisma`: The Prisma CLI (for migrations, generation). + * `typescript`, `@types/node`, `ts-node`: For TypeScript support (recommended). + * `nodemon`: To automatically restart the server during development. + +3. **Initialize Prisma:** + ```bash + npx prisma init --datasource-provider postgresql + ``` + * This creates a `prisma` directory with a `schema.prisma` file and a `.env` file. + +4. **Configure `.env`:** + Edit the newly created `.env` file and set your PostgreSQL connection URL: + ```dotenv + # .env + # Replace with your actual database user, password, host, port, and database name + DATABASE_URL="postgresql://YOUR_USER:YOUR_PASSWORD@localhost:5432/finance_db?schema=public" + + # API Server Configuration (Optional, but good practice) + API_HOST=0.0.0.0 + API_PORT=3050 + API_BASE_URL=https://finance.ptrwd.com # Used for documentation/reference, not binding + ``` + +5. **Define Prisma Schema (`prisma/schema.prisma`):** + Define the model for your bank accounts. + + ```prisma + // prisma/schema.prisma + + generator client { + provider = "prisma-client-js" + } + + datasource db { + provider = "postgresql" + url = env("DATABASE_URL") + } + + model BankAccount { + id Int @id @default(autoincrement()) + name String // e.g., "Checking Account", "Savings XYZ" + bankName String // e.g., "Chase", "Wells Fargo" + accountNumber String @unique // Consider encryption in a real app + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("bank_accounts") // Optional: specify table name in snake_case + } + ``` + +6. **Create Initial Database Migration:** + ```bash + npx prisma migrate dev --name init_bank_account + ``` + * This command does two things: + * Creates an SQL migration file in `prisma/migrations`. + * Applies the migration to your database, creating the `bank_accounts` table. + +7. **Generate Prisma Client:** + Ensure the client is generated based on your schema: + ```bash + npx prisma generate + ``` + * This command reads your `schema.prisma` and generates the typed `@prisma/client`. You typically run this after any schema change. + +8. **Create the Fastify Server (`src/server.ts` or `server.js`):** + + ```typescript + // src/server.ts (if using TypeScript) + // If using Javascript, remove type annotations and use require instead of import + + import Fastify, { FastifyInstance, RouteShorthandOptions } from 'fastify'; + import { Server, IncomingMessage, ServerResponse } from 'http'; + import { PrismaClient, BankAccount } from '@prisma/client'; + import dotenv from 'dotenv'; + + // Load environment variables + dotenv.config(); + + const prisma = new PrismaClient(); + const server: FastifyInstance = Fastify({ + logger: true // Enable logging + }); + + // --- Type Definitions for Payloads/Params (Good Practice) --- + interface BankAccountCreateParams { + name: string; + bankName: string; + accountNumber: string; + } + + interface BankAccountUpdateParams { + id: string; + } + interface BankAccountUpdateBody { + name?: string; + bankName?: string; + accountNumber?: string; + } + + interface BankAccountDeleteParams { + id: string; + } + + // --- API Routes --- + const API_PREFIX = '/api/bank-account'; + + // 1. Create Bank Account + server.post<{ Body: BankAccountCreateParams }>( + `${API_PREFIX}/create`, + async (request, reply): Promise => { + try { + const { name, bankName, accountNumber } = request.body; + const newAccount = await prisma.bankAccount.create({ + data: { + name, + bankName, + accountNumber, + }, + }); + reply.code(201); // Resource Created + return newAccount; + } catch (error: any) { + server.log.error(error); + // Basic duplicate check example (Prisma throws P2002) + if (error.code === 'P2002' && error.meta?.target?.includes('accountNumber')) { + reply.code(409); // Conflict + throw new Error(`Bank account with number ${request.body.accountNumber} already exists.`); + } + reply.code(500); + throw new Error('Failed to create bank account.'); + } + } + ); + + // 2. Update Bank Account + server.post<{ Params: BankAccountUpdateParams; Body: BankAccountUpdateBody }>( + `${API_PREFIX}/update/:id`, + async (request, reply): Promise => { + try { + const { id } = request.params; + const updateData = request.body; + + // Ensure ID is a valid number before querying + const accountId = parseInt(id, 10); + if (isNaN(accountId)) { + reply.code(400); + throw new Error('Invalid account ID format.'); + } + + const updatedAccount = await prisma.bankAccount.update({ + where: { id: accountId }, + data: updateData, + }); + return updatedAccount; + } catch (error: any) { + server.log.error(error); + // Handle case where account doesn't exist (Prisma throws P2025) + if (error.code === 'P2025') { + reply.code(404); // Not Found + throw new Error(`Bank account with ID ${request.params.id} not found.`); + } + // Handle potential duplicate account number on update + if (error.code === 'P2002' && error.meta?.target?.includes('accountNumber')) { + reply.code(409); // Conflict + throw new Error(`Bank account with number ${request.body.accountNumber} already exists.`); + } + reply.code(500); + throw new Error('Failed to update bank account.'); + } + } + ); + + // 3. Delete Bank Account + server.delete<{ Params: BankAccountDeleteParams }>( + `${API_PREFIX}/delete/:id`, + async (request, reply): Promise<{ message: string; deletedAccount: BankAccount }> => { + try { + const { id } = request.params; + // Ensure ID is a valid number + const accountId = parseInt(id, 10); + if (isNaN(accountId)) { + reply.code(400); + throw new Error('Invalid account ID format.'); + } + + const deletedAccount = await prisma.bankAccount.delete({ + where: { id: accountId }, + }); + return { message: `Bank account with ID ${id} deleted successfully.`, deletedAccount }; + } catch (error: any) { + server.log.error(error); + // Handle case where account doesn't exist (Prisma throws P2025) + if (error.code === 'P2025') { + reply.code(404); // Not Found + throw new Error(`Bank account with ID ${request.params.id} not found.`); + } + reply.code(500); + throw new Error('Failed to delete bank account.'); + } + } + ); + + // 4. Get All Bank Accounts + server.get( + `${API_PREFIX}/`, // Using trailing slash for consistency, Fastify often handles both + async (request, reply): Promise => { + try { + const accounts = await prisma.bankAccount.findMany({ + orderBy: { // Optional: Order results + createdAt: 'desc' + } + }); + return accounts; + } catch (error: any) { + server.log.error(error); + reply.code(500); + throw new Error('Failed to retrieve bank accounts.'); + } + } + ); + + // Optional: Get Single Bank Account by ID (Often useful) + server.get<{ Params: { id: string } }>( + `${API_PREFIX}/:id`, + async (request, reply): Promise => { + try { + const { id } = request.params; + const accountId = parseInt(id, 10); + if (isNaN(accountId)) { + reply.code(400); + throw new Error('Invalid account ID format.'); + } + + const account = await prisma.bankAccount.findUnique({ + where: { id: accountId }, + }); + + if (!account) { + reply.code(404); + throw new Error(`Bank account with ID ${id} not found.`); + } + return account; + } catch (error: any) { + // Catch errors that might have been thrown before prisma query + if (!reply.sent) { // Check if a reply hasn't already been sent (e.g., by 400 error) + server.log.error(error); + reply.code(500); + throw new Error('Failed to retrieve bank account.'); + } + // If reply already sent, 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 --- + const start = async () => { + try { + const host = process.env.API_HOST || '0.0.0.0'; + const port = parseInt(process.env.API_PORT || '3050', 10); + + await server.listen({ port, host }); + + // Log after listen resolves (Fastify v3+) + // server.log.info(`Server listening at http://${host}:${port}`); + // Note: Fastify logger automatically logs listening address + + } catch (err) { + server.log.error(err); + await prisma.$disconnect(); // Ensure prisma disconnects on startup error too + process.exit(1); + } + }; + + start(); + + // Export prisma instance if needed elsewhere (e.g., tests, scripts) + // export { prisma }; + ``` + +9. **Configure `tsconfig.json` (if using TypeScript):** + Create a `tsconfig.json` file in the root directory: + ```json + // tsconfig.json + { + "compilerOptions": { + "target": "ES2017", // Or newer + "module": "CommonJS", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "sourceMap": true, // Helpful for debugging + "resolveJsonModule": true // Allows importing JSON files + }, + "include": ["src/**/*"], // Only compile files in src + "exclude": ["node_modules", "**/*.spec.ts"] // Exclude tests if you have them + } + ``` + +10. **Add Scripts to `package.json`:** + + ```json + // package.json (add or modify the scripts section) + "scripts": { + "build": "tsc", // Compile TypeScript to JavaScript (if using TS) + "start": "node dist/server.js", // Run compiled JS (if using TS) + "dev": "nodemon --watch src --ext ts --exec ts-node src/server.ts", // Dev mode with TS + // --- OR if using plain JavaScript --- + // "start": "node server.js", + // "dev": "nodemon server.js", + // --- Prisma commands for convenience --- + "prisma:migrate": "prisma migrate dev", + "prisma:generate": "prisma generate", + "prisma:studio": "prisma studio" // GUI to view/edit data + }, + ``` + +11. **Run the Development Server:** + ```bash + npm run dev + ``` + +**How to Test (using `curl`):** + +*(Replace `YOUR_ACCOUNT_ID` with an actual ID after creating an account)* + +* **Create:** + ```bash + curl -X POST -H "Content-Type: application/json" \ + -d '{"name": "My Checking", "bankName": "Local Bank", "accountNumber": "123456789"}' \ + http://localhost:3050/api/bank-account/create + ``` + +* **Get All:** + ```bash + curl http://localhost:3050/api/bank-account/ + ``` + +* **Update (using the ID returned from create):** + ```bash + curl -X POST -H "Content-Type: application/json" \ + -d '{"name": "My Primary Checking"}' \ + http://localhost:3050/api/bank-account/update/YOUR_ACCOUNT_ID + ``` + +* **Delete (using the ID):** + ```bash + curl -X DELETE http://localhost:3050/api/bank-account/delete/YOUR_ACCOUNT_ID + ``` + +* **Get Specific (using the ID):** + ```bash + curl http://localhost:3050/api/bank-account/YOUR_ACCOUNT_ID + ``` + +This skeleton provides the core structure. You can build upon this by adding more robust error handling, input validation (using Fastify's built-in schema validation), authentication/authorization, more complex queries, and organizing routes into separate files/plugins as the application grows. diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..323cd8d --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,21 @@ +// prisma/schema.prisma + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model BankAccount { + id Int @id @default(autoincrement()) + name String // e.g., "Checking Account", "Savings XYZ" + bankName String // e.g., "Chase", "Wells Fargo" + accountNumber String @unique // Consider encryption in a real app + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("bank_accounts") // Optional: specify table name in snake_case +} \ No newline at end of file diff --git a/reset-environment.sh b/reset-environment.sh new file mode 100755 index 0000000..bc814ce --- /dev/null +++ b/reset-environment.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +set -euo pipefail + +echo "Resetting finance development environment..." + +cd /home/acedanger/dev/finance || exit 1 + +npx prisma migrate reset --force || exit 1 + +docker compose down + +rm -f package-lock.json +[ -d dist ] && rm -rf dist || true +[ -d node_modules ] && rm -rf node_modules || true +npm install || exit 1 +npm run build || exit 1 + +docker compose -f docker-compose.yml up -d + +echo "$(date '+%Y-%m-%d %H:%M:%S') - Environment reset complete." diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..8b62898 --- /dev/null +++ b/src/server.ts @@ -0,0 +1,246 @@ +// src/server.ts + +import Fastify, { FastifyInstance } from 'fastify'; +import { Server, IncomingMessage, ServerResponse } from 'http'; +import { PrismaClient, BankAccount } from '@prisma/client'; +import dotenv from 'dotenv'; +import { z } from 'zod'; +import { ZodTypeProvider } from 'fastify-type-provider-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: any) { + server.log.error(error); + if (error.code === 'P2002' && 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: any) { + server.log.error(error); + if (error.code === 'P2025') { // Record to update not found + reply.code(404); + throw new Error(`Bank account with ID ${request.params.id} not found.`); + } + if (error.code === 'P2002' && 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: any) { + server.log.error(error); + if (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: any) { + 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: any) { + // 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 = 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();