-
Notifications
You must be signed in to change notification settings - Fork 22
Expand file tree
/
Copy pathdb.js
More file actions
148 lines (132 loc) · 5.63 KB
/
Copy pathdb.js
File metadata and controls
148 lines (132 loc) · 5.63 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
// db.js (PostgreSQL persistence layer)
const { Pool } = require('pg');
// Trim env values — pasting into hosting dashboards (Render, Heroku, etc.)
// frequently introduces trailing spaces or newlines that break connections.
const env = (key, fallback) => (process.env[key] || fallback || '').toString().trim();
const dbHost = env('DB_HOST', 'localhost');
// Hosted Postgres (Render, Heroku, Supabase, Neon, RDS…) requires SSL; local
// Postgres usually doesn't. Enable SSL when DB_SSL=true OR when the host is a
// recognised managed provider (so a forgotten DB_SSL doesn't break the deploy).
const sslFlag = env('DB_SSL').toLowerCase() === 'true';
const hostNeedsSsl = /(render\.com|neon\.tech|supabase\.(co|com)|amazonaws\.com|heroku)/i.test(
dbHost
);
const useSsl = sslFlag || hostNeedsSsl;
const pool = new Pool({
user: env('DB_USER', 'your_username'),
host: dbHost,
database: env('DB_NAME', 'your_database'),
password: env('DB_PASSWORD', 'your_password'),
port: Number(env('DB_PORT', '5432')),
ssl: useSsl ? { rejectUnauthorized: false } : false,
});
// ──────────────────────────────────────────────────────────────────────────
// Schema
// ──────────────────────────────────────────────────────────────────────────
async function createTables() {
await pool.query(`
CREATE TABLE IF NOT EXISTS conversations (
id SERIAL PRIMARY KEY,
chatId TEXT NOT NULL,
projectName TEXT NOT NULL,
role TEXT NOT NULL,
content TEXT NOT NULL,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
await pool.query(`
CREATE TABLE IF NOT EXISTS projects (
project_name TEXT PRIMARY KEY,
admin_chat_id BIGINT NOT NULL,
admin_password TEXT,
websites JSONB NOT NULL DEFAULT '[]'::jsonb,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
await pool.query(`
CREATE INDEX IF NOT EXISTS idx_projects_admin_chat_id ON projects(admin_chat_id)
`);
console.log('✅ Database schema ready (conversations + projects)');
}
// Note: createTables() is NOT called at module load — it's exported and
// awaited explicitly from stateManager.init() at startup, BEFORE any code
// tries to read from the projects table. This avoids a race where a SELECT
// could fire before CREATE TABLE finishes on a fresh database.
// ──────────────────────────────────────────────────────────────────────────
// Conversation history (unchanged API)
// ──────────────────────────────────────────────────────────────────────────
function saveMessage(chatId, projectName, role, content) {
return pool
.query(
`INSERT INTO conversations (chatId, projectName, role, content)
VALUES ($1, $2, $3, $4)
RETURNING id`,
[chatId, projectName, role, content]
)
.then((result) => result.rows[0].id);
}
function loadConversationHistory(chatId, projectName) {
return pool
.query(
`SELECT role, content FROM conversations
WHERE chatId = $1 AND projectName = $2
ORDER BY timestamp ASC`,
[chatId, projectName]
)
.then((result) => result.rows);
}
function clearConversationHistory(chatId, projectName) {
return pool
.query(
`DELETE FROM conversations
WHERE chatId = $1 AND projectName = $2`,
[chatId, projectName]
)
.then((result) => result.rowCount);
}
// ──────────────────────────────────────────────────────────────────────────
// Projects (new — replaces in-memory storage)
// ──────────────────────────────────────────────────────────────────────────
async function loadAllProjects() {
const result = await pool.query(`SELECT * FROM projects`);
const map = new Map();
for (const row of result.rows) {
map.set(row.project_name, {
projectName: row.project_name,
adminChatId: Number(row.admin_chat_id),
adminPassword: row.admin_password,
websites: row.websites || [],
});
}
return map;
}
async function upsertProject(name, project) {
await pool.query(
`INSERT INTO projects (project_name, admin_chat_id, admin_password, websites, updated_at)
VALUES ($1, $2, $3, $4::jsonb, CURRENT_TIMESTAMP)
ON CONFLICT (project_name) DO UPDATE SET
admin_chat_id = EXCLUDED.admin_chat_id,
admin_password = EXCLUDED.admin_password,
websites = EXCLUDED.websites,
updated_at = CURRENT_TIMESTAMP`,
[name, project.adminChatId, project.adminPassword, JSON.stringify(project.websites || [])]
);
}
async function deleteProjectRow(name) {
await pool.query(`DELETE FROM projects WHERE project_name = $1`, [name]);
}
module.exports = {
// schema
createTables,
// conversations
saveMessage,
loadConversationHistory,
clearConversationHistory,
// projects
loadAllProjects,
upsertProject,
deleteProjectRow,
// raw pool (for graceful shutdown)
pool,
};