@gv-sh/specgen-server
Version:
SpecGen Server - API for Speculative Fiction Generator
425 lines (365 loc) • 11.8 kB
JavaScript
// services/databaseService.js
/* global process */
const fs = require('fs').promises;
const path = require('path');
const sqliteService = require('./sqliteService');
// Use test database in development mode
const isDevelopment = process.env.NODE_ENV !== 'production';
const DATABASE_PATH = path.resolve(
// eslint-disable-next-line no-undef
__dirname,
`../data/${isDevelopment ? 'test-database.json' : 'database.json'}`
);
// Default database structure
const DEFAULT_DATABASE = {
categories: [],
parameters: []
};
/**
* Service for handling JSON file database operations
*/
class DatabaseService {
/**
* Ensure the database file exists with a valid structure
* @private
*/
async #ensureDatabaseFile() {
try {
// Check if the directory exists
await fs.mkdir(path.dirname(DATABASE_PATH), { recursive: true });
try {
// Try to read the existing file
await fs.access(DATABASE_PATH);
} catch {
// File doesn't exist, create with default structure
await this.#writeDatabase(DEFAULT_DATABASE);
}
} catch (error) {
// Critical error in file system operations
this.#logError('Failed to ensure database file', error);
throw new Error('Database initialization failed');
}
}
/**
* Write data to the database file
* @private
* @param {Object} data - Data to write
*/
async #writeDatabase(data) {
try {
// Validate data structure
const validatedData = this.#validateDatabaseStructure(data);
await fs.writeFile(
DATABASE_PATH,
JSON.stringify(validatedData, null, 2),
'utf8'
);
} catch (error) {
this.#logError('Error writing database', error);
throw new Error('Database write failed');
}
}
/**
* Validate and normalize database structure
* @private
* @param {Object} data - Data to validate
* @returns {Object} - Validated database structure
*/
#validateDatabaseStructure(data) {
return {
categories: Array.isArray(data.categories) ? data.categories : [],
parameters: Array.isArray(data.parameters) ? data.parameters : []
};
}
/**
* Log errors with context
* @private
* @param {string} message - Error message
* @param {Error} error - Error object
*/
#logError(message, error) {
if (isDevelopment) {
console.error(message, {
errorMessage: error.message,
errorStack: error.stack,
databasePath: DATABASE_PATH
});
}
}
/**
* Get all data from the database
* @returns {Promise<Object>} - Database content
*/
async getData() {
try {
// Ensure database file exists
await this.#ensureDatabaseFile();
// Read the file
const rawData = await fs.readFile(DATABASE_PATH, 'utf8');
try {
// Parse and validate the data
const parsedData = JSON.parse(rawData);
return this.#validateDatabaseStructure(parsedData);
} catch (parseError) {
// If parsing fails, reset to default
this.#logError('Database file corruption detected', parseError);
await this.#writeDatabase(DEFAULT_DATABASE);
return DEFAULT_DATABASE;
}
} catch (error) {
this.#logError('Unexpected error reading database', error);
return DEFAULT_DATABASE;
}
}
/**
* Save data to the database
* @param {Object} data - Data to save
* @returns {Promise<void>}
*/
async saveData(data) {
try {
// Validate input
if (!data || typeof data !== 'object') {
throw new Error('Invalid data: must be an object');
}
// Ensure database file exists and write validated data
await this.#ensureDatabaseFile();
await this.#writeDatabase(data);
} catch (error) {
this.#logError('Error saving database', error);
throw error;
}
}
/**
* Get all categories
* @returns {Promise<Array>} - All categories
*/
async getCategories() {
const data = await this.getData();
return data.categories || [];
}
/**
* Get a category by ID
* @param {String} id - Category ID
* @returns {Promise<Object|null>} - Category or null if not found
*/
async getCategoryById(id) {
const data = await this.getData();
return (data.categories || []).find(category => category.id === id) || null;
}
/**
* Create a new category
* @param {Object} category - Category to create
* @returns {Promise<Object>} - Created category
*/
async createCategory(category) {
const data = await this.getData();
// Ensure categories array exists
if (!data.categories) {
data.categories = [];
}
// Validate category
if (!category || !category.name) {
throw new Error('Invalid category: name is required');
}
data.categories.push(category);
await this.saveData(data);
return category;
}
/**
* Update a category
* @param {String} id - Category ID
* @param {Object} updatedCategory - Updated category data
* @returns {Promise<Object|null>} - Updated category or null if not found
*/
async updateCategory(id, updatedCategory) {
const data = await this.getData();
// Ensure categories array exists
if (!data.categories) {
data.categories = [];
return null;
}
const index = data.categories.findIndex(category => category.id === id);
if (index === -1) return null;
data.categories[index] = { ...data.categories[index], ...updatedCategory };
await this.saveData(data);
return data.categories[index];
}
/**
* Delete a category and its parameters
* @param {String} id - Category ID
* @returns {Promise<Boolean>} - True if deleted, false if not found
*/
async deleteCategory(id) {
const data = await this.getData();
// Ensure categories and parameters arrays exist
if (!data.categories) data.categories = [];
if (!data.parameters) data.parameters = [];
const initialLength = data.categories.length;
// Filter out the category to delete
data.categories = data.categories.filter(category => category.id !== id);
// Also delete all parameters associated with this category
data.parameters = data.parameters.filter(parameter => parameter.categoryId !== id);
await this.saveData(data);
return data.categories.length < initialLength;
}
/**
* Get all parameters
* @returns {Promise<Array>} - All parameters
*/
async getParameters() {
const data = await this.getData();
return data.parameters || [];
}
/**
* Get parameters by category ID
* @param {String} categoryId - Category ID
* @returns {Promise<Array>} - Parameters for the category
*/
async getParametersByCategoryId(categoryId) {
const data = await this.getData();
return (data.parameters || []).filter(parameter => parameter.categoryId === categoryId);
}
/**
* Get a parameter by ID
* @param {String} id - Parameter ID
* @returns {Promise<Object|null>} - Parameter or null if not found
*/
async getParameterById(id) {
const data = await this.getData();
return (data.parameters || []).find(parameter => parameter.id === id) || null;
}
/**
* Create a new parameter
* @param {Object} parameter - Parameter to create
* @returns {Promise<Object>} - Created parameter
*/
async createParameter(parameter) {
const data = await this.getData();
// Ensure parameters array exists
if (!data.parameters) {
data.parameters = [];
}
// Validate parameter
if (!parameter || !parameter.name || !parameter.type) {
throw new Error('Invalid parameter: name and type are required');
}
data.parameters.push(parameter);
await this.saveData(data);
return parameter;
}
/**
* Update a parameter
* @param {String} id - Parameter ID
* @param {Object} updatedParameter - Updated parameter data
* @returns {Promise<Object|null>} - Updated parameter or null if not found
*/
async updateParameter(id, updatedParameter) {
const data = await this.getData();
// Ensure parameters array exists
if (!data.parameters) {
data.parameters = [];
return null;
}
const index = data.parameters.findIndex(parameter => parameter.id === id);
if (index === -1) return null;
data.parameters[index] = { ...data.parameters[index], ...updatedParameter };
await this.saveData(data);
return data.parameters[index];
}
/**
* Delete a parameter
* @param {String} id - Parameter ID
* @returns {Promise<Boolean>} - True if deleted, false if not found
*/
async deleteParameter(id) {
const data = await this.getData();
// Ensure parameters array exists
if (!data.parameters) {
data.parameters = [];
return false;
}
const initialLength = data.parameters.length;
// Filter out the parameter to delete
data.parameters = data.parameters.filter(parameter => parameter.id !== id);
await this.saveData(data);
return data.parameters.length < initialLength;
}
/**
* Get all generated content
* @param {Object} filters - Optional filters (type, title, etc.)
* @returns {Promise<Array>} - All generated content matching filters
*/
async getGeneratedContent(filters = {}) {
return sqliteService.getGeneratedContent(filters);
}
/**
* Get generated content by ID
* @param {String} id - Content ID
* @returns {Promise<Object|null>} - Content or null if not found
*/
async getContentById(id) {
return sqliteService.getContentById(id);
}
/**
* Get content by specific year
* @param {Number} year - Year to filter by
* @returns {Promise<Array>} - Content items for the specified year
*/
async getContentByYear(year) {
return sqliteService.getContentByYear(year);
}
/**
* Get list of available years in content
* @returns {Promise<Array>} - List of years with content
*/
async getAvailableYears() {
return sqliteService.getAvailableYears();
}
/**
* Create and save generated content
* @param {Object} content - Generated content with metadata
* @returns {Promise<Object>} - Saved content with ID
*/
async saveGeneratedContent(content) {
// Validate content
if (!content || (typeof content !== 'object')) {
throw new Error('Invalid content: must be an object');
}
// If no title provided, generate one
if (!content.title) {
const contentType = content.type || 'fiction';
content.title = `${contentType.charAt(0).toUpperCase() + contentType.slice(1)} ${new Date().toISOString().slice(0, 10)}`;
}
// Set ID and creation timestamp if not provided
if (!content.id) {
content.id = `content-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
}
if (!content.createdAt) {
content.createdAt = new Date().toISOString();
}
if (!content.updatedAt) {
content.updatedAt = content.createdAt;
}
// Use SQLite service to save the content
return sqliteService.saveGeneratedContent(content);
}
/**
* Update generated content
* @param {String} id - Content ID
* @param {Object} updates - Updated content data
* @returns {Promise<Object|null>} - Updated content or null if not found
*/
async updateGeneratedContent(id, updates) {
return sqliteService.updateGeneratedContent(id, updates);
}
/**
* Delete generated content
* @param {String} id - Content ID
* @returns {Promise<Boolean>} - True if deleted, false if not found
*/
async deleteGeneratedContent(id) {
return sqliteService.deleteGeneratedContent(id);
}
}
module.exports = new DatabaseService();