From 28e280556e142b2c871dcc0f9234d25b76ce40f5 Mon Sep 17 00:00:00 2001 From: sloppyjosh Date: Thu, 7 Mar 2024 01:52:44 -0500 Subject: [PATCH] updated database service to static method --- src/db/services/database.ts | 81 ++++++++++++++++--------- src/db/services/databaseMaintenance.ts | 14 +---- src/index.ts | 12 ++-- src/rdrama/services/CommentProcessor.ts | 8 +-- src/reddit/session/SessionManager.ts | 12 ++-- src/utils/ShouldNotify.ts | 4 +- src/workflows/WorkflowOrchestrator.ts | 9 ++- 7 files changed, 76 insertions(+), 64 deletions(-) diff --git a/src/db/services/database.ts b/src/db/services/database.ts index 8cf346c..79cf407 100644 --- a/src/db/services/database.ts +++ b/src/db/services/database.ts @@ -1,19 +1,26 @@ import { Database } from 'sqlite'; import { Comment } from '../../rdrama/models/Comment'; +import { DatabaseInitializer } from '../initializeDatabase'; /** * Service for interacting with the SQLite database for operations related to comments and user mentions. */ export class DatabaseService { - private db: Database; - /** - * Creates a new DatabaseService instance with a provided database connection. + * Retrieves the singleton instance of the database. + * This method ensures that a single database instance is used throughout the application, + * following the singleton pattern for managing database connections. * - * @param {Database} db - The SQLite database connection. + * @returns A promise that resolves to the initialized database instance. + * @throws Will throw an error if the database cannot be initialized. */ - public constructor(db: Database) { - this.db = db; + private static async getDatabase(): Promise { + const databaseInitializer = DatabaseInitializer.getInstance(); + const db = await databaseInitializer.getDbInstance() + if (!db) { + throw new Error('Failed to initialize the database.'); + } + return db } /** @@ -24,7 +31,8 @@ export class DatabaseService { * * @param {Comment} comment - The comment object to insert. */ - public async insertComment(comment: Comment): Promise { + public static async insertComment(comment: Comment): Promise { + const db = await DatabaseService.getDatabase() const sql = ` INSERT INTO comments ( id, author_id, author_name, body, body_html, created_utc, deleted_utc, @@ -36,7 +44,7 @@ export class DatabaseService { ?, ?, ?, ?, ?, ?, ? ) `; - await this.db.run(sql, [ + await db.run(sql, [ comment.id, comment.author_id, comment.author_name, comment.body, comment.body_html, comment.created_utc, comment.deleted_utc, comment.distinguished ? 1 : 0, comment.downvotes, comment.edited_utc, comment.is_banned ? 1 : 0, comment.is_bot ? 1 : 0, comment.is_nsfw ? 1 : 0, comment.level, comment.permalink, comment.pinned, comment.post_id, JSON.stringify(comment.replies), JSON.stringify(comment.reports), comment.score, comment.upvotes @@ -45,23 +53,35 @@ export class DatabaseService { /** * Inserts a new user mention into the database. + * This method is static and can be called without creating an instance of the class. + * It uses `getDatabase` method internally to ensure that database operations + * are performed on the same database instance throughout the application. * - * @param {Object} mention - The user mention object to insert, containing rdrama_comment_id, username, and optionally message. + * @param mention - The user mention object to insert. It must include: + * - rdrama_comment_id: The ID of the comment from the r/Drama platform. + * - username: The mentioned Reddit username in a standardized format (e.g., u/username). + * - message: (Optional) The content of the message sent to the mentioned user. + * + * @throws Will throw an error if the database cannot be initialized or the insert operation fails. */ - public async insertUserMention(mention: { rdrama_comment_id: number; username: string; message?: string }): Promise { + public static async insertUserMention(mention: { rdrama_comment_id: number; username: string; message?: string }): Promise { + const db = await DatabaseService.getDatabase() const sql = `INSERT INTO user_mentions (rdrama_comment_id, username, message) VALUES (?, ?, ?)`; - await this.db.run(sql, [mention.rdrama_comment_id, mention.username, mention.message]); + await db.run(sql, [mention.rdrama_comment_id, mention.username, mention.message]); } + + /** * Queries the database for an existing comment. * * @param {string} commentId - The ID of the comment to search for. * @returns {Promise} A boolean indicating whether the comment exists. */ - public async commentExists(commentId: string): Promise { + public static async commentExists(commentId: string): Promise { + const db = await DatabaseService.getDatabase() const sql = `SELECT 1 FROM comments WHERE id = ?`; - const result = await this.db.get(sql, [commentId]); + const result = await db.get(sql, [commentId]); return !!result; } @@ -71,9 +91,10 @@ export class DatabaseService { * @param {string} username - The username to search for. * @returns {Promise} A boolean indicating whether the username has been mentioned. */ - public async userMentionExists(username: string): Promise { + public static async userMentionExists(username: string): Promise { + const db = await DatabaseService.getDatabase() const sql = `SELECT 1 FROM user_mentions WHERE username = ?`; - const result = await this.db.get(sql, [username]); + const result = await db.get(sql, [username]); return !!result; } @@ -84,8 +105,9 @@ export class DatabaseService { * @param {string} taskName - The name of the maintenance task for which to retrieve the last run timestamp. * @returns {Promise} A promise that resolves to the Date of the last run if found, or null if not found. */ - public async getLastRunTimestamp(taskName: string): Promise { - const result = await this.db.get(`SELECT last_run FROM maintenance_log WHERE task_name = ?`, [taskName]); + public static async getLastRunTimestamp(taskName: string): Promise { + const db = await DatabaseService.getDatabase() + const result = await db.get(`SELECT last_run FROM maintenance_log WHERE task_name = ?`, [taskName]); return result ? new Date(result.last_run) : null; } @@ -98,9 +120,10 @@ export class DatabaseService { * @param {string} taskName - The name of the maintenance task for which to update the last run timestamp. * @returns {Promise} A promise that resolves when the operation is complete. */ - public async updateLastRunTimestamp(taskName: string): Promise { + public static async updateLastRunTimestamp(taskName: string): Promise { // Assumes an "upsert" approach for the maintenance_log table - await this.db.run( + const db = await DatabaseService.getDatabase() + await db.run( `INSERT INTO maintenance_log (task_name, last_run) VALUES (?, ?) ON CONFLICT(task_name) @@ -115,9 +138,10 @@ export class DatabaseService { * * @param {number} [days=1] - The age of comments to be purged, in days. Defaults to 30 days. */ - public async purgeOldComments(days: number = 1): Promise { + public static async purgeOldComments(days: number = 1): Promise { + const db = await DatabaseService.getDatabase() console.log(`Purging comments older than ${days} days...`); - await this.db.run(` + await db.run(` DELETE FROM comments WHERE datetime(created_utc, 'unixepoch') < datetime('now', '-${days} days') `); @@ -129,7 +153,8 @@ export class DatabaseService { * @param {string} token_identifier - The baseurl for the axios instance. * @param {Object} tokenData - The OAuth token data. */ - async upsertOAuthToken(token_identifier: string, tokenData: any) { + public static async upsertOAuthToken(token_identifier: string, tokenData: any) { + const db = await DatabaseService.getDatabase() const { access_token, token_type, expires_in, scope } = tokenData; const expiryTimestamp = Math.floor(Date.now() / 1000) + expires_in; console.log('token_identifier', token_identifier) @@ -138,7 +163,7 @@ export class DatabaseService { console.log('expires_in', expires_in) console.log('scope', scope) - await this.db.run(` + await db.run(` INSERT INTO oauth_tokens (token_identifier, access_token, token_type, expires_in, expiry_timestamp, scope) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(token_identifier) DO UPDATE SET @@ -155,8 +180,9 @@ export class DatabaseService { * @param {string} token_identifier - The baseurl for the axios instance. * @returns {Promise} The current OAuth token data or null if expired or not found. */ - async getCurrentOAuthToken(token_identifier: string) { - const tokenRow = await this.db.get(` + public static async getCurrentOAuthToken(token_identifier: string) { + const db = await DatabaseService.getDatabase() + const tokenRow = await db.get(` SELECT access_token, token_type, scope, expiry_timestamp FROM oauth_tokens WHERE token_identifier = ? `, token_identifier); @@ -169,13 +195,14 @@ export class DatabaseService { * * @returns {Promise} True if the cooldown has passed, allowing new notifications to be sent. */ - public async canSendNotification(): Promise { + public static async canSendNotification(): Promise { + const db = await DatabaseService.getDatabase() const cooldownHours = process.env.NOTIFICATION_COOLDOWN_HOURS || 4; const sql = ` SELECT MAX(sent_time) as last_notification_time FROM user_mentions `; - const result = await this.db.get(sql); + const result = await db.get(sql); if (!result || !result.last_notification_time) { // No notifications have been sent yet, or unable to retrieve the last sent time. diff --git a/src/db/services/databaseMaintenance.ts b/src/db/services/databaseMaintenance.ts index 0298437..18504c5 100644 --- a/src/db/services/databaseMaintenance.ts +++ b/src/db/services/databaseMaintenance.ts @@ -5,15 +5,7 @@ import { DatabaseService } from './Database'; * This service is responsible for periodically running maintenance tasks based on specified intervals. */ export class DatabaseMaintenanceService { - private databaseService: DatabaseService; - /** - * Initializes a new instance of the DatabaseMaintenanceService. - * @param {DatabaseService} databaseService - An instance of DatabaseService for database operations. - */ - constructor(databaseService: DatabaseService) { - this.databaseService = databaseService; - } /** * A list of maintenance tasks to be executed, each with a name, action, and interval. @@ -49,7 +41,7 @@ export class DatabaseMaintenanceService { */ private async shouldRunTask(taskName: string, interval: number): Promise { // Use the DatabaseService to check the last run timestamp from the maintenance_log table - const lastRun = await this.databaseService.getLastRunTimestamp(taskName); + const lastRun = await DatabaseService.getLastRunTimestamp(taskName); if (!lastRun) return true; // Task has never run const now = Date.now(); @@ -62,7 +54,7 @@ export class DatabaseMaintenanceService { private async purgeOldComments() { console.log("Purging old comments..."); // Use the DatabaseService for the SQL operation - await this.databaseService.purgeOldComments(); + await DatabaseService.purgeOldComments(); } /** @@ -72,6 +64,6 @@ export class DatabaseMaintenanceService { */ private async updateLastRunTimestamp(taskName: string) { // Use the DatabaseService to update the last run timestamp in the maintenance_log table - await this.databaseService.updateLastRunTimestamp(taskName); + await DatabaseService.updateLastRunTimestamp(taskName); } } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 6f47b5a..676e843 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,8 +20,7 @@ async function startApplication() { if (!db) { throw new Error('Failed to initialize the database.'); } - const databaseService = new DatabaseService(db) - const canSend = await databaseService.canSendNotification(); + const canSend = await DatabaseService.canSendNotification(); const coolDownHours = process.env.NOTIFICATION_COOLDOWN_HOURS if (!canSend) { console.log(`Last Message Sent less than ${coolDownHours ? coolDownHours : 4} hours ago. Set NOTIFICATION_COOLDOWN_HOURS to change this`) @@ -37,14 +36,14 @@ async function startApplication() { rDramaSessionManager.setAuthorizationToken(process.env.RDRAMA_API_KEY); console.log('Database Maintenance Start') - const databaseMaintenance = new DatabaseMaintenanceService(databaseService) + const databaseMaintenance = new DatabaseMaintenanceService() await databaseMaintenance.runMaintenanceTasks() console.log('Reddit Session Start') - await redditSession.getInstance(databaseService) + await redditSession.getInstance() // Initialize services with any required dependencies - const commentFetcher = new CommentProcessor(databaseService); + const commentFetcher = new CommentProcessor(); const commentParser = new CommentParser(); const commentPoster = new CommentPoster() const messageService = new MessageService(); @@ -54,8 +53,7 @@ async function startApplication() { commentFetcher, commentParser, commentPoster, - messageService, - databaseService, + messageService ); await workflowOrchestrator.executeWorkflow(); } diff --git a/src/rdrama/services/CommentProcessor.ts b/src/rdrama/services/CommentProcessor.ts index 1c29a88..6729e5d 100644 --- a/src/rdrama/services/CommentProcessor.ts +++ b/src/rdrama/services/CommentProcessor.ts @@ -9,15 +9,13 @@ import { CommentFetcher } from './CommentFetcher'; */ export class CommentProcessor { private maxPages: number; - private databaseService: DatabaseService; /** * Creates an instance of CommentProcessor. * @param {DatabaseService} databaseService - The service for database operations. * @param {number} maxPages - The maximum number of pages to fetch from the r/Drama API. Defaults to 10. */ - constructor(databaseService: DatabaseService, maxPages: number = 10) { - this.databaseService = databaseService; + constructor(maxPages: number = 10) { this.maxPages = maxPages; } @@ -41,12 +39,12 @@ export class CommentProcessor { // Check if the comment was already processed in this batch if (comments.some(c => c.id === comment.id)) continue; - const exists = await this.databaseService.commentExists(comment.id.toString()); + const exists = await DatabaseService.commentExists(comment.id.toString()); if (exists) { stopFetching = true; break; // Stop processing this batch of comments } - await this.databaseService.insertComment(comment) + await DatabaseService.insertComment(comment) comments.push(comment); } diff --git a/src/reddit/session/SessionManager.ts b/src/reddit/session/SessionManager.ts index a75f03f..1dafa32 100644 --- a/src/reddit/session/SessionManager.ts +++ b/src/reddit/session/SessionManager.ts @@ -10,10 +10,8 @@ dotenv.config(); class RedditSessionManager { private static instance: RedditSessionManager; public axiosInstance: AxiosInstance; - private databaseService: DatabaseService; - private constructor(databaseService: DatabaseService) { - this.databaseService = databaseService; + private constructor() { axiosThrottle.use(axios, { requestsPerSecond: 1 }); // Throttle setup this.axiosInstance = axios.create({ @@ -29,9 +27,9 @@ class RedditSessionManager { }); } - public static async getInstance(databaseService: DatabaseService): Promise { + public static async getInstance(): Promise { if (!RedditSessionManager.instance) { - RedditSessionManager.instance = new RedditSessionManager(databaseService); + RedditSessionManager.instance = new RedditSessionManager(); await RedditSessionManager.instance.initializeAuthentication(); } return RedditSessionManager.instance; @@ -39,7 +37,7 @@ class RedditSessionManager { private async initializeAuthentication() { // Check the database for an existing token - const currentToken = await this.databaseService.getCurrentOAuthToken(this.axiosInstance.defaults.baseURL as string); + const currentToken = await DatabaseService.getCurrentOAuthToken(this.axiosInstance.defaults.baseURL as string); if (currentToken && new Date() < new Date(currentToken.expiry_timestamp * 1000)) { this.axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${currentToken.access_token}`; console.log('Using existing Reddit API token from database.'); @@ -76,7 +74,7 @@ class RedditSessionManager { }); // Upsert the new token into the database - await this.databaseService.upsertOAuthToken( + await DatabaseService.upsertOAuthToken( this.axiosInstance.defaults.baseURL as string, { access_token: response.data.access_token, diff --git a/src/utils/ShouldNotify.ts b/src/utils/ShouldNotify.ts index 250be7f..6bcf1c6 100644 --- a/src/utils/ShouldNotify.ts +++ b/src/utils/ShouldNotify.ts @@ -5,7 +5,7 @@ import { DatabaseService } from '../db/services/Database'; // Load environment variables from .env file dotenv.config(); -export async function shouldNotifyUser(username: string, redditService: RedditService, databaseService: DatabaseService): Promise { +export async function shouldNotifyUser(username: string, redditService: RedditService): Promise { const userInfo = await redditService.getUserInfo(username); if (!userInfo) return false; @@ -15,7 +15,7 @@ export async function shouldNotifyUser(username: string, redditService: RedditSe const excludeEmployees = process.env.EXCLUDE_EMPLOYEES !== 'false'; // Defaults to true unless explicitly set to 'false' const notifyAcceptPms = accept_pms !== false; // Notify if accept_pms is true or undefined const karmaThreshold = parseInt(process.env.KARMA_THRESHOLD || '100000', 10); - const hasBeenNotifiedBefore = await databaseService.userMentionExists(username); + const hasBeenNotifiedBefore = await DatabaseService.userMentionExists(username); const meetsCriteria = (!excludeMods || !is_mod) && // Notify unless we're excluding mods and the user is a mod diff --git a/src/workflows/WorkflowOrchestrator.ts b/src/workflows/WorkflowOrchestrator.ts index 5bdeadc..5c56896 100644 --- a/src/workflows/WorkflowOrchestrator.ts +++ b/src/workflows/WorkflowOrchestrator.ts @@ -13,7 +13,6 @@ class WorkflowOrchestrator { private commentParser: CommentParser, private commentPoster: CommentPoster, private messageService: MessageService, - private databaseService: DatabaseService, ) { } /** @@ -38,15 +37,15 @@ class WorkflowOrchestrator { author_name: comment.author_name, }; for (const redditUser of redditUsers) { - const userMentionExists = await this.databaseService.userMentionExists(redditUser) + const userMentionExists = await DatabaseService.userMentionExists(redditUser) if (userMentionExists) continue const commentResponseRdrama = this.messageService.getRandomRdramaMessage(placeholdersRdrama) const postedComment = await this.commentPoster.postComment(`c_${comment.id}`, `##### TEST MESSAGE NO REDDITOR PINGED (YET...)\n${commentResponseRdrama}`) //const postedComment = await this.commentPoster.postComment(`c_${comment.id}`, ${commentResponse}`) //TODO uncomment after golive console.log(`Sent Comment to`, JSON.stringify(postedComment, null, 4)) - const redditSession = await RedditSessionManager.getInstance(this.databaseService) + const redditSession = await RedditSessionManager.getInstance() const redditService = new RedditService(redditSession) - const resultshouldNotifyUser = await shouldNotifyUser(redditUser, redditService, this.databaseService) + const resultshouldNotifyUser = await shouldNotifyUser(redditUser, redditService) if (!resultshouldNotifyUser) continue const placeholdersReddit = { author_name: comment.author_name, @@ -54,7 +53,7 @@ class WorkflowOrchestrator { permalink: comment.permalink }; const redditMessage = this.messageService.getRandomRedditMessage(placeholdersReddit) - await this.databaseService.insertUserMention({ + await DatabaseService.insertUserMention({ rdrama_comment_id: comment.id, username: redditUser, message: redditMessage,