Files
github-stats-extended/apps/backend/src/common/database.js
T
MartinandMarco Pasqualetti d476f8cbe1 extract "core" package, small improvements (#137)
This PR mainly refactors the code by extracting a "core" package which
is then used by the "backend" and "frontend" apps.

Apart from this the PR also contains several smaller changes like fixing
dependabot, upgrading dependencies, aligned "package.json" files, added
tests, ...

addresses step 1 of #32 
closes #136

---------

Co-authored-by: Marco Pasqualetti <marco.pasqualetti@live.com>
2026-04-18 10:37:26 +02:00

262 lines
6.2 KiB
JavaScript

export let pool = null;
if (process.env.POSTGRES_URL) {
const { Pool } = await import("pg");
pool = new Pool({
connectionString: process.env.POSTGRES_URL,
});
}
/**
* Creates all required tables if they do not exist.
*/
async function createAllTables() {
if (!pool) {
return;
}
await pool.query(`
CREATE TABLE IF NOT EXISTS requests (
request TEXT PRIMARY KEY,
requested_at TIMESTAMP NOT NULL DEFAULT now(),
user_requested_at TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS authenticated_users (
user_id TEXT PRIMARY KEY,
access_token TEXT NOT NULL,
user_key TEXT,
private_access BOOLEAN NOT NULL DEFAULT false
);
`);
}
/**
* Stores or updates a request in the database.
*/
export async function storeRequest(req) {
if (!pool) {
return;
}
const isBypass = req.headers && req.headers["x-bypass-store"];
const insertQuery = isBypass
? `
INSERT INTO requests (request, requested_at)
VALUES ($1, NOW())
ON CONFLICT (request)
DO UPDATE SET requested_at = EXCLUDED.requested_at
`
: `
INSERT INTO requests (request, requested_at, user_requested_at)
VALUES ($1, NOW(), NOW())
ON CONFLICT (request)
DO UPDATE SET requested_at = EXCLUDED.requested_at, user_requested_at = EXCLUDED.user_requested_at
`;
try {
await pool.query(insertQuery, [req.url]);
} catch (err) {
// Check for undefined_table error (SQLSTATE 42P01)
if (err.code === "42P01") {
await createAllTables();
// Retry the insert after creating the table
await pool.query(insertQuery, [req.url]);
} else {
throw err; // Re-throw if it's some other error
}
}
}
/**
* Deletes all requests older than the specified time from the database.
*/
export async function deleteOldRequests(interval) {
if (!pool) {
return;
}
const deleteQuery = `
DELETE FROM requests
WHERE user_requested_at < NOW() - INTERVAL '${interval}'
`;
try {
let result = await pool.query(deleteQuery);
console.log(`Deleted ${result.rowCount} old requests.`);
} catch (err) {
if (err.code === "42P01") {
console.log("Error deleting requests, table doesn't exist");
} else {
throw err;
}
}
}
/**
* Fetches all requests which are between minInterval and maxInterval old.
*
* @returns {Promise<string[]>} Array of all requests between minInterval and maxInterval old.
*/
export async function getRecentRequests(minInterval, maxInterval) {
if (!pool) {
return [];
}
const query = `
SELECT request
FROM requests
WHERE requested_at >= NOW() - INTERVAL '${maxInterval}'
AND requested_at < NOW() - INTERVAL '${minInterval}'
ORDER BY requested_at ASC
`;
let rows;
try {
({ rows } = await pool.query(query));
} catch (err) {
if (err.code === "42P01") {
console.log("Error fetching requests, table doesn't exist");
} else {
throw err;
}
}
return rows.map((row) => row.request);
}
/**
* Inserts or updates a user in the database.
*
* @param {string} userId GitHub userId (login name)
* @param {string} accessToken GitHub access token
* @param {string|null} userKey Optional user key
* @param {boolean} privateAccess Whether private access was requested
*/
export async function storeUser(userId, accessToken, userKey, privateAccess) {
if (!pool) {
return;
}
const insertQuery = `
INSERT INTO authenticated_users (user_id, access_token, user_key, private_access)
VALUES ($1, $2, $3, $4)
ON CONFLICT (user_id)
DO UPDATE SET
access_token = EXCLUDED.access_token,
user_key = EXCLUDED.user_key,
private_access = EXCLUDED.private_access
`;
try {
await pool.query(insertQuery, [
userId,
accessToken,
userKey,
privateAccess,
]);
} catch (err) {
if (err.code === "42P01") {
await createAllTables();
await pool.query(insertQuery, [
userId,
accessToken,
userKey,
privateAccess,
]);
} else {
throw err;
}
}
}
/**
* Delete a user from the database.
*
* @param userKey user key of the user which is to be deleted.
*/
export async function deleteUser(userKey) {
if (!pool) {
return;
}
const deleteQuery = `
DELETE FROM authenticated_users
WHERE user_key = $1
`;
try {
await pool.query(deleteQuery, [userKey]);
} catch (err) {
if (err.code === "42P01") {
console.log("Error deleting user, table doesn't exist");
} else {
throw err;
}
}
}
/**
* Fetches token and private access status for a given user_key.
*
* @param {string} userKey user key of the user to fetch information for
* @returns {Promise<{token: string, privateAccess: boolean} | null>} token and private access status, or null if user not found
*/
export async function getUserAccessByKey(userKey) {
if (!pool) {
return null;
}
const query = `
SELECT access_token, private_access
FROM authenticated_users
WHERE user_key = $1
LIMIT 1
`;
try {
const { rows } = await pool.query(query, [userKey]);
if (rows.length === 0) {
return null;
}
return {
token: rows[0].access_token,
privateAccess: rows[0].private_access,
};
} catch (err) {
if (err.code === "42P01") {
return null;
} else {
throw err;
}
}
}
/**
* Fetches token and private access status for a given username.
*
* @param {string} userName GitHub username of the user to fetch information for
* @returns {Promise<{token: string, privateAccess: boolean} | null>} token and private access status, or null if user not found
*/
export async function getUserAccessByName(userName) {
if (!pool) {
return null;
}
const query = `
SELECT access_token, private_access
FROM authenticated_users
WHERE user_id = $1
LIMIT 1
`;
try {
const { rows } = await pool.query(query, [userName]);
if (rows.length === 0) {
return null;
}
return {
token: rows[0].access_token,
privateAccess: rows[0].private_access,
};
} catch (err) {
if (err.code === "42P01") {
return null;
} else {
throw err;
}
}
}