Files
Ken Yasue ddad5ae78c feat(m8): SSE hub + realtime chat with mentions, tests, e2e
- SseHub (project-scoped client mgmt + broadcast, implements SseBroadcaster)
  with SSE-formatted payload; singleton getSseHub
- ChatRepository (message CRUD, soft-delete, search, pagination, project
  isolation) + ChatService (membership permission, send/edit/delete,
  @email mention -> mention notification, SSE broadcast of created/updated/deleted)
- SSE stream endpoint (ReadableStream + heartbeat + abort cleanup, membership)
- Chat message API routes (history/send/edit/delete)
- ChatWindow client (EventSource auto-reconnect, history fetch, realtime
  append/update/delete), chat page, ProjectNav チャット link
- SseEvent type now carries ChatMessage for chat events
- Unit (SseHub, ChatRepository, ChatService) + integration chat-sse-broadcast
  + e2e chat-sse (two contexts realtime)
2026-06-25 01:45:34 +02:00

304 lines
6.6 KiB
TypeScript

/**
* 全Entity型定義・共有型
* functional-design.md のデータモデルに対応。
* タイムスタンプはISO8601文字列(TEXT)。真偽値はINTEGER(0/1)。
*/
// ===== 列挙型 =====
export type UserRole = 'system_admin' | 'project_admin' | 'member' | 'guest';
export type UserStatus = 'active' | 'inactive';
export type ProjectStatus = 'active' | 'on_hold' | 'completed' | 'archived';
export type ProjectMemberRole = 'admin' | 'member' | 'guest';
export type BoardCategory =
| 'notice'
| 'spec'
| 'minutes'
| 'question'
| 'decision'
| 'trouble'
| 'memo';
export type TodoPriority = 'low' | 'normal' | 'high';
export type MilestoneStatus = 'open' | 'closed';
export type CalendarEventType =
| 'meeting'
| 'deadline'
| 'milestone'
| 'todo'
| 'reminder'
| 'custom';
export type MeetingMemberStatus = 'invited' | 'accepted' | 'declined';
export type NotificationType =
| 'mention'
| 'todo_assigned'
| 'todo_due_soon'
| 'meeting_invited'
| 'board_commented'
| 'project_added'
| 'file_shared'
| 'note_updated';
// ===== エンティティ =====
export interface User {
id: number;
name: string;
email: string;
passwordHash: string | null;
avatarUrl: string | null;
role: UserRole;
status: UserStatus;
createdAt: string;
updatedAt: string;
}
export interface Project {
id: number;
name: string;
description: string | null;
status: ProjectStatus;
ownerId: number;
createdAt: string;
updatedAt: string;
}
export interface ProjectMember {
id: number;
projectId: number;
userId: number;
role: ProjectMemberRole;
joinedAt: string;
}
export interface BoardThread {
id: number;
projectId: number;
title: string;
bodyMd: string;
authorId: number;
category: BoardCategory | null;
isPinned: number;
isImportant: number;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
}
export interface BoardComment {
id: number;
threadId: number;
authorId: number;
bodyMd: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
}
export interface ChatMessage {
id: number;
projectId: number;
authorId: number;
body: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
}
export interface TodoColumn {
id: number;
projectId: number;
name: string;
orderIndex: number;
createdAt: string;
updatedAt: string;
}
export interface TodoItem {
id: number;
projectId: number;
columnId: number;
title: string;
description: string | null;
assigneeId: number | null;
creatorId: number;
priority: TodoPriority;
startDate: string | null;
dueDate: string | null;
completedAt: string | null;
orderIndex: number;
milestoneId: number | null;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
}
export interface FileAsset {
id: number;
projectId: number;
uploaderId: number;
filename: string;
originalName: string;
mimeType: string;
size: number;
path: string;
createdAt: string;
deletedAt: string | null;
}
export interface ProjectNote {
id: number;
projectId: number;
title: string;
bodyMd: string;
tags: string | null;
isPinned: number;
createdById: number;
updatedById: number;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
}
export interface Milestone {
id: number;
projectId: number;
title: string;
description: string | null;
dueDate: string | null;
status: MilestoneStatus;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
}
export interface CalendarEvent {
id: number;
projectId: number;
title: string;
description: string | null;
type: CalendarEventType;
startAt: string;
endAt: string | null;
createdById: number;
relatedTodoId: number | null;
relatedMilestoneId: number | null;
relatedMeetingId: number | null;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
}
export interface Meeting {
id: number;
projectId: number;
title: string;
description: string | null;
location: string | null;
meetingUrl: string | null;
startAt: string;
endAt: string;
agendaMd: string | null;
minutesMd: string | null;
createdById: number;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
}
export interface MeetingMember {
id: number;
meetingId: number;
userId: number;
status: MeetingMemberStatus;
}
export interface Notification {
id: number;
userId: number;
projectId: number | null;
type: NotificationType;
title: string;
body: string | null;
readAt: string | null;
createdAt: string;
}
export interface ActivityLog {
id: number;
projectId: number;
actorId: number;
action: string;
targetType: string;
targetId: number | null;
metadataJson: string | null;
createdAt: string;
}
export interface SchemaMigration {
id: number;
filename: string;
appliedAt: string;
}
// ===== 通知イベント =====
export interface NotificationEventBase {
projectId: number | null;
title: string;
body: string | null;
}
/**
* 通知イベントの判別共用体。
* resolveTargets で type ごとに対象ユーザーを解決する。
*/
export type NotificationEvent =
| (NotificationEventBase & {
type: 'mention';
mentionedUserId: number;
})
| (NotificationEventBase & { type: 'todo_assigned'; assigneeId: number })
| (NotificationEventBase & { type: 'todo_due_soon'; assigneeId: number })
| (NotificationEventBase & {
type: 'meeting_invited';
memberIds: number[];
})
| (NotificationEventBase & {
type: 'board_commented';
threadAuthorId: number;
})
| (NotificationEventBase & { type: 'project_added'; addedUserId: number })
| (NotificationEventBase & {
type: 'file_shared';
projectMemberIds: number[];
})
| (NotificationEventBase & {
type: 'note_updated';
projectMemberIds: number[];
});
/** SSE配信イベント(M8でSseHubが扱う)。 */
export type SseEvent =
| {
type: 'chat.message.created';
data: { projectId: number; message: ChatMessage };
}
| {
type: 'chat.message.updated';
data: { projectId: number; message: ChatMessage };
}
| {
type: 'chat.message.deleted';
data: { projectId: number; id: number };
}
| { type: 'todo.updated'; data: { projectId: number } }
| { type: 'file.uploaded'; data: { projectId: number } }
| { type: 'meeting.created'; data: { projectId: number } }
| { type: 'note.updated'; data: { projectId: number } }
| { type: 'notification.created'; data: { projectId: number | null } };
/** SSE配信を行うコンポーネントの抽象(M8のSseHubが実装) */
export interface SseBroadcaster {
broadcast(projectId: number, event: SseEvent): void;
}