@codervisor/devlog-core
Version:
Core devlog management functionality
151 lines (150 loc) • 6.28 kB
JavaScript
/**
* Database-backed Project Manager
*
* Manages projects using database storage without per-project storage configuration.
* Uses the centralized application storage configuration.
*/
import { ProjectEntity } from '../entities/project.entity.js';
import { getDataSource } from '../utils/typeorm-config.js';
import { ProjectValidator } from '../validation/project-schemas.js';
export class ProjectService {
static instance = null;
database;
repository;
constructor() {
// Database initialization will happen in ensureInitialized()
this.database = null; // Temporary placeholder
this.repository = null; // Temporary placeholder
}
static getInstance() {
if (!ProjectService.instance) {
ProjectService.instance = new ProjectService();
}
return ProjectService.instance;
}
/**
* Initialize the database connection if not already initialized
*/
async ensureInitialized() {
try {
if (!this.database || !this.database.isInitialized) {
console.log('[ProjectService] Getting initialized DataSource...');
this.database = await getDataSource();
this.repository = this.database.getRepository(ProjectEntity);
console.log('[ProjectService] DataSource ready with entities:', this.database.entityMetadatas.length);
console.log('[ProjectService] Repository initialized:', !!this.repository);
// Create default project if it doesn't exist
await this.createDefaultProject();
}
}
catch (error) {
console.error('[ProjectService] Failed to initialize:', error);
throw error;
}
}
/**
* Create default project
*/
async createDefaultProject() {
const defaultProject = {
name: 'Default Project',
description: 'Default devlog project',
settings: {
defaultPriority: 'medium',
},
};
// Create project directly without initialization check since this is called during initialization
const existing = await this.repository.findOne({ where: { name: defaultProject.name } });
if (existing) {
return; // Default project already exists
}
// Create and save new project entity
const entity = ProjectEntity.fromProjectData(defaultProject);
await this.repository.save(entity);
}
async list() {
await this.ensureInitialized(); // Ensure initialization
const entities = await this.repository.find({
order: { lastAccessedAt: 'DESC' },
});
return entities.map((entity) => entity.toProjectMetadata());
}
async get(id) {
await this.ensureInitialized(); // Ensure initialization
const entity = await this.repository.findOne({ where: { id } });
if (!entity) {
return null;
}
// Update last accessed time
entity.lastAccessedAt = new Date();
await this.repository.save(entity);
return entity.toProjectMetadata();
}
async create(project) {
await this.ensureInitialized(); // Ensure initialization
// Validate input data
const validation = ProjectValidator.validateCreateRequest(project);
if (!validation.success) {
throw new Error(`Invalid project data: ${validation.errors.join(', ')}`);
}
const validatedProject = validation.data;
// Check for duplicate project name
const uniqueCheck = await ProjectValidator.validateUniqueProjectName(validatedProject.name, undefined, async (name) => {
const existing = await this.repository.findOne({ where: { name } });
return !!existing;
});
if (!uniqueCheck.success) {
throw new Error(uniqueCheck.error);
}
// Create and save new project entity
const entity = ProjectEntity.fromProjectData(validatedProject);
const savedEntity = await this.repository.save(entity);
return savedEntity.toProjectMetadata();
}
async update(id, updates) {
await this.ensureInitialized(); // Ensure initialization
// Validate project ID
const idValidation = ProjectValidator.validateProjectId(id);
if (!idValidation.success) {
throw new Error(`Invalid project ID: ${idValidation.errors.join(', ')}`);
}
// Validate update data
const validation = ProjectValidator.validateUpdateRequest(updates);
if (!validation.success) {
throw new Error(`Invalid update data: ${validation.errors.join(', ')}`);
}
const validatedUpdates = validation.data;
const entity = await this.repository.findOne({ where: { id } });
if (!entity) {
throw new Error(`Project with ID '${id}' not found`);
}
// Check for duplicate project name if name is being updated
if (validatedUpdates.name && validatedUpdates.name !== entity.name) {
const uniqueCheck = await ProjectValidator.validateUniqueProjectName(validatedUpdates.name, id, async (name, excludeId) => {
const existing = await this.repository.findOne({
where: { name },
});
return !!existing && existing.id !== excludeId;
});
if (!uniqueCheck.success) {
throw new Error(uniqueCheck.error);
}
}
// Update entity
entity.updateFromProjectData(validatedUpdates);
const savedEntity = await this.repository.save(entity);
return savedEntity.toProjectMetadata();
}
async delete(id) {
await this.ensureInitialized(); // Ensure initialization
// Validate project ID
const idValidation = ProjectValidator.validateProjectId(id);
if (!idValidation.success) {
throw new Error(`Invalid project ID: ${idValidation.errors.join(', ')}`);
}
const result = await this.repository.delete({ id });
if (result.affected === 0) {
throw new Error(`Project with ID '${id}' not found`);
}
}
}