From 821cf379a99e97e76dad04bd933a6977db2bdb49 Mon Sep 17 00:00:00 2001 From: Todd Date: Fri, 20 Mar 2026 11:21:04 -0400 Subject: [PATCH] Database added --- migrate.js | 31 ++++++++++ package-lock.json | 147 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 3 +- server.js | 110 +++++++++++++++++++++++++--------- 4 files changed, 263 insertions(+), 28 deletions(-) create mode 100644 migrate.js diff --git a/migrate.js b/migrate.js new file mode 100644 index 0000000..de5e1d0 --- /dev/null +++ b/migrate.js @@ -0,0 +1,31 @@ +import fs from 'fs'; +import pg from 'pg'; + +const { Pool } = pg; +const pool = new Pool({ + connectionString: "postgresql://app_user:Al5eVT1SJ7i4UHmSZr1F@204.168.129.249:5432/app_data", + ssl: false +}); + +const quotes = JSON.parse(fs.readFileSync('quotes.json', 'utf8')); + +async function migrate() { + await pool.query(` + CREATE TABLE IF NOT EXISTS quotes ( + id SERIAL PRIMARY KEY, + quote_id VARCHAR(255) UNIQUE NOT NULL, + data JSONB NOT NULL + ) + `); + + for (const q of quotes) { + await pool.query( + 'INSERT INTO quotes (quote_id, data) VALUES ($1, $2) ON CONFLICT (quote_id) DO UPDATE SET data = $2', + [String(q.id), q] + ); + } + console.log(`Successfully migrated ${quotes.length} quotes to your new production database!`); + process.exit(0); +} + +migrate().catch(console.error); diff --git a/package-lock.json b/package-lock.json index 35b71d2..4cc1fe4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "cors": "^2.8.6", "express": "^5.2.1", "lucide-react": "^0.575.0", + "pg": "^8.20.0", "react": "^19.2.0", "react-dom": "^19.2.0", "xlsx": "^0.18.5" @@ -3425,6 +3426,95 @@ "url": "https://opencollective.com/express" } }, + "node_modules/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3474,6 +3564,45 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -3915,6 +4044,15 @@ "node": ">=0.10.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/ssf": { "version": "0.11.2", "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", @@ -4307,6 +4445,15 @@ "node": ">=0.8" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index a3d280d..6c07992 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "cors": "^2.8.6", "express": "^5.2.1", "lucide-react": "^0.575.0", + "pg": "^8.20.0", "react": "^19.2.0", "react-dom": "^19.2.0", "xlsx": "^0.18.5" @@ -31,4 +32,4 @@ "nodemon": "^3.1.14", "vite": "^7.3.1" } -} \ No newline at end of file +} diff --git a/server.js b/server.js index bef06ae..17a03ae 100644 --- a/server.js +++ b/server.js @@ -3,13 +3,35 @@ import cors from 'cors'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; +import pg from 'pg'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const app = express(); const PORT = 3001; -const DATA_FILE = path.join(__dirname, 'quotes.json'); + +// Use DATA_DIR from environment if available (for Dokploy volumes), otherwise fallback to the current directory +const DATA_DIR = process.env.DATA_DIR || __dirname; +const DATA_FILE = path.join(DATA_DIR, 'quotes.json'); + +// Database Setup +const { Pool } = pg; +const pool = process.env.DATABASE_URL ? new Pool({ + connectionString: process.env.DATABASE_URL, + ssl: process.env.PG_SSL === 'true' ? { rejectUnauthorized: false } : false +}) : null; + +if (pool) { + pool.query(` + CREATE TABLE IF NOT EXISTS quotes ( + id SERIAL PRIMARY KEY, + quote_id VARCHAR(255) UNIQUE NOT NULL, + data JSONB NOT NULL + ) + `).then(() => console.log("PostgreSQL Connected & Table Verified")) + .catch(err => console.error("Could not create PostgreSQL table:", err)); +} app.use(cors()); app.use(express.json()); @@ -20,7 +42,7 @@ app.use((req, res, next) => { next(); }); -// Helper to read data +// Helper to read data (legacy JSON mode) const readData = () => { try { if (!fs.existsSync(DATA_FILE)) { @@ -34,7 +56,7 @@ const readData = () => { } }; -// Helper to write data +// Helper to write data (legacy JSON mode) const writeData = (data) => { try { fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2), 'utf8'); @@ -44,40 +66,72 @@ const writeData = (data) => { }; // GET all quotes -app.get('/api/quotes', (req, res) => { - const quotes = readData(); - res.json(quotes); +app.get('/api/quotes', async (req, res) => { + if (pool) { + try { + const result = await pool.query('SELECT data FROM quotes ORDER BY id ASC'); + res.json(result.rows.map(row => row.data)); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Failed to fetch from database" }); + } + } else { + const quotes = readData(); + res.json(quotes); + } }); // POST a new or updated quote -app.post('/api/quotes', (req, res) => { +app.post('/api/quotes', async (req, res) => { const newQuote = req.body; - let quotes = readData(); - - const existingIndex = quotes.findIndex(q => q.id === newQuote.id); - if (existingIndex >= 0) { - quotes[existingIndex] = newQuote; // Update + if (pool) { + try { + await pool.query( + 'INSERT INTO quotes (quote_id, data) VALUES ($1, $2) ON CONFLICT (quote_id) DO UPDATE SET data = $2', + [String(newQuote.id), newQuote] + ); + res.status(201).json(newQuote); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Failed to save to database" }); + } } else { - quotes.push(newQuote); // Add new + let quotes = readData(); + const existingIndex = quotes.findIndex(q => q.id === newQuote.id); + if (existingIndex >= 0) { + quotes[existingIndex] = newQuote; // Update + } else { + quotes.push(newQuote); // Add new + } + writeData(quotes); + res.status(201).json(newQuote); } - - writeData(quotes); - res.status(201).json(newQuote); }); // DELETE a quote -app.delete('/api/quotes/:id', (req, res) => { - const idToDelete = parseInt(req.params.id) || req.params.id; // Handle number or string - let quotes = readData(); - - const initialLength = quotes.length; - quotes = quotes.filter(q => q.id != idToDelete); - - if (quotes.length < initialLength) { - writeData(quotes); - res.status(200).json({ message: "Quote deleted successfully" }); +app.delete('/api/quotes/:id', async (req, res) => { + const idToDelete = req.params.id; + if (pool) { + try { + const result = await pool.query('DELETE FROM quotes WHERE quote_id = $1', [String(idToDelete)]); + if (result.rowCount > 0) res.status(200).json({ message: "Quote deleted successfully" }); + else res.status(404).json({ message: "Quote not found" }); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Failed to delete from database" }); + } } else { - res.status(404).json({ message: "Quote not found" }); + const idToDeleteFallback = parseInt(idToDelete) || idToDelete; // Handle number or string + let quotes = readData(); + const initialLength = quotes.length; + quotes = quotes.filter(q => q.id != idToDeleteFallback); + + if (quotes.length < initialLength) { + writeData(quotes); + res.status(200).json({ message: "Quote deleted successfully" }); + } else { + res.status(404).json({ message: "Quote not found" }); + } } }); @@ -91,4 +145,6 @@ app.get(/(.*)/, (req, res) => { app.listen(PORT, '0.0.0.0', () => { console.log(`Backend server running on port ${PORT}`); + if (pool) console.log(`[INFO] DATABASE_URL detected. Running in PostgreSQL mode.`); + else console.log(`[INFO] No DATABASE_URL detected. Running in legacy JSON file mode.`); });