Clone
import { NotFoundError, ValidationError } from '../utils/errors';
export class Database {
constructor(db) {
this.db = db;
}
// User operations
async createUser(id, email) {
const stmt = this.db.prepare(
'INSERT INTO users (id, email) VALUES (?, ?)'
);
return await stmt.bind(id, email).run();
}
async getUserByEmail(email) {
const stmt = this.db.prepare('SELECT * FROM users WHERE email = ?');
return await stmt.bind(email).first();
}
async getUserById(id) {
const stmt = this.db.prepare('SELECT * FROM users WHERE id = ?');
return await stmt.bind(id).first();
}
// Authorization operations
async isEmailAuthorized(email) {
const stmt = this.db.prepare(
'SELECT * FROM authorized_emails WHERE email = ? AND authorized = true'
);
const result = await stmt.bind(email).first();
return !!result;
}
async addAuthorizedEmail(email) {
const stmt = this.db.prepare(
'INSERT OR REPLACE INTO authorized_emails (email) VALUES (?)'
);
return await stmt.bind(email).run();
}
// Entry operations
async createEntry(data) {
const { id, userId, name, originalUrl, slug, logoUrl } = data;
const stmt = this.db.prepare(
`INSERT INTO entries (id, user_id, name, original_url, slug, logo_url)
VALUES (?, ?, ?, ?, ?, ?)`
);
return await stmt.bind(id, userId, name, originalUrl, slug, logoUrl).run();
}
async getEntryBySlug(slug) {
const stmt = this.db.prepare('SELECT * FROM entries WHERE slug = ?');
return await stmt.bind(slug).first();
}
async getEntryById(id) {
const stmt = this.db.prepare('SELECT * FROM entries WHERE id = ?');
return await stmt.bind(id).first();
}
async getUserEntries(userId, limit = 50, offset = 0) {
const stmt = this.db.prepare(
`SELECT * FROM entries
WHERE user_id = ?
ORDER BY created_at DESC
LIMIT ? OFFSET ?`
);
const result = await stmt.bind(userId, limit, offset).all();
return result.results;
}
async updateEntry(id, updates) {
const fields = [];
const values = [];
for (const [key, value] of Object.entries(updates)) {
// Convert camelCase to snake_case
const dbField = key.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
fields.push(`${dbField} = ?`);
values.push(value);
}
values.push(id);
const stmt = this.db.prepare(
`UPDATE entries
SET ${fields.join(', ')}, updated_at = CURRENT_TIMESTAMP
WHERE id = ?`
);
return await stmt.bind(...values).run();
}
async deleteEntry(id) {
const stmt = this.db.prepare('DELETE FROM entries WHERE id = ?');
return await stmt.bind(id).run();
}
async incrementClickCount(slug) {
const stmt = this.db.prepare(
'UPDATE entries SET click_count = click_count + 1 WHERE slug = ?'
);
return await stmt.bind(slug).run();
}
// Analytics operations
async recordAnalytics(data) {
const { entryId, ipHash, userAgent, referer, country, city } = data;
const stmt = this.db.prepare(
`INSERT INTO analytics (entry_id, ip_hash, user_agent, referer, country, city)
VALUES (?, ?, ?, ?, ?, ?)`
);
return await stmt.bind(entryId, ipHash, userAgent, referer, country, city).run();
}
async getEntryAnalytics(entryId, days = 30) {
const stmt = this.db.prepare(
`SELECT
COUNT(*) as total_clicks,
COUNT(DISTINCT ip_hash) as unique_visitors,
DATE(timestamp) as date,
country,
COUNT(*) as clicks
FROM analytics
WHERE entry_id = ?
AND timestamp > datetime('now', '-' || ? || ' days')
GROUP BY DATE(timestamp), country
ORDER BY date DESC`
);
const result = await stmt.bind(entryId, days).all();
return result.results;
}
// Auth token operations
async createAuthToken(token, email, expiresAt) {
const stmt = this.db.prepare(
'INSERT INTO auth_tokens (token, email, expires_at) VALUES (?, ?, ?)'
);
return await stmt.bind(token, email, expiresAt).run();
}
async getAuthToken(token) {
const stmt = this.db.prepare(
`SELECT * FROM auth_tokens
WHERE token = ?
AND expires_at > datetime('now')
AND used = false`
);
return await stmt.bind(token).first();
}
async markTokenUsed(token) {
const stmt = this.db.prepare(
'UPDATE auth_tokens SET used = true WHERE token = ?'
);
return await stmt.bind(token).run();
}
async cleanupExpiredTokens() {
const stmt = this.db.prepare(
'DELETE FROM auth_tokens WHERE expires_at < datetime("now")'
);
return await stmt.run();
}
}