Files
opengroupware/repositories/ProjectRepository.ts
Ken Yasue 9eb391ea8d feat(m4): project & member management with tests and e2e
- ProjectRepository, ProjectMemberRepository, minimal NotificationRepository
- ProjectService: create/update/archive/delete project, add/remove member,
  permission checks (isMember/admin), member-added notification, getDashboard
  skeleton, getMyProjects, getMemberRole
- projectValidator, API services factory
- API routes: projects CRUD, members list/add/remove
- Screens: dashboard (project list + create form), project overview, members,
  settings; layout (Header/Sidebar/ProjectNav) and project components
- Unit tests (Project/ProjectMember repositories, ProjectService),
  integration project-member-permission, e2e project-management
2026-06-25 01:02:10 +02:00

127 lines
3.2 KiB
TypeScript

import type { SqliteDatabase } from '@/lib/db/sqlite';
import type { Project, ProjectStatus } from '@/lib/types';
interface ProjectRow {
id: number;
name: string;
description: string | null;
status: string;
owner_id: number;
created_at: string;
updated_at: string;
}
function mapProject(row: ProjectRow): Project {
return {
id: row.id,
name: row.name,
description: row.description,
status: row.status as ProjectStatus,
ownerId: row.owner_id,
createdAt: row.created_at,
updatedAt: row.updated_at,
};
}
export interface CreateProjectInput {
name: string;
description?: string;
ownerId: number;
}
export interface UpdateProjectInput {
name?: string;
description?: string | null;
status?: ProjectStatus;
}
/**
* projectsテーブルへのデータアクセスを担うRepository。
*/
export class ProjectRepository {
constructor(private readonly db: SqliteDatabase) {}
findById(id: number): Project | null {
const row = this.db.get<ProjectRow>(
'SELECT * FROM projects WHERE id = @id',
{ id }
);
return row ? mapProject(row) : null;
}
findByOwner(ownerId: number): Project[] {
const rows = this.db.query<ProjectRow>(
'SELECT * FROM projects WHERE owner_id = @ownerId ORDER BY id',
{ ownerId }
);
return rows.map(mapProject);
}
/** ユーザーが参加しているプロジェクト一覧を取得する */
findProjectsByUserId(userId: number): Project[] {
const rows = this.db.query<ProjectRow>(
`SELECT p.* FROM projects p
INNER JOIN project_members pm ON pm.project_id = p.id
WHERE pm.user_id = @userId
ORDER BY p.id`,
{ userId }
);
return rows.map(mapProject);
}
create(input: CreateProjectInput): Project {
const now = new Date().toISOString();
const result = this.db.execute(
`INSERT INTO projects (name, description, status, owner_id, created_at, updated_at)
VALUES (@name, @description, @status, @ownerId, @createdAt, @updatedAt)`,
{
name: input.name,
description: input.description ?? null,
status: 'active',
ownerId: input.ownerId,
createdAt: now,
updatedAt: now,
}
);
const created = this.findById(Number(result.lastInsertRowid));
if (!created) {
throw new Error('Failed to create project');
}
return created;
}
update(id: number, input: UpdateProjectInput): Project | null {
const fields: string[] = ['updated_at = @updatedAt'];
const params: Record<string, unknown> = {
updatedAt: new Date().toISOString(),
id,
};
if (input.name !== undefined) {
fields.push('name = @name');
params.name = input.name;
}
if (input.description !== undefined) {
fields.push('description = @description');
params.description = input.description;
}
if (input.status !== undefined) {
fields.push('status = @status');
params.status = input.status;
}
this.db.execute(
`UPDATE projects SET ${fields.join(', ')} WHERE id = @id`,
params
);
return this.findById(id);
}
delete(id: number): boolean {
const result = this.db.execute('DELETE FROM projects WHERE id = @id', {
id,
});
return result.changes > 0;
}
}