Files
opengroupware/repositories/ProjectMemberRepository.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

124 lines
3.2 KiB
TypeScript

import type { SqliteDatabase } from '@/lib/db/sqlite';
import type {
ProjectMember,
ProjectMemberRole,
User,
UserRole,
UserStatus,
} from '@/lib/types';
interface ProjectMemberRow {
id: number;
project_id: number;
user_id: number;
role: string;
joined_at: string;
}
interface MemberWithUserRow extends ProjectMemberRow {
name: string;
email: string;
avatar_url: string | null;
user_role: string;
user_status: string;
}
export interface ProjectMemberWithUser extends ProjectMember {
user: Pick<User, 'id' | 'name' | 'email' | 'avatarUrl' | 'role' | 'status'>;
}
function mapMember(row: ProjectMemberRow): ProjectMember {
return {
id: row.id,
projectId: row.project_id,
userId: row.user_id,
role: row.role as ProjectMemberRole,
joinedAt: row.joined_at,
};
}
function mapMemberWithUser(row: MemberWithUserRow): ProjectMemberWithUser {
return {
...mapMember(row),
user: {
id: row.user_id,
name: row.name,
email: row.email,
avatarUrl: row.avatar_url,
role: row.user_role as UserRole,
status: row.user_status as UserStatus,
},
};
}
/**
* project_membersテーブルへのデータアクセスを担うRepository。
*/
export class ProjectMemberRepository {
constructor(private readonly db: SqliteDatabase) {}
findByProject(projectId: number): ProjectMemberWithUser[] {
const rows = this.db.query<MemberWithUserRow>(
`SELECT pm.*, u.name, u.email, u.avatar_url, u.role AS user_role, u.status AS user_status
FROM project_members pm
INNER JOIN users u ON u.id = pm.user_id
WHERE pm.project_id = @projectId
ORDER BY pm.id`,
{ projectId }
);
return rows.map(mapMemberWithUser);
}
findByUser(userId: number): ProjectMember[] {
const rows = this.db.query<ProjectMemberRow>(
'SELECT * FROM project_members WHERE user_id = @userId ORDER BY id',
{ userId }
);
return rows.map(mapMember);
}
add(
projectId: number,
userId: number,
role: ProjectMemberRole
): ProjectMember {
const now = new Date().toISOString();
const result = this.db.execute(
`INSERT INTO project_members (project_id, user_id, role, joined_at)
VALUES (@projectId, @userId, @role, @joinedAt)`,
{ projectId, userId, role, joinedAt: now }
);
return {
id: Number(result.lastInsertRowid),
projectId,
userId,
role,
joinedAt: now,
};
}
remove(projectId: number, userId: number): boolean {
const result = this.db.execute(
'DELETE FROM project_members WHERE project_id = @projectId AND user_id = @userId',
{ projectId, userId }
);
return result.changes > 0;
}
isMember(projectId: number, userId: number): boolean {
const row = this.db.get<{ id: number }>(
'SELECT id FROM project_members WHERE project_id = @projectId AND user_id = @userId',
{ projectId, userId }
);
return row !== null;
}
getRole(projectId: number, userId: number): ProjectMemberRole | null {
const row = this.db.get<{ role: string }>(
'SELECT role FROM project_members WHERE project_id = @projectId AND user_id = @userId',
{ projectId, userId }
);
return row ? (row.role as ProjectMemberRole) : null;
}
}