CrossTalkPM/src/db/initializeDatabase.ts

116 lines
4.1 KiB
TypeScript

import { Database, open } from 'sqlite';
import sqlite3 from 'sqlite3';
import * as fs from 'fs/promises';
import * as path from 'path';
/**
* Singleton class responsible for initializing and setting up the SQLite database.
* It ensures that only one instance of the database is created and utilized throughout the application.
*/
export class DatabaseInitializer {
private static instance: DatabaseInitializer;
private db: Database | undefined;
private initializationPromise: Promise<void> | undefined;
/**
* The DatabaseInitializer's constructor is private to prevent direct instantiation with the `new` operator
* and ensure the Singleton pattern is followed.
*/
private constructor() { }
/**
* Gets the singleton instance of the DatabaseInitializer.
* @returns The singleton instance of the DatabaseInitializer.
*/
public static getInstance(): DatabaseInitializer {
if (!DatabaseInitializer.instance) {
DatabaseInitializer.instance = new DatabaseInitializer();
DatabaseInitializer.instance.initializationPromise = DatabaseInitializer.instance.setupDatabase();
}
return DatabaseInitializer.instance;
}
/**
* Retrieves the initialized database instance.
*
* @returns {Promise<Database | undefined>} The initialized database instance, or `undefined` if the
* database has not been initialized or if initialization failed.
*/
public async getDbInstance(): Promise<Database | undefined> {
if (this.initializationPromise) {
await this.initializationPromise; // Wait for the database setup to complete.
this.initializationPromise = undefined; // Clear promise after initial setup to avoid subsequent waits.
}
return this.db;
}
/**
* Initializes the SQLite database. If the database file does not exist, it will be created.
* @param dbPath The path to the SQLite database file.
* @returns A promise that resolves with the Database instance.
* @throws {Error} Throws an error if there's an issue opening the database.
*/
private async initializeDatabase(dbPath: string): Promise<Database> {
try {
const db = await open({
filename: dbPath,
driver: sqlite3.Database
});
console.log('Database initialized successfully.');
return db;
} catch (error) {
console.error('Failed to initialize the database:', error);
throw new Error('Failed to initialize the database.');
}
}
/**
* Executes SQL files found within a specified folder. This function is designed to run migration and seed files.
* @param db The SQLite database instance.
* @param folderName The name of the folder containing the SQL files, relative to the class location.
* @throws {Error} Throws an error if there's an issue reading the directory or executing SQL files.
*/
private async runSqlFiles(db: Database, folderName: string): Promise<void> {
const folderPath = path.join(__dirname, '.', folderName); // Adjust for class location within src/db
console.log('folderPath', folderPath)
let files: string[];
try {
files = await fs.readdir(folderPath);
} catch (error) {
console.log(`Could not find or access the folder at ${folderPath}. Skipping execution of SQL files.`);
return; // Exit the function if the folder doesn't exist or can't be accessed
}
console.log('files', files)
const sqlFiles = files.filter(file => file.endsWith('.sql'));
if (sqlFiles.length === 0) {
console.log(`No SQL files found in ${folderName}. Skipping.`);
return;
}
for (const file of sqlFiles.sort()) {
const sql = await fs.readFile(path.join(folderPath, file), 'utf8');
await db.exec(sql);
console.log(`Executed ${file} in ${folderName}`);
}
}
/**
* The main method for setting up the database. It runs the migrations first, then seeds the database.
* @example
* DatabaseInitializer.getInstance();
*/
private async setupDatabase(): Promise<void> {
const dbPath = path.join(__dirname, '.', 'appData.db');
this.db = await this.initializeDatabase(dbPath);
await this.runSqlFiles(this.db!, 'migrations');
await this.runSqlFiles(this.db!, 'seed');
}
}