Database added
This commit is contained in:
31
migrate.js
Normal file
31
migrate.js
Normal 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
147
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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"
|
||||
|
||||
110
server.js
110
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.`);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user