fix: deduplicate and focus code on projects
This commit is contained in:
@@ -23,6 +23,25 @@ export class ProjectEngine extends EventEmitter {
|
||||
super();
|
||||
}
|
||||
|
||||
private assertDeletableProject(id: string): void {
|
||||
if (id === 'default') {
|
||||
throw new Error('Cannot delete the default project');
|
||||
}
|
||||
}
|
||||
|
||||
private mapDbProjectToProjectData(dbProject: Project): ProjectData {
|
||||
return {
|
||||
id: dbProject.id,
|
||||
name: dbProject.name,
|
||||
slug: dbProject.slug,
|
||||
description: dbProject.description || undefined,
|
||||
dataPath: dbProject.dataPath || undefined,
|
||||
createdAt: dbProject.createdAt,
|
||||
updatedAt: dbProject.updatedAt,
|
||||
isActive: dbProject.isActive ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
private generateSlug(name: string): string {
|
||||
return name
|
||||
.toLowerCase()
|
||||
@@ -160,10 +179,7 @@ export class ProjectEngine extends EventEmitter {
|
||||
}
|
||||
|
||||
async deleteProject(id: string): Promise<boolean> {
|
||||
// Prevent deleting the default project
|
||||
if (id === 'default') {
|
||||
throw new Error('Cannot delete the default project');
|
||||
}
|
||||
this.assertDeletableProject(id);
|
||||
|
||||
const db = getDatabase().getLocal();
|
||||
const existing = await db.select().from(projects).where(eq(projects.id, id)).get();
|
||||
@@ -182,10 +198,7 @@ export class ProjectEngine extends EventEmitter {
|
||||
}
|
||||
|
||||
async deleteProjectWithData(id: string): Promise<boolean> {
|
||||
// Prevent deleting the default project
|
||||
if (id === 'default') {
|
||||
throw new Error('Cannot delete the default project');
|
||||
}
|
||||
this.assertDeletableProject(id);
|
||||
|
||||
const db = getDatabase().getLocal();
|
||||
const existing = await db.select().from(projects).where(eq(projects.id, id)).get();
|
||||
@@ -224,32 +237,14 @@ export class ProjectEngine extends EventEmitter {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: dbProject.id,
|
||||
name: dbProject.name,
|
||||
slug: dbProject.slug,
|
||||
description: dbProject.description || undefined,
|
||||
dataPath: dbProject.dataPath || undefined,
|
||||
createdAt: dbProject.createdAt,
|
||||
updatedAt: dbProject.updatedAt,
|
||||
isActive: dbProject.isActive ?? false,
|
||||
};
|
||||
return this.mapDbProjectToProjectData(dbProject);
|
||||
}
|
||||
|
||||
async getAllProjects(): Promise<ProjectData[]> {
|
||||
const db = getDatabase().getLocal();
|
||||
const dbProjects = await db.select().from(projects).all();
|
||||
|
||||
return dbProjects.map(p => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
slug: p.slug,
|
||||
description: p.description || undefined,
|
||||
dataPath: p.dataPath || undefined,
|
||||
createdAt: p.createdAt,
|
||||
updatedAt: p.updatedAt,
|
||||
isActive: p.isActive ?? false,
|
||||
}));
|
||||
return dbProjects.map(p => this.mapDbProjectToProjectData(p));
|
||||
}
|
||||
|
||||
async getActiveProject(): Promise<ProjectData | null> {
|
||||
@@ -261,16 +256,7 @@ export class ProjectEngine extends EventEmitter {
|
||||
return this.getProject('default');
|
||||
}
|
||||
|
||||
return {
|
||||
id: dbProject.id,
|
||||
name: dbProject.name,
|
||||
slug: dbProject.slug,
|
||||
description: dbProject.description || undefined,
|
||||
dataPath: dbProject.dataPath || undefined,
|
||||
createdAt: dbProject.createdAt,
|
||||
updatedAt: dbProject.updatedAt,
|
||||
isActive: dbProject.isActive ?? false,
|
||||
};
|
||||
return this.mapDbProjectToProjectData(dbProject);
|
||||
}
|
||||
|
||||
async setActiveProject(id: string): Promise<ProjectData | null> {
|
||||
|
||||
@@ -215,6 +215,15 @@ describe('ProjectEngine', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should short-circuit before database access when deleting default project', async () => {
|
||||
await expect(projectEngine.deleteProject('default')).rejects.toThrow(
|
||||
'Cannot delete the default project'
|
||||
);
|
||||
|
||||
expect(mockLocalDb.select).not.toHaveBeenCalled();
|
||||
expect(mockLocalDb.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return false for non-existent project', async () => {
|
||||
const result = await projectEngine.deleteProject('non-existent-id');
|
||||
expect(result).toBe(false);
|
||||
@@ -260,6 +269,15 @@ describe('ProjectEngine', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should short-circuit before database access when deleting default project', async () => {
|
||||
await expect(projectEngine.deleteProjectWithData('default')).rejects.toThrow(
|
||||
'Cannot delete the default project'
|
||||
);
|
||||
|
||||
expect(mockLocalDb.select).not.toHaveBeenCalled();
|
||||
expect(mockLocalDb.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return false for non-existent project', async () => {
|
||||
const result = await projectEngine.deleteProjectWithData('non-existent-id');
|
||||
expect(result).toBe(false);
|
||||
@@ -990,6 +1008,35 @@ describe('ProjectEngine', () => {
|
||||
expect(result?.id).toBe('active-project');
|
||||
expect(result?.isActive).toBe(true);
|
||||
});
|
||||
|
||||
it('should normalize nullable fields in active project mapping', async () => {
|
||||
const activeProject = {
|
||||
id: 'active-nullable',
|
||||
name: 'Active Nullable',
|
||||
slug: 'active-nullable',
|
||||
description: null,
|
||||
dataPath: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
isActive: null,
|
||||
};
|
||||
|
||||
vi.mocked(mockLocalDb.select).mockImplementation(() => {
|
||||
const chain = createSelectChain();
|
||||
chain.where = vi.fn().mockReturnValue({
|
||||
...chain,
|
||||
get: vi.fn().mockResolvedValue(activeProject),
|
||||
});
|
||||
return chain;
|
||||
});
|
||||
|
||||
const result = await projectEngine.getActiveProject();
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.description).toBeUndefined();
|
||||
expect(result?.dataPath).toBeUndefined();
|
||||
expect(result?.isActive).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setActiveProject', () => {
|
||||
|
||||
Reference in New Issue
Block a user