postgreSQL integration
This commit is contained in:
parent
e61ddaa8b0
commit
057c2ca5d1
7 changed files with 251 additions and 59 deletions
|
|
@ -2,7 +2,11 @@
|
|||
PORT=3000
|
||||
NODE_ENV=production
|
||||
|
||||
# Database & Data Persistence Path
|
||||
# PostgreSQL Connection String (Coolify / Managed Postgres / Docker)
|
||||
DATABASE_URL=postgresql://postgres:postgres_secure_pass_2026@postgres:5432/balance_sheet_db
|
||||
POSTGRES_PASSWORD=postgres_secure_pass_2026
|
||||
|
||||
# Fallback File Storage Path (Used if DATABASE_URL is omitted or offline)
|
||||
DATA_PATH=/app/data/portal-data.json
|
||||
|
||||
# Optional SMTP Email Configuration (If using real SMTP server)
|
||||
|
|
|
|||
80
COOLIFY.md
80
COOLIFY.md
|
|
@ -1,69 +1,53 @@
|
|||
# Deployment Guide for Coolify
|
||||
# Coolify & PostgreSQL Deployment Guide
|
||||
|
||||
This repository is optimized for deployment on **Coolify** using Docker or Docker Compose.
|
||||
This application consists of:
|
||||
1. **Server Side**: Node.js + Express backend (`server.ts`) serving REST APIs, managing authentication, ledger reconciliation, audit logs, FX rates, and financial reports.
|
||||
2. **Database**: PostgreSQL (via `pg` connection pool) with automatic JSON fall-back backup.
|
||||
3. **Frontend**: React SPA served directly via Express in production.
|
||||
|
||||
---
|
||||
|
||||
## Quick Deployment Options in Coolify
|
||||
## 1. Quick Coolify Deployment Options
|
||||
|
||||
### Option A: Deployment via Git Repository (Recommended)
|
||||
### Option A: Docker Compose Deployment (Application + PostgreSQL in One Stack)
|
||||
|
||||
1. **Push Code to Git**: Push this repository to GitHub, GitLab, or your self-hosted Git service.
|
||||
2. **Add New Resource in Coolify**:
|
||||
- Go to your Coolify dashboard.
|
||||
- Click **+ Add Resource** -> **Public Repository** or **Private Repository**.
|
||||
- Paste your repository URL and select the `main` branch.
|
||||
3. **Select Build Pack**:
|
||||
- Select **Dockerfile**. Coolify will automatically detect the `Dockerfile` in the root directory.
|
||||
4. **Configure Port & Network**:
|
||||
- **Port**: Set `3000`.
|
||||
5. **Configure Persistent Volume**:
|
||||
- Under **Storage / Volumes**, add a persistent volume mapping to prevent data loss on container redeployments:
|
||||
- **Destination Path**: `/app/data`
|
||||
6. **Deploy**:
|
||||
- Click **Deploy**. Coolify will build the multi-stage Docker image and start your application.
|
||||
|
||||
---
|
||||
|
||||
### Option B: Deployment via Docker Compose
|
||||
|
||||
1. **Add New Resource in Coolify**:
|
||||
1. **In Coolify Dashboard**:
|
||||
- Click **+ Add Resource** -> **Docker Compose**.
|
||||
2. **Source Code**:
|
||||
- Select your Git Repository or paste the contents of `docker-compose.yml`.
|
||||
3. **Environment Variables**:
|
||||
- Add the following environment variables in Coolify UI:
|
||||
- Point to your Git Repository or paste the contents of `docker-compose.yml`.
|
||||
2. **Environment Variables**:
|
||||
Add the following in Coolify:
|
||||
```env
|
||||
NODE_ENV=production
|
||||
PORT=3000
|
||||
DATA_PATH=/app/data/portal-data.json
|
||||
POSTGRES_PASSWORD=your_custom_secure_password
|
||||
```
|
||||
4. **Deploy**:
|
||||
- Click **Deploy**. Coolify will spin up the container and attach the `portal_data` volume automatically.
|
||||
3. **Deploy**:
|
||||
- Coolify will build the app container and spin up a dedicated `postgres:16-alpine` database container with health checks and persistent volume storage (`balance_sheet_postgres_data`).
|
||||
|
||||
---
|
||||
|
||||
## Local Testing with Docker
|
||||
### Option B: Coolify Managed PostgreSQL + Standalone App Container
|
||||
|
||||
To build and test locally before deploying to Coolify:
|
||||
If you prefer using Coolify's built-in managed PostgreSQL database resource:
|
||||
|
||||
### Using Docker Compose:
|
||||
```bash
|
||||
docker-compose up --build -d
|
||||
```
|
||||
Access the app at: `http://localhost:3000`
|
||||
|
||||
### Using Docker directly:
|
||||
```bash
|
||||
docker build -t balance-sheet-portal .
|
||||
docker run -p 3000:3000 -v portal_data:/app/data balance-sheet-portal
|
||||
1. **Create Database in Coolify**:
|
||||
- Click **+ Add Resource** -> **PostgreSQL**.
|
||||
- Coolify will provide an internal database connection URL (e.g., `postgresql://postgres:pass@coolify-postgres-host:5432/balance_sheet_db`).
|
||||
2. **Deploy App Service**:
|
||||
- Add your Git repo as a **Dockerfile** application.
|
||||
- Set environment variable in Coolify UI:
|
||||
```env
|
||||
DATABASE_URL=postgresql://postgres:pass@coolify-postgres-host:5432/balance_sheet_db
|
||||
PORT=3000
|
||||
```
|
||||
3. **Deploy**:
|
||||
- The Express server will automatically connect to the PostgreSQL database, create the necessary table structures on boot, and store all portal state in PostgreSQL.
|
||||
|
||||
---
|
||||
|
||||
## Health Check Endpoint
|
||||
## 2. Server-Side Architecture Details
|
||||
|
||||
The container includes a built-in health check targeting:
|
||||
`GET /api/health`
|
||||
|
||||
It returns `{"status": "ok"}` when the Express server and Vite static engine are healthy.
|
||||
- **Automatic Table Creation**: On server boot, the backend runs `initPgDatabase()` to verify/create `app_portal_state`.
|
||||
- **Automatic State Recovery**: If PostgreSQL contains existing data, the portal state is loaded directly from PostgreSQL. If empty, baseline seed data is written to PostgreSQL.
|
||||
- **Resilient Fallback**: If `DATABASE_URL` is omitted, the app gracefully operates using persistent JSON storage (`/app/data/portal-data.json`).
|
||||
- **Health Check**: Express exposes `GET /api/health` for Docker container monitoring.
|
||||
|
|
|
|||
32
bun.lock
32
bun.lock
|
|
@ -7,6 +7,7 @@
|
|||
"dependencies": {
|
||||
"@google/genai": "^2.4.0",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@types/pg": "^8.20.4",
|
||||
"@types/three": "^0.185.4",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"dotenv": "^17.2.3",
|
||||
|
|
@ -14,6 +15,7 @@
|
|||
"lucide-react": "^0.546.0",
|
||||
"motion": "^12.23.24",
|
||||
"nodemailer": "^9.0.4",
|
||||
"pg": "^8.22.0",
|
||||
"react": "^19.0.1",
|
||||
"react-dom": "^19.0.1",
|
||||
"recharts": "^3.10.1",
|
||||
|
|
@ -292,6 +294,8 @@
|
|||
|
||||
"@types/nodemailer": ["@types/nodemailer@8.0.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-PxpaInm8V1JQDd4j0ds5HfvWQk8JupS1C0Picb96QJsrrRDjBH+DlK7L4ZdNSqNULhiZRQHc40nLVShaGxXAMw=="],
|
||||
|
||||
"@types/pg": ["@types/pg@8.20.4", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-Jz7UDOlIiFJuacC0TlBoLyNtmwlA/wpIyPDd3tvUqlRM+HzkWy2xUgpFpaXtbfTAFF6sIGq5lsCDBdJnhky1Xg=="],
|
||||
|
||||
"@types/qs": ["@types/qs@6.15.1", "", {}, "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw=="],
|
||||
|
||||
"@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
|
||||
|
|
@ -572,6 +576,22 @@
|
|||
|
||||
"path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="],
|
||||
|
||||
"pg": ["pg@8.22.0", "", { "dependencies": { "pg-connection-string": "^2.14.0", "pg-pool": "^3.14.0", "pg-protocol": "^1.15.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA=="],
|
||||
|
||||
"pg-cloudflare": ["pg-cloudflare@1.4.0", "", {}, "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A=="],
|
||||
|
||||
"pg-connection-string": ["pg-connection-string@2.14.0", "", {}, "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg=="],
|
||||
|
||||
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
|
||||
|
||||
"pg-pool": ["pg-pool@3.14.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw=="],
|
||||
|
||||
"pg-protocol": ["pg-protocol@1.15.0", "", {}, "sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ=="],
|
||||
|
||||
"pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
|
||||
|
||||
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
|
||||
|
|
@ -580,6 +600,14 @@
|
|||
|
||||
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
||||
|
||||
"postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
|
||||
|
||||
"postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="],
|
||||
|
||||
"postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
|
||||
|
||||
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
|
||||
|
||||
"protobufjs": ["protobufjs@7.6.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw=="],
|
||||
|
||||
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||
|
|
@ -636,6 +664,8 @@
|
|||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
|
||||
|
||||
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="],
|
||||
|
|
@ -678,6 +708,8 @@
|
|||
|
||||
"ws": ["ws@8.21.2", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw=="],
|
||||
|
||||
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
|
||||
|
||||
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||
|
||||
"@babel/core/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
|
|
|||
|
|
@ -1,6 +1,26 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
# PostgreSQL Database Service
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: balance-sheet-postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: balance_sheet_db
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres_secure_pass_2026}
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_db_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres -d balance_sheet_db"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# Express + React Application Web Service
|
||||
balance-sheet-portal:
|
||||
build:
|
||||
context: .
|
||||
|
|
@ -8,11 +28,15 @@ services:
|
|||
image: balance-sheet-portal:latest
|
||||
container_name: balance-sheet-portal
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- "3000"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "${PORT:-3000}:3000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3000
|
||||
- DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD:-postgres_secure_pass_2026}@postgres:5432/balance_sheet_db
|
||||
- DATA_PATH=/app/data/portal-data.json
|
||||
volumes:
|
||||
- portal_data:/app/data
|
||||
|
|
@ -27,3 +51,6 @@ volumes:
|
|||
portal_data:
|
||||
name: balance_sheet_portal_data
|
||||
driver: local
|
||||
postgres_db_data:
|
||||
name: balance_sheet_postgres_data
|
||||
driver: local
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
"dependencies": {
|
||||
"@google/genai": "^2.4.0",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@types/pg": "^8.20.4",
|
||||
"@types/three": "^0.185.4",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"dotenv": "^17.2.3",
|
||||
|
|
@ -21,6 +22,7 @@
|
|||
"lucide-react": "^0.546.0",
|
||||
"motion": "^12.23.24",
|
||||
"nodemailer": "^9.0.4",
|
||||
"pg": "^8.22.0",
|
||||
"react": "^19.0.1",
|
||||
"react-dom": "^19.0.1",
|
||||
"recharts": "^3.10.1",
|
||||
|
|
|
|||
23
server.ts
23
server.ts
|
|
@ -38,6 +38,7 @@ import {
|
|||
MakerCheckerStatus
|
||||
} from './src/types.js';
|
||||
import { AccountingEngine, DEFAULT_FX_RATES } from './src/services/accountingEngine.js';
|
||||
import { initPgDatabase, loadStateFromPg, saveStateToPg } from './src/db/postgres.js';
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
|
||||
|
|
@ -45,7 +46,7 @@ const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
|
|||
app.use(express.json());
|
||||
|
||||
// In-Memory Database / Persistent File Storage
|
||||
interface DB {
|
||||
export interface DB {
|
||||
users: User[];
|
||||
submissions: Record<string, Record<BranchId, BranchSubmission>>; // period -> branchId -> submission
|
||||
smtpConfig: SmtpConfig;
|
||||
|
|
@ -186,10 +187,30 @@ function saveDB(data: DB) {
|
|||
} catch (err) {
|
||||
console.error('Error saving portal-data.json:', err);
|
||||
}
|
||||
|
||||
// Asynchronously push state to PostgreSQL if configured
|
||||
saveStateToPg(data).catch((err) => {
|
||||
console.error('Error syncing state to PostgreSQL:', err);
|
||||
});
|
||||
}
|
||||
|
||||
let db = loadDB();
|
||||
|
||||
// Initialize PostgreSQL if DATABASE_URL or POSTGRES_HOST is present
|
||||
(async () => {
|
||||
const pgReady = await initPgDatabase();
|
||||
if (pgReady) {
|
||||
const pgState = await loadStateFromPg();
|
||||
if (pgState) {
|
||||
db = pgState;
|
||||
console.log('[PostgreSQL] DB state successfully synced from PostgreSQL database.');
|
||||
} else {
|
||||
console.log('[PostgreSQL] Initializing PostgreSQL database with baseline state...');
|
||||
await saveStateToPg(db);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// AUTH ENDPOINTS
|
||||
// -------------------------------------------------------------
|
||||
|
|
|
|||
122
src/db/postgres.ts
Normal file
122
src/db/postgres.ts
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
import pg from 'pg';
|
||||
import type { DB } from '../../server';
|
||||
|
||||
const { Pool } = pg;
|
||||
|
||||
let pool: pg.Pool | null = null;
|
||||
|
||||
export function getPgPool(): pg.Pool | null {
|
||||
if (pool) return pool;
|
||||
|
||||
const connectionString =
|
||||
process.env.DATABASE_URL ||
|
||||
process.env.POSTGRES_URL ||
|
||||
process.env.DATABASE_PRIVATE_URL;
|
||||
|
||||
const host = process.env.POSTGRES_HOST || process.env.SQL_HOST;
|
||||
const user = process.env.POSTGRES_USER || process.env.SQL_USER;
|
||||
const password = process.env.POSTGRES_PASSWORD || process.env.SQL_PASSWORD;
|
||||
const database = process.env.POSTGRES_DB || process.env.SQL_DB_NAME || 'balance_sheet_db';
|
||||
const port = parseInt(process.env.POSTGRES_PORT || '5432', 10);
|
||||
|
||||
if (connectionString) {
|
||||
console.log('[PostgreSQL] Initializing connection pool via DATABASE_URL');
|
||||
pool = new Pool({
|
||||
connectionString,
|
||||
max: 10,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 5000,
|
||||
});
|
||||
} else if (host && user) {
|
||||
console.log(`[PostgreSQL] Initializing connection pool via host ${host}:${port}, db: ${database}`);
|
||||
pool = new Pool({
|
||||
host,
|
||||
port,
|
||||
user,
|
||||
password,
|
||||
database,
|
||||
max: 10,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 5000,
|
||||
});
|
||||
} else {
|
||||
console.log('[PostgreSQL] No DATABASE_URL or POSTGRES_HOST provided. Operating in file-backed mode.');
|
||||
return null;
|
||||
}
|
||||
|
||||
pool.on('error', (err) => {
|
||||
console.error('[PostgreSQL] Unexpected idle client error:', err);
|
||||
});
|
||||
|
||||
return pool;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes PostgreSQL schema table if needed
|
||||
*/
|
||||
export async function initPgDatabase(): Promise<boolean> {
|
||||
const p = getPgPool();
|
||||
if (!p) return false;
|
||||
|
||||
try {
|
||||
const client = await p.connect();
|
||||
try {
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS app_portal_state (
|
||||
id VARCHAR(50) PRIMARY KEY,
|
||||
data JSONB NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
console.log('[PostgreSQL] Database table app_portal_state ensured.');
|
||||
return true;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[PostgreSQL] Initialization failed:', err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load state from PostgreSQL
|
||||
*/
|
||||
export async function loadStateFromPg(): Promise<DB | null> {
|
||||
const p = getPgPool();
|
||||
if (!p) return null;
|
||||
|
||||
try {
|
||||
const res = await p.query(`SELECT data FROM app_portal_state WHERE id = $1`, ['main_state']);
|
||||
if (res.rows.length > 0 && res.rows[0].data) {
|
||||
console.log('[PostgreSQL] Successfully restored portal state from PostgreSQL database.');
|
||||
return res.rows[0].data as DB;
|
||||
}
|
||||
return null;
|
||||
} catch (err) {
|
||||
console.error('[PostgreSQL] Error loading state:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save state to PostgreSQL asynchronously
|
||||
*/
|
||||
export async function saveStateToPg(state: DB): Promise<void> {
|
||||
const p = getPgPool();
|
||||
if (!p) return;
|
||||
|
||||
try {
|
||||
await p.query(
|
||||
`
|
||||
INSERT INTO app_portal_state (id, data, updated_at)
|
||||
VALUES ($1, $2, NOW())
|
||||
ON CONFLICT (id) DO UPDATE
|
||||
SET data = EXCLUDED.data, updated_at = NOW();
|
||||
`,
|
||||
['main_state', JSON.stringify(state)]
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('[PostgreSQL] Error saving state:', err);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue