Ship the receipt pipeline (Livin/Grab/BRI), PocketBase spendings, PM2 ecosystem, and a proper .gitignore that excludes secrets, SQLite data, and media dumps. Co-authored-by: Cursor <cursoragent@cursor.com>
103 lines
2.8 KiB
JavaScript
103 lines
2.8 KiB
JavaScript
import { mkdirSync } from "node:fs";
|
|
import { dirname } from "node:path";
|
|
import Database from "better-sqlite3";
|
|
import { SQLITE_PATH } from "./config.js";
|
|
|
|
mkdirSync(dirname(SQLITE_PATH), { recursive: true });
|
|
|
|
const db = new Database(SQLITE_PATH);
|
|
db.pragma("journal_mode = WAL");
|
|
db.pragma("foreign_keys = ON");
|
|
db.pragma("busy_timeout = 5000");
|
|
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
google_sub TEXT NOT NULL UNIQUE,
|
|
email TEXT NOT NULL,
|
|
name TEXT,
|
|
picture TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS oauth_tokens (
|
|
user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
|
access_token TEXT NOT NULL,
|
|
refresh_token TEXT,
|
|
expiry INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS watch_state (
|
|
user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
|
history_id TEXT NOT NULL,
|
|
expiration INTEGER NOT NULL
|
|
);
|
|
`);
|
|
|
|
const getUserByIdStmt = db.prepare(`SELECT * FROM users WHERE id = ?`);
|
|
const getUserByEmailStmt = db.prepare(
|
|
`SELECT * FROM users WHERE LOWER(email) = LOWER(?)`,
|
|
);
|
|
const getTokensStmt = db.prepare(`SELECT * FROM oauth_tokens WHERE user_id = ?`);
|
|
const listUsersWithTokensStmt = db.prepare(`
|
|
SELECT u.*
|
|
FROM users u
|
|
INNER JOIN oauth_tokens t ON t.user_id = u.id
|
|
`);
|
|
const saveTokensStmt = db.prepare(`
|
|
INSERT INTO oauth_tokens (user_id, access_token, refresh_token, expiry)
|
|
VALUES (@userId, @accessToken, @refreshToken, @expiry)
|
|
ON CONFLICT(user_id) DO UPDATE SET
|
|
access_token = excluded.access_token,
|
|
refresh_token = COALESCE(excluded.refresh_token, oauth_tokens.refresh_token),
|
|
expiry = excluded.expiry
|
|
`);
|
|
const getWatchStateStmt = db.prepare(
|
|
`SELECT * FROM watch_state WHERE user_id = ?`,
|
|
);
|
|
const saveWatchStateStmt = db.prepare(`
|
|
INSERT INTO watch_state (user_id, history_id, expiration)
|
|
VALUES (@userId, @historyId, @expiration)
|
|
ON CONFLICT(user_id) DO UPDATE SET
|
|
history_id = excluded.history_id,
|
|
expiration = excluded.expiration
|
|
`);
|
|
|
|
export function getUserById(id) {
|
|
return getUserByIdStmt.get(id) || null;
|
|
}
|
|
|
|
export function getUserByEmail(email) {
|
|
if (!email) return null;
|
|
return getUserByEmailStmt.get(email) || null;
|
|
}
|
|
|
|
export function getTokens(userId) {
|
|
return getTokensStmt.get(userId) || null;
|
|
}
|
|
|
|
export function listUsersWithTokens() {
|
|
return listUsersWithTokensStmt.all();
|
|
}
|
|
|
|
export function saveTokens(userId, { accessToken, refreshToken, expiry }) {
|
|
saveTokensStmt.run({
|
|
userId,
|
|
accessToken,
|
|
refreshToken: refreshToken || null,
|
|
expiry,
|
|
});
|
|
}
|
|
|
|
export function getWatchState(userId) {
|
|
return getWatchStateStmt.get(userId) || null;
|
|
}
|
|
|
|
export function saveWatchState(userId, { historyId, expiration }) {
|
|
saveWatchStateStmt.run({
|
|
userId,
|
|
historyId: String(historyId),
|
|
expiration: Number(expiration),
|
|
});
|
|
}
|