mirror of
https://github.com/acedanger/finance.git
synced 2025-12-05 22:50:12 -08:00
Merge branch 'main' of github.com:acedanger/finance
This commit is contained in:
13
.devcontainer/.env.example
Normal file
13
.devcontainer/.env.example
Normal file
@@ -0,0 +1,13 @@
|
||||
# GitHub MCP Server Configuration
|
||||
GITHUB_PERSONAL_ACCESS_TOKEN=your_token_here
|
||||
|
||||
# PostgreSQL Configuration
|
||||
POSTGRES_USER=financeuser
|
||||
POSTGRES_PASSWORD=changeme
|
||||
POSTGRES_DB=finance
|
||||
POSTGRES_PORT=5432
|
||||
|
||||
# pgAdmin Configuration
|
||||
PGADMIN_DEFAULT_EMAIL=peter@peterwood.dev
|
||||
PGADMIN_DEFAULT_PASSWORD=admin
|
||||
PGADMIN_PORT=5050
|
||||
12
.devcontainer/Dockerfile
Normal file
12
.devcontainer/Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
||||
FROM mcr.microsoft.com/devcontainers/typescript-node:1-20-bullseye
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/acedanger/finance
|
||||
LABEL org.opencontainers.image.description="Development container for Finance application with Node.js, TypeScript, and dev tools"
|
||||
|
||||
# Install additional OS packages
|
||||
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
&& apt-get -y install --no-install-recommends git-core \
|
||||
&& apt-get clean -y && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install global npm packages if needed
|
||||
RUN su node -c "npm install -g typescript"
|
||||
115
.devcontainer/build-and-push.ps1
Normal file
115
.devcontainer/build-and-push.ps1
Normal file
@@ -0,0 +1,115 @@
|
||||
# Requires -Version 5.0
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$GitHubUsername
|
||||
)
|
||||
|
||||
# Stop on first error
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# Configuration
|
||||
$ImageName = "finance-devcontainer"
|
||||
$ImageTag = "latest"
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$DockerfilePath = Join-Path $ScriptDir "Dockerfile"
|
||||
$EnvFile = Join-Path $ScriptDir ".env"
|
||||
$FullImageName = "ghcr.io/$GitHubUsername/$ImageName`:$ImageTag"
|
||||
|
||||
# Function to check required tools
|
||||
function Test-RequiredTools {
|
||||
$tools = @("docker", "gh")
|
||||
foreach ($tool in $tools) {
|
||||
if (-not (Get-Command $tool -ErrorAction SilentlyContinue)) {
|
||||
Write-Error "Error: $tool is required but not installed"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Function to load environment variables from .env file
|
||||
function Get-EnvContent {
|
||||
if (Test-Path $EnvFile) {
|
||||
Write-Host "Loading environment from $EnvFile"
|
||||
$envContent = Get-Content $EnvFile
|
||||
foreach ($line in $envContent) {
|
||||
if ($line -match '^GITHUB_PERSONAL_ACCESS_TOKEN=(.*)$') {
|
||||
return $matches[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
Write-Error "Error: GITHUB_PERSONAL_ACCESS_TOKEN not found in $EnvFile"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Verify prerequisites
|
||||
Write-Host "=== Building Development Container ==="
|
||||
Write-Host "Username: $GitHubUsername"
|
||||
Write-Host "Image: $FullImageName"
|
||||
Write-Host "Dockerfile: $DockerfilePath"
|
||||
|
||||
# Check required tools
|
||||
Test-RequiredTools
|
||||
|
||||
# Check GitHub authentication
|
||||
try {
|
||||
gh auth status
|
||||
} catch {
|
||||
Write-Error "Error: Not authenticated with GitHub. Please run 'gh auth login' first"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Get GitHub PAT from .env file
|
||||
$GitHubPAT = Get-EnvContent
|
||||
|
||||
if ([string]::IsNullOrEmpty($GitHubPAT)) {
|
||||
Write-Error "Error: GITHUB_PERSONAL_ACCESS_TOKEN is empty"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "Using PAT: $($GitHubPAT.Substring(0, 4))... (first 4 chars)"
|
||||
|
||||
# Build the image
|
||||
Write-Host "`n=> Building image..."
|
||||
try {
|
||||
docker build -t $FullImageName -f $DockerfilePath $ScriptDir
|
||||
} catch {
|
||||
Write-Error "Error: Docker build failed"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Log in to GitHub Container Registry
|
||||
Write-Host "`n=> Logging into GitHub Container Registry..."
|
||||
$GitHubPAT | docker login ghcr.io -u $GitHubUsername --password-stdin
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Error: Failed to authenticate with GitHub Container Registry"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Push to GitHub Container Registry
|
||||
Write-Host "`n=> Pushing image to GitHub Container Registry..."
|
||||
docker push $FullImageName
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error @"
|
||||
Error: Failed to push image
|
||||
Please check your GitHub PAT has the required permissions:
|
||||
- read:packages
|
||||
- write:packages
|
||||
"@
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "`n=== Success! ==="
|
||||
Write-Host "The development container image has been built and pushed"
|
||||
Write-Host "`nTo use this image, update your devcontainer.json with:"
|
||||
Write-Host @"
|
||||
{
|
||||
"image": "$FullImageName"
|
||||
}
|
||||
"@
|
||||
|
||||
Write-Host "`nNext steps:"
|
||||
Write-Host "1. Update .devcontainer/devcontainer.json with the image reference above"
|
||||
Write-Host "2. Rebuild your development container in VS Code"
|
||||
Write-Host " (Command Palette -> 'Dev Containers: Rebuild Container')"
|
||||
92
.devcontainer/build-and-push.sh
Normal file
92
.devcontainer/build-and-push.sh
Normal file
@@ -0,0 +1,92 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Exit on error, undefined variables, and pipe failures
|
||||
set -euo pipefail
|
||||
|
||||
# Configuration
|
||||
GITHUB_USERNAME=$1
|
||||
IMAGE_NAME="finance-devcontainer"
|
||||
IMAGE_TAG="latest"
|
||||
|
||||
# Load environment variables from .env file if it exists
|
||||
ENV_FILE="$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd)/.env"
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
echo "Loading environment from $ENV_FILE"
|
||||
# Use grep to find the PAT line and extract the value, handling both Unix and Windows line endings
|
||||
GITHUB_PERSONAL_ACCESS_TOKEN=$(grep -a "^GITHUB_PERSONAL_ACCESS_TOKEN=" "$ENV_FILE" | sed 's/^GITHUB_PERSONAL_ACCESS_TOKEN=//' | tr -d '\r')
|
||||
export GITHUB_PERSONAL_ACCESS_TOKEN
|
||||
fi
|
||||
|
||||
# Check for required username argument
|
||||
if [ -z "${GITHUB_USERNAME:-}" ]; then
|
||||
echo "Error: GitHub username is required"
|
||||
echo "Usage: $0 <github_username>"
|
||||
echo "Example: $0 acedanger"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for required tools
|
||||
for cmd in docker gh; do
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
echo "Error: $cmd is required but not installed"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Verify PAT is loaded
|
||||
if [ -z "${GITHUB_PERSONAL_ACCESS_TOKEN:-}" ]; then
|
||||
echo "Error: GITHUB_PERSONAL_ACCESS_TOKEN is not set"
|
||||
echo "Please ensure it is defined in $ENV_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check GitHub authentication
|
||||
if ! gh auth status >/dev/null 2>&1; then
|
||||
echo "Error: Not authenticated with GitHub. Please run 'gh auth login' first"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get absolute path to Dockerfile
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd)"
|
||||
DOCKERFILE_PATH="$SCRIPT_DIR/Dockerfile"
|
||||
|
||||
FULL_IMAGE_NAME="ghcr.io/$GITHUB_USERNAME/$IMAGE_NAME:$IMAGE_TAG"
|
||||
|
||||
echo "=== Building Development Container ==="
|
||||
echo "Username: $GITHUB_USERNAME"
|
||||
echo "Image: $FULL_IMAGE_NAME"
|
||||
echo "Dockerfile: $DOCKERFILE_PATH"
|
||||
echo "Using PAT: ${GITHUB_PERSONAL_ACCESS_TOKEN:0:4}... (first 4 chars)"
|
||||
|
||||
# Build the image
|
||||
echo -e "\n=> Building image..."
|
||||
if ! docker build -t "$FULL_IMAGE_NAME" -f "$DOCKERFILE_PATH" "$SCRIPT_DIR"; then
|
||||
echo "Error: Docker build failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Log in to GitHub Container Registry
|
||||
echo -e "\n=> Logging into GitHub Container Registry..."
|
||||
echo "$GITHUB_PERSONAL_ACCESS_TOKEN" | docker login ghcr.io -u "$GITHUB_USERNAME" --password-stdin
|
||||
|
||||
# Push to GitHub Container Registry
|
||||
echo -e "\n=> Pushing image to GitHub Container Registry..."
|
||||
if ! docker push "$FULL_IMAGE_NAME"; then
|
||||
echo "Error: Failed to push image"
|
||||
echo "Please check your GitHub PAT has the required permissions:"
|
||||
echo " - read:packages"
|
||||
echo " - write:packages"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "\n=== Success! ==="
|
||||
echo "The development container image has been built and pushed"
|
||||
echo -e "\nTo use this image, update your devcontainer.json with:"
|
||||
echo '{
|
||||
"image": "'$FULL_IMAGE_NAME'"
|
||||
}'
|
||||
|
||||
echo -e "\nNext steps:"
|
||||
echo "1. Update .devcontainer/devcontainer.json with the image reference above"
|
||||
echo "2. Rebuild your development container in VS Code"
|
||||
echo " (Command Palette -> 'Dev Containers: Rebuild Container')"
|
||||
@@ -1,8 +1,16 @@
|
||||
{
|
||||
"name": "Finance App Development",
|
||||
"image": "mcr.microsoft.com/devcontainers/typescript-node:1-20-bullseye",
|
||||
"image": "ghcr.io/acedanger/finance-devcontainer:latest",
|
||||
"workspaceFolder": "/workspaces/finance",
|
||||
"workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/finance,type=bind,consistency=cached",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/git:1": {}
|
||||
"ghcr.io/devcontainers/features/git:1": {},
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
|
||||
"ghcr.io/devcontainers/features/github-cli:1": {}
|
||||
},
|
||||
"runArgs": ["--dns", "8.8.8.8", "--dns", "8.8.4.4", "--network=host", "--dns-search=."],
|
||||
"containerEnv": {
|
||||
"HOSTALIASES": "/etc/host.aliases"
|
||||
},
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
@@ -11,27 +19,39 @@
|
||||
"GitHub.copilot",
|
||||
"GitHub.copilot-chat",
|
||||
"ms-vscode.vscode-typescript-next",
|
||||
"bradlc.vscode-tailwindcss"
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"biomejs.biome",
|
||||
"PKief.material-icon-theme",
|
||||
"Gruntfuggly.todo-tree",
|
||||
"humao.rest-client"
|
||||
],
|
||||
"settings": {
|
||||
"editor.formatOnSave": true,
|
||||
"[typescript]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": null,
|
||||
"editor.defaultFormatter": "biomejs.biome",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": true
|
||||
"source.fixAll.biome": "explicit",
|
||||
"source.organizeImports.biome": "explicit"
|
||||
}
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": null,
|
||||
"editor.defaultFormatter": "biomejs.biome",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": true
|
||||
"source.fixAll.biome": "explicit",
|
||||
"source.organizeImports.biome": "explicit"
|
||||
}
|
||||
},
|
||||
"[astro]": {
|
||||
"editor.defaultFormatter": "astro-build.astro-vscode"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[markdown]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"typescript.updateImportsOnFileMove.enabled": "always",
|
||||
"editor.bracketPairColorization.enabled": true,
|
||||
"files.exclude": {
|
||||
@@ -40,12 +60,31 @@
|
||||
"**/node_modules": true,
|
||||
"**/.idea": true
|
||||
},
|
||||
"terminal.integrated.defaultProfile.linux": "bash"
|
||||
"terminal.integrated.defaultProfile.linux": "bash",
|
||||
"mcp.servers.github": {
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
"--env-file",
|
||||
"${containerWorkspaceFolder}/.devcontainer/.env",
|
||||
"ghcr.io/github/github-mcp-server"
|
||||
],
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"forwardPorts": [3000],
|
||||
"postCreateCommand": "npm install && npm run check",
|
||||
"remoteUser": "node",
|
||||
"mounts": ["type=bind,source=${localEnv:HOME}/.ssh,target=/home/node/.ssh,readonly"]
|
||||
"mounts": [
|
||||
"type=bind,source=${localEnv:HOME}${localEnv:USERPROFILE}/.ssh,target=/home/node/.ssh,readonly"
|
||||
],
|
||||
"updateRemoteUserUID": true,
|
||||
"remoteEnv": {
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "${localEnv:GITHUB_PERSONAL_ACCESS_TOKEN}"
|
||||
},
|
||||
"postStartCommand": "gh auth status || gh auth login"
|
||||
}
|
||||
|
||||
85
.github/APP_GOAL.md
vendored
Normal file
85
.github/APP_GOAL.md
vendored
Normal file
@@ -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?
|
||||
26
.github/copilot-instructions.md
vendored
26
.github/copilot-instructions.md
vendored
@@ -10,6 +10,28 @@ This project is a web user interface (UI) for a CRUD (Create, Read, Update, Dele
|
||||
* **Language:** TypeScript, JavaScript (client-side scripts), HTML, CSS
|
||||
* **Styling:** Plain CSS (`src/styles/global.css`)
|
||||
* **Data:** Using Astro's built-in API routes in `src/pages/api/` with a temporary in-memory store (`src/data/store.ts`). **The goal is to eventually replace the in-memory store with a persistent database.**
|
||||
* **Development Environment:** VS Code Dev Container using private Docker image (`ghcr.io/acedanger/finance-devcontainer:latest`)
|
||||
|
||||
## Development Environment
|
||||
|
||||
* **Dev Container:** The project uses a VS Code Dev Container for consistent development environments.
|
||||
* Container configuration in `.devcontainer/devcontainer.json`
|
||||
* Uses a private container image hosted on GitHub Container Registry
|
||||
* Image: `ghcr.io/acedanger/finance-devcontainer:latest`
|
||||
* Includes all necessary development tools and extensions
|
||||
* Configured with GitHub CLI and authentication
|
||||
* Features Docker-in-Docker support for additional container needs
|
||||
* **Container Features:**
|
||||
* Node.js and npm pre-installed
|
||||
* Git and GitHub CLI configured
|
||||
* TypeScript support
|
||||
* VS Code extensions pre-configured
|
||||
* Docker-in-Docker capability
|
||||
* Automatic GitHub authentication
|
||||
* **Authentication:**
|
||||
* Requires GitHub Personal Access Token for private container access
|
||||
* Token should be configured in `.devcontainer/.env`
|
||||
* GitHub CLI authentication handled in post-start command
|
||||
|
||||
## Current State & Key Features
|
||||
|
||||
@@ -36,6 +58,10 @@ This project is a web user interface (UI) for a CRUD (Create, Read, Update, Dele
|
||||
|
||||
## File Structure Overview
|
||||
|
||||
* `.devcontainer/`: Development container configuration
|
||||
* `devcontainer.json`: VS Code Dev Container configuration
|
||||
* `Dockerfile`: Base container definition (for reference)
|
||||
* `.env.example`: Template for container environment variables
|
||||
* `src/components/`: Reusable UI components.
|
||||
* `src/data/`: Data store and persistence layer.
|
||||
* `src/layouts/`: Base page layout(s).
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -27,3 +27,6 @@ pnpm-debug.log*
|
||||
|
||||
# Test coverage
|
||||
coverage/
|
||||
|
||||
# DevContainer environment files
|
||||
.devcontainer/.env
|
||||
|
||||
155
ENVIRONMENT_SETUP.md
Normal file
155
ENVIRONMENT_SETUP.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# Development Environment Setup Guide
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Windows, macOS, or Linux
|
||||
- Git
|
||||
- Docker Desktop
|
||||
- Visual Studio Code with Remote - Containers extension
|
||||
- GitHub CLI
|
||||
|
||||
## Initial Setup
|
||||
|
||||
1. **Clone the Repository**
|
||||
```bash
|
||||
git clone https://github.com/acedanger/finance.git
|
||||
cd finance
|
||||
```
|
||||
|
||||
2. **Create Environment Files**
|
||||
```bash
|
||||
cp .devcontainer/.env.example .devcontainer/.env
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
## GitHub Personal Access Token (PAT)
|
||||
|
||||
1. **Create a new PAT:**
|
||||
- Go to GitHub Settings > Developer settings > Personal access tokens > Tokens (classic)
|
||||
- Click "Generate new token (classic)"
|
||||
- Name: `finance-dev-token`
|
||||
- Set expiration: 90 days (recommended)
|
||||
- Required scopes:
|
||||
- `repo` (Full control of private repositories)
|
||||
- `read:packages` (Download container images)
|
||||
- `write:packages` (Upload container images)
|
||||
- `delete:packages` (Optional: manage container versions)
|
||||
- `workflow` (GitHub Actions integration)
|
||||
|
||||
2. **Configure the PAT:**
|
||||
- Open `.devcontainer/.env`
|
||||
- Set `GITHUB_PERSONAL_ACCESS_TOKEN=your_token_here`
|
||||
|
||||
## Building the Development Container
|
||||
|
||||
### Using VS Code (Recommended)
|
||||
1. Open the project in VS Code
|
||||
2. When prompted, click "Reopen in Container"
|
||||
- Or press F1 and run "Dev Containers: Rebuild and Reopen in Container"
|
||||
|
||||
### Using Command Line
|
||||
1. Install the devcontainer CLI:
|
||||
```bash
|
||||
npm install -g @devcontainers/cli
|
||||
```
|
||||
|
||||
2. Build the container:
|
||||
```bash
|
||||
devcontainer build .
|
||||
```
|
||||
|
||||
3. Start the container:
|
||||
```bash
|
||||
# With post-create command
|
||||
devcontainer up --workspace-folder .
|
||||
|
||||
# Skip post-create command
|
||||
devcontainer up --workspace-folder . --skip-post-create
|
||||
```
|
||||
|
||||
## Building and Pushing the Container Image
|
||||
|
||||
1. Ensure you're in the `.devcontainer` directory
|
||||
|
||||
2. Choose your preferred script:
|
||||
|
||||
**Using PowerShell:**
|
||||
```powershell
|
||||
.\build-and-push.ps1 your_github_username
|
||||
```
|
||||
|
||||
**Using Git Bash/Unix Shell:**
|
||||
```bash
|
||||
chmod +x build-and-push.sh
|
||||
./build-and-push.sh your_github_username
|
||||
```
|
||||
|
||||
Both scripts perform the same functions:
|
||||
- Validate prerequisites (Docker, GitHub CLI)
|
||||
- Load GitHub PAT from `.env` file
|
||||
- Build the container image
|
||||
- Push to GitHub Container Registry
|
||||
- Provide next steps for using the image
|
||||
|
||||
## Environment Files
|
||||
|
||||
The project uses two separate environment files:
|
||||
|
||||
1. `.devcontainer/.env`:
|
||||
- Container-specific configuration
|
||||
- GitHub PAT
|
||||
- PostgreSQL settings
|
||||
- pgAdmin settings
|
||||
|
||||
2. `.env`:
|
||||
- Application-specific configuration
|
||||
- Development server port
|
||||
- API base URL
|
||||
- Node environment
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Container Build Issues
|
||||
- Ensure Docker Desktop is running
|
||||
- Check that your PAT has the correct permissions
|
||||
- Try rebuilding without cache: `devcontainer build . --no-cache`
|
||||
|
||||
### GitHub Authentication Issues
|
||||
- Verify your PAT in `.devcontainer/.env`
|
||||
- Try logging in manually: `gh auth login`
|
||||
- Check GitHub CLI status: `gh auth status`
|
||||
|
||||
### Network Issues
|
||||
- If DNS resolution fails, try using different DNS servers in `devcontainer.json`
|
||||
- Check if GitHub Container Registry (ghcr.io) is accessible
|
||||
- Verify Docker network settings
|
||||
|
||||
### VS Code Issues
|
||||
- Install/update the Remote - Containers extension
|
||||
- Clear VS Code's container cache
|
||||
- Check VS Code's "Dev Containers" output panel for detailed logs
|
||||
|
||||
## Common Operations
|
||||
|
||||
### Rebuilding the Container
|
||||
```bash
|
||||
# Using VS Code
|
||||
F1 -> "Dev Containers: Rebuild Container"
|
||||
|
||||
# Using CLI
|
||||
devcontainer build . --no-cache
|
||||
```
|
||||
|
||||
### Updating the Container Image
|
||||
1. Make changes to `Dockerfile` or `devcontainer.json`
|
||||
2. Run the build script:
|
||||
```bash
|
||||
./build-and-push.sh your_github_username
|
||||
```
|
||||
3. Update image reference in `devcontainer.json`
|
||||
4. Rebuild container in VS Code
|
||||
|
||||
### Starting Development Server
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
58
README.md
58
README.md
@@ -11,4 +11,60 @@ A web application for managing financial transactions across multiple bank accou
|
||||
* **Key Features (Implemented & Planned):** Account switching, transaction listing, adding, editing, and deleting transactions.
|
||||
|
||||
## Logs
|
||||
This app is currently deployed using Cloudflare Pages. The logs can be viewed with the `npx wrangler pages deployment tail --project-name finance` command. T
|
||||
This app is currently deployed using Cloudflare Pages. The logs can be viewed with the `npx wrangler pages deployment tail --project-name finance` command.
|
||||
|
||||
## Development Environment Setup
|
||||
|
||||
For detailed setup instructions, including container building, environment configuration, and troubleshooting, see [ENVIRONMENT_SETUP.md](ENVIRONMENT_SETUP.md).
|
||||
|
||||
### GitHub MCP Server
|
||||
The project uses GitHub's MCP server for development tasks. The server runs in a Docker container and is automatically configured when you open the project in a devcontainer.
|
||||
|
||||
#### Configuration
|
||||
- The MCP server uses GitHub authentication via Personal Access Token
|
||||
- Token is stored securely in `.devcontainer/.env` (not committed to repository)
|
||||
- GitHub CLI is installed in the devcontainer for easier authentication management
|
||||
- Container health monitoring is configured
|
||||
|
||||
#### Usage
|
||||
The MCP server will automatically start when you open the project in a devcontainer. If you need to manually authenticate:
|
||||
1. Run `gh auth login` in the terminal
|
||||
2. Follow the prompts to authenticate with your GitHub account
|
||||
|
||||
### Database Setup
|
||||
|
||||
## Development Container Setup
|
||||
|
||||
### GitHub Personal Access Token
|
||||
Before using the development container, you'll need a GitHub Personal Access Token (PAT) with the following permissions:
|
||||
- `repo` (Full control of private repositories)
|
||||
- `read:packages` (Download container images)
|
||||
- `write:packages` (Upload container images)
|
||||
- `delete:packages` (Optional: manage container versions)
|
||||
- `workflow` (GitHub Actions integration)
|
||||
|
||||
### Using the Development Container
|
||||
|
||||
#### Command Line Interface
|
||||
You can build and start the development container using the devcontainer CLI:
|
||||
|
||||
```bash
|
||||
# Build the container
|
||||
devcontainer build .
|
||||
|
||||
# Start the container (with post-create command)
|
||||
devcontainer up --workspace-folder .
|
||||
|
||||
# Start the container (skip post-create)
|
||||
devcontainer up --workspace-folder . --skip-post-create
|
||||
```
|
||||
|
||||
#### VS Code
|
||||
1. Open the project in VS Code
|
||||
2. Press F1 and run "Dev Containers: Rebuild and Reopen in Container"
|
||||
- To skip post-create steps: Press F1 and run "Dev Containers: Rebuild Container Without Cache"
|
||||
|
||||
### Troubleshooting
|
||||
- If GitHub authentication fails, ensure your PAT is correctly set in `.devcontainer/.env`
|
||||
- For network issues, try rebuilding the container with `--no-cache` option
|
||||
- Check the VS Code "Dev Containers" output panel for detailed logs
|
||||
9
bank_account.http
Normal file
9
bank_account.http
Normal file
@@ -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"}
|
||||
|
||||
###
|
||||
|
||||
44
docker-compose.yml
Normal file
44
docker-compose.yml
Normal file
@@ -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:
|
||||
413
overview.md
Normal file
413
overview.md
Normal file
@@ -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<BankAccount> => {
|
||||
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<BankAccount> => {
|
||||
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<BankAccount[]> => {
|
||||
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<BankAccount> => {
|
||||
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.
|
||||
679
package-lock.json
generated
679
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
21
prisma/schema.prisma
Normal file
21
prisma/schema.prisma
Normal file
@@ -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
|
||||
}
|
||||
21
reset-environment.sh
Executable file
21
reset-environment.sh
Executable file
@@ -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."
|
||||
@@ -5,6 +5,8 @@ import { currentAccountId as currentAccountIdStore, refreshKey } from '../stores
|
||||
import type { Account } from '../types';
|
||||
import { formatCurrency } from '../utils';
|
||||
|
||||
type AccountSummaryProps = {};
|
||||
|
||||
export default function AccountSummary() {
|
||||
const currentAccountId = useStore(currentAccountIdStore);
|
||||
const refreshCounter = useStore(refreshKey);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
currentAccountId as currentAccountIdStore,
|
||||
@@ -9,7 +10,9 @@ import {
|
||||
import type { Transaction } from '../types';
|
||||
import { formatCurrency, formatDate } from '../utils';
|
||||
|
||||
export default function TransactionTable() {
|
||||
type TransactionTableProps = {};
|
||||
|
||||
export default function TransactionTable({}: TransactionTableProps) {
|
||||
const currentAccountId = useStore(currentAccountIdStore);
|
||||
const refreshCounter = useStore(refreshKey);
|
||||
|
||||
|
||||
249
src/server.ts
Normal file
249
src/server.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
// src/server.ts
|
||||
|
||||
import { IncomingMessage, Server, ServerResponse } from 'http';
|
||||
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: 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<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: 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<BankAccount[]> => {
|
||||
// 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<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: 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 = 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();
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { APIContext } from 'astro';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { listAccounts } from '../pages/api/accounts';
|
||||
import { getAccount } from '../pages/api/accounts/[id]';
|
||||
import { listTransactions } from '../pages/api/accounts/[id]/transactions';
|
||||
import { GET as getAccount } from '../pages/api/accounts/[id]/index';
|
||||
import { GET as listTransactions } from '../pages/api/accounts/[id]/transactions/index';
|
||||
import { GET as listAccounts } from '../pages/api/accounts/index';
|
||||
import { createMockAPIContext } from './setup';
|
||||
|
||||
describe('Accounts API', () => {
|
||||
|
||||
@@ -7,12 +7,13 @@
|
||||
// - Add load testing for API endpoints
|
||||
// - Implement test data factories
|
||||
|
||||
import type { APIContext } from 'astro';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { accounts, transactions } from '../data/store';
|
||||
import { createTransaction } from '../pages/api/transactions';
|
||||
import { updateTransaction } from '../pages/api/transactions/[id]';
|
||||
import { DELETE as deleteTransaction } from '../pages/api/transactions/[id]/index';
|
||||
import {
|
||||
DELETE as deleteTransaction,
|
||||
PUT as updateTransaction,
|
||||
} from '../pages/api/transactions/[id]/index';
|
||||
import { POST as createTransaction } from '../pages/api/transactions/index';
|
||||
import type { Transaction } from '../types';
|
||||
import { createMockAPIContext } from './setup';
|
||||
|
||||
@@ -272,7 +273,7 @@ describe('Transactions API', () => {
|
||||
const initialCount = transactions.length;
|
||||
|
||||
const response = await deleteTransaction(
|
||||
createMockAPIContext({ params: { id: '1' } }) as APIContext,
|
||||
createMockAPIContext({ params: { id: '1' } }) as any,
|
||||
);
|
||||
|
||||
expect(response.status).toBe(204);
|
||||
@@ -291,7 +292,7 @@ describe('Transactions API', () => {
|
||||
|
||||
it('should return 404 for non-existent transaction', async () => {
|
||||
const response = await deleteTransaction(
|
||||
createMockAPIContext({ params: { id: '999' } }) as APIContext,
|
||||
createMockAPIContext({ params: { id: '999' } }) as any,
|
||||
);
|
||||
|
||||
const error = await response.json();
|
||||
@@ -312,7 +313,7 @@ describe('Transactions API', () => {
|
||||
transactions.push(testTransaction);
|
||||
|
||||
const response = await deleteTransaction(
|
||||
createMockAPIContext({ params: { id: 'test-delete' } }) as APIContext,
|
||||
createMockAPIContext({ params: { id: 'test-delete' } }) as any,
|
||||
);
|
||||
|
||||
const error = await response.json();
|
||||
|
||||
Reference in New Issue
Block a user