Database added

This commit is contained in:
2026-03-20 11:21:04 -04:00
parent 2dec1f1f93
commit 821cf379a9
4 changed files with 263 additions and 28 deletions

31
migrate.js Normal file
View File

@ -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);

147
package-lock.json generated
View File

@ -12,6 +12,7 @@
"cors": "^2.8.6", "cors": "^2.8.6",
"express": "^5.2.1", "express": "^5.2.1",
"lucide-react": "^0.575.0", "lucide-react": "^0.575.0",
"pg": "^8.20.0",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"xlsx": "^0.18.5" "xlsx": "^0.18.5"
@ -3425,6 +3426,95 @@
"url": "https://opencollective.com/express" "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": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@ -3474,6 +3564,45 @@
"node": "^10 || ^12 || >=14" "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": { "node_modules/prelude-ls": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@ -3915,6 +4044,15 @@
"node": ">=0.10.0" "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": { "node_modules/ssf": {
"version": "0.11.2", "version": "0.11.2",
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
@ -4307,6 +4445,15 @@
"node": ">=0.8" "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": { "node_modules/y18n": {
"version": "5.0.8", "version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",

View File

@ -15,6 +15,7 @@
"cors": "^2.8.6", "cors": "^2.8.6",
"express": "^5.2.1", "express": "^5.2.1",
"lucide-react": "^0.575.0", "lucide-react": "^0.575.0",
"pg": "^8.20.0",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"xlsx": "^0.18.5" "xlsx": "^0.18.5"
@ -31,4 +32,4 @@
"nodemon": "^3.1.14", "nodemon": "^3.1.14",
"vite": "^7.3.1" "vite": "^7.3.1"
} }
} }

110
server.js
View File

@ -3,13 +3,35 @@ import cors from 'cors';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import pg from 'pg';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const app = express(); const app = express();
const PORT = 3001; 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(cors());
app.use(express.json()); app.use(express.json());
@ -20,7 +42,7 @@ app.use((req, res, next) => {
next(); next();
}); });
// Helper to read data // Helper to read data (legacy JSON mode)
const readData = () => { const readData = () => {
try { try {
if (!fs.existsSync(DATA_FILE)) { 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) => { const writeData = (data) => {
try { try {
fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2), 'utf8'); fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2), 'utf8');
@ -44,40 +66,72 @@ const writeData = (data) => {
}; };
// GET all quotes // GET all quotes
app.get('/api/quotes', (req, res) => { app.get('/api/quotes', async (req, res) => {
const quotes = readData(); if (pool) {
res.json(quotes); 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 // POST a new or updated quote
app.post('/api/quotes', (req, res) => { app.post('/api/quotes', async (req, res) => {
const newQuote = req.body; const newQuote = req.body;
let quotes = readData(); if (pool) {
try {
const existingIndex = quotes.findIndex(q => q.id === newQuote.id); await pool.query(
if (existingIndex >= 0) { 'INSERT INTO quotes (quote_id, data) VALUES ($1, $2) ON CONFLICT (quote_id) DO UPDATE SET data = $2',
quotes[existingIndex] = newQuote; // Update [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 { } 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 // DELETE a quote
app.delete('/api/quotes/:id', (req, res) => { app.delete('/api/quotes/:id', async (req, res) => {
const idToDelete = parseInt(req.params.id) || req.params.id; // Handle number or string const idToDelete = req.params.id;
let quotes = readData(); if (pool) {
try {
const initialLength = quotes.length; const result = await pool.query('DELETE FROM quotes WHERE quote_id = $1', [String(idToDelete)]);
quotes = quotes.filter(q => q.id != idToDelete); if (result.rowCount > 0) res.status(200).json({ message: "Quote deleted successfully" });
else res.status(404).json({ message: "Quote not found" });
if (quotes.length < initialLength) { } catch (err) {
writeData(quotes); console.error(err);
res.status(200).json({ message: "Quote deleted successfully" }); res.status(500).json({ error: "Failed to delete from database" });
}
} else { } 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', () => { app.listen(PORT, '0.0.0.0', () => {
console.log(`Backend server running on port ${PORT}`); 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.`);
}); });