feat: initial codebase v0.4.1
Some checks failed
Deploy to Staging / build-and-deploy (push) Failing after 9m44s
Some checks failed
Deploy to Staging / build-and-deploy (push) Failing after 9m44s
- Extract complete codebase from running staging pod - Add Dockerfile with multi-stage build for Node.js + Puppeteer - Configure CI/CD workflows for staging and production deployment - Include all source files, configs, and public assets
This commit is contained in:
commit
b58f634318
28 changed files with 5669 additions and 0 deletions
100
src/services/db.ts
Normal file
100
src/services/db.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import pg from "pg";
|
||||
import logger from "./logger.js";
|
||||
const { Pool } = pg;
|
||||
|
||||
const TRANSIENT_ERRORS = new Set([
|
||||
"ECONNRESET", "ECONNREFUSED", "EPIPE", "ETIMEDOUT",
|
||||
"57P01", "57P02", "57P03", "08006", "08003", "08001",
|
||||
]);
|
||||
|
||||
const pool = new Pool({
|
||||
host: process.env.DATABASE_HOST || "main-db-pooler.postgres.svc",
|
||||
port: parseInt(process.env.DATABASE_PORT || "5432", 10),
|
||||
database: process.env.DATABASE_NAME || "snapapi",
|
||||
user: process.env.DATABASE_USER || "docfast",
|
||||
password: process.env.DATABASE_PASSWORD || "docfast",
|
||||
max: 10,
|
||||
idleTimeoutMillis: 10000,
|
||||
connectionTimeoutMillis: 5000,
|
||||
keepAlive: true,
|
||||
keepAliveInitialDelayMillis: 10000,
|
||||
});
|
||||
|
||||
pool.on("error", (err) => {
|
||||
logger.error({ err }, "Unexpected error on idle PostgreSQL client");
|
||||
});
|
||||
|
||||
function isTransientError(err: any): boolean {
|
||||
if (!err) return false;
|
||||
const code = err.code || "";
|
||||
const msg = (err.message || "").toLowerCase();
|
||||
if (TRANSIENT_ERRORS.has(code)) return true;
|
||||
if (msg.includes("no available server") || msg.includes("connection terminated") || msg.includes("connection refused")) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function queryWithRetry(text: string, params?: any[], maxRetries = 3): Promise<pg.QueryResult> {
|
||||
let lastError: any;
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
let client: pg.PoolClient | undefined;
|
||||
try {
|
||||
client = await pool.connect();
|
||||
const result = await client.query(text, params);
|
||||
client.release();
|
||||
return result;
|
||||
} catch (err: any) {
|
||||
if (client) try { client.release(true); } catch (_) {}
|
||||
lastError = err;
|
||||
if (!isTransientError(err) || attempt === maxRetries) throw err;
|
||||
const delayMs = Math.min(1000 * Math.pow(2, attempt), 5000);
|
||||
logger.warn({ err: err.message, attempt: attempt + 1 }, "Transient DB error, retrying...");
|
||||
await new Promise(r => setTimeout(r, delayMs));
|
||||
}
|
||||
}
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
export async function connectWithRetry(maxRetries = 3): Promise<pg.PoolClient> {
|
||||
let lastError: any;
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
const client = await pool.connect();
|
||||
await client.query("SELECT 1");
|
||||
return client;
|
||||
} catch (err: any) {
|
||||
lastError = err;
|
||||
if (!isTransientError(err) || attempt === maxRetries) throw err;
|
||||
await new Promise(r => setTimeout(r, Math.min(1000 * Math.pow(2, attempt), 5000)));
|
||||
}
|
||||
}
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
export async function initDatabase(): Promise<void> {
|
||||
const client = await connectWithRetry();
|
||||
try {
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS api_keys (
|
||||
key TEXT PRIMARY KEY,
|
||||
tier TEXT NOT NULL DEFAULT 'free',
|
||||
email TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
stripe_customer_id TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_api_keys_email ON api_keys(email);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS usage (
|
||||
key TEXT NOT NULL,
|
||||
month_key TEXT NOT NULL,
|
||||
count INT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (key, month_key)
|
||||
);
|
||||
`);
|
||||
logger.info("PostgreSQL tables initialized");
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
export { pool };
|
||||
export default pool;
|
||||
Loading…
Add table
Add a link
Reference in a new issue