@aiquants/daily-report 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/client.d.mts +81 -3
  2. package/dist/client.d.ts +81 -3
  3. package/dist/client.js +4 -4
  4. package/dist/client.js.map +1 -1
  5. package/dist/client.mjs +4 -4
  6. package/dist/client.mjs.map +1 -1
  7. package/dist/index.d.mts +2 -2
  8. package/dist/index.d.ts +2 -2
  9. package/dist/index.js +1 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.mjs +1 -1
  12. package/dist/index.mjs.map +1 -1
  13. package/dist/server.d.mts +434 -6
  14. package/dist/server.d.ts +434 -6
  15. package/dist/server.js +6 -6
  16. package/dist/server.js.map +1 -1
  17. package/dist/server.mjs +6 -6
  18. package/dist/server.mjs.map +1 -1
  19. package/dist/{sse-schema-rbG114od.d.mts → sse-schema-Df49KA7B.d.mts} +643 -249
  20. package/dist/{sse-schema-eXcMG-Ej.d.ts → sse-schema-RHckD7TS.d.ts} +643 -249
  21. package/dist/styles/daily-report.standalone.css +1 -1
  22. package/dist/{types-D1PKubyo.d.mts → types-Ct1ggzy-.d.mts} +30 -1
  23. package/dist/{types-D1PKubyo.d.ts → types-Ct1ggzy-.d.ts} +30 -1
  24. package/package.json +3 -3
  25. package/src/client/components/daily-report-attachment-indicator.spec.tsx +51 -0
  26. package/src/client/components/daily-report-attachment-indicator.tsx +45 -0
  27. package/src/client/components/daily-report-attachment-list.spec.tsx +94 -0
  28. package/src/client/components/daily-report-attachment-list.tsx +112 -0
  29. package/src/client/components/daily-report-detail-list.tsx +14 -7
  30. package/src/client/components/daily-report-list.tsx +19 -11
  31. package/src/client/components/report-views.spec.tsx +340 -0
  32. package/src/client/config-context.tsx +8 -0
  33. package/src/client/contexts/daily-report-action-context.tsx +2 -0
  34. package/src/client/hooks/use-responsive-layout.spec.ts +74 -0
  35. package/src/client/hooks/use-responsive-layout.ts +56 -0
  36. package/src/client/utils/constants.spec.ts +96 -0
  37. package/src/client/utils/constants.ts +47 -2
  38. package/src/client.ts +2 -0
  39. package/src/server/handlers.attachment.spec.ts +470 -0
  40. package/src/server/handlers.ts +426 -1
  41. package/src/server/ports.ts +74 -0
  42. package/src/server/schema.ts +80 -5
  43. package/src/server/service.spec.ts +280 -14
  44. package/src/server/service.ts +308 -25
  45. package/src/server/test-helpers/handlers-config.ts +142 -0
  46. package/src/server.ts +16 -1
  47. package/src/shared/sse-schema.spec.ts +50 -0
  48. package/src/shared/sse-schema.ts +27 -0
  49. package/src/shared/types.ts +31 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server/cache.ts","../src/shared/business-date.ts","../src/shared/logger.ts","../src/shared/sse-schema.ts","../src/server/etag.ts","../src/server/response.ts","../src/server/sse-reader.ts","../src/server/handlers.ts","../src/server/service.ts","../src/server/authz.ts","../src/server/external-source.ts","../src/server/schema.ts","../src/server.ts"],"sourcesContent":["/**\n * In-memory SQL result cache with TTL, snapshot, and cross-worker epoch (via redis) support.\n * TTL・スナップショット・クロスワーカー epoch (redis 経由) を備えた SQL 結果のインメモリキャッシュ。\n *\n * epoch は任意注入の redis ポート経由で取得・更新する (未注入時は常に 0 = epoch 無効)。\n */\nimport type { DailyReportRedisProvider } from \"./ports\"\n\nexport type SqlResultCacheQueryOptions = {\n forceRefresh?: boolean\n snapshot?: boolean\n ttlMsOverride?: number\n epochKey?: string\n}\n\n/**\n * Creates epoch helpers bound to an optional redis provider.\n * 任意の redis プロバイダーに束縛された epoch ヘルパーを生成する処理。\n */\nexport const createEpochStore = (redis?: DailyReportRedisProvider) => ({\n /**\n * Gets the current epoch value from Redis.\n * Redis からエポック値を取得する。エラー時は 0 を返す (常に re-fetch = safe 方向)。\n */\n async getEpoch(epochKey: string): Promise<number> {\n try {\n const client = await redis?.getClient()\n if (!client) return 0\n const val = await client.get(epochKey)\n return val ? Number(val) : 0\n } catch {\n return 0\n }\n },\n /**\n * Increments the epoch counter in Redis.\n * Redis のエポックカウンターをインクリメントする。\n */\n async incrementEpoch(epochKey: string): Promise<void> {\n try {\n const client = await redis?.getClient()\n if (!client) return\n await client.incr(epochKey)\n } catch {\n // Redis エラー時は epoch 更新をスキップ (次回 GET で 0 → stale 扱い → re-fetch)\n }\n },\n})\n\nexport type EpochStore = ReturnType<typeof createEpochStore>\n\n/**\n * In-memory cache for SQL query results with TTL and snapshot support.\n * TTLとスナップショット機能を備えたSQLクエリ結果のインメモリキャッシュ。\n */\nexport class SqlResultCache {\n private readonly buckets = new Map<string, { records: readonly unknown[]; expireAt: number; epoch: number }>()\n private readonly inFlight = new Map<string, Promise<readonly unknown[]>>()\n\n constructor(\n private readonly config: { defaultTtlMs: number },\n private readonly epochStore: EpochStore,\n ) {}\n\n /**\n * Invalidates a specific cache bucket.\n * 指定されたキャッシュバケットを無効化します。\n */\n invalidate = (cacheKey: string): void => {\n this.buckets.delete(cacheKey)\n this.inFlight.delete(cacheKey)\n }\n\n /**\n * Retrieves records from cache or fetches them if expired/missing.\n * キャッシュからレコードを取得するか、期限切れや未存在の場合はフェッチします。\n */\n getOrFetch = async <T>(\n opts: {\n cacheKey: string\n fetcher: () => Promise<readonly T[]>\n } & SqlResultCacheQueryOptions,\n ): Promise<readonly T[]> => {\n const { cacheKey, forceRefresh, snapshot } = opts\n const bucket = this.buckets.get(cacheKey)\n\n // 強制リフレッシュ時はキャッシュと進行中のリクエストをクリア\n if (forceRefresh) {\n this.buckets.delete(cacheKey)\n this.inFlight.delete(cacheKey)\n }\n\n // 有効なキャッシュがあれば返却 (epoch check 込み)\n if (!forceRefresh && bucket && bucket.expireAt > Date.now()) {\n if (opts.epochKey) {\n // epochKey 指定時は Redis の epoch と比較して stale を検出\n const currentEpoch = await this.epochStore.getEpoch(opts.epochKey)\n if (bucket.epoch === currentEpoch) {\n return snapshot ? (bucket.records.map((r) => structuredClone(r)) as T[]) : (bucket.records as T[])\n }\n // epoch 不一致 → stale bucket を除去して re-fetch へ\n this.buckets.delete(cacheKey)\n } else {\n return snapshot ? (bucket.records.map((r) => structuredClone(r)) as T[]) : (bucket.records as T[])\n }\n }\n\n // 進行中のリクエストがあれば相乗り\n if (!forceRefresh && this.inFlight.has(cacheKey)) {\n const shared = (await this.inFlight.get(cacheKey)) as readonly T[]\n return snapshot ? shared.map((r) => structuredClone(r)) : shared\n }\n\n // 新規フェッチとキャッシュ更新\n // 自己参照 (this.inFlight.get(cacheKey) === promise) のため、定義前参照を回避する確定代入アサーション\n let promise!: Promise<readonly T[]>\n promise = (async () => {\n const records = await opts.fetcher()\n // invalidation 中に完了したリクエストはキャッシュに書き込まない(レース防止)\n // ただし forceRefresh の場合は常に書き込む\n if (forceRefresh || this.inFlight.get(cacheKey) === promise) {\n const expireAt = Date.now() + (opts.ttlMsOverride ?? this.config.defaultTtlMs)\n // キャッシュ保存時に現在の epoch を記録\n const epoch = opts.epochKey ? await this.epochStore.getEpoch(opts.epochKey) : 0\n this.buckets.set(cacheKey, { records, expireAt, epoch })\n // 蓄積防止のため、サイズが一定値を超えたら期限切れキャッシュを一括クリーンアップ\n if (this.buckets.size > 500) {\n this.cleanExpired()\n }\n }\n return records\n })()\n\n if (!forceRefresh) this.inFlight.set(cacheKey, promise as Promise<readonly unknown[]>)\n\n try {\n const result = await promise\n return snapshot ? result.map((r) => structuredClone(r)) : result\n } finally {\n this.inFlight.delete(cacheKey)\n }\n }\n\n /**\n * Clears the cache for a specific key.\n * 指定されたキーのキャッシュをクリアします。\n */\n flush = (cacheKey: string): void => {\n this.buckets.delete(cacheKey)\n this.inFlight.delete(cacheKey)\n }\n\n /**\n * Clears all cached buckets and in-flight requests.\n * すべてのキャッシュバケットと進行中のリクエストを全クリアします。\n */\n clearAll = (): void => {\n this.buckets.clear()\n this.inFlight.clear()\n }\n\n /**\n * Invalidates cache buckets matching a prefix.\n * 指定されたプレフィックスに一致するキャッシュバケットを無効化します。\n */\n invalidatePrefix = (prefix: string): void => {\n for (const key of this.buckets.keys()) {\n if (key.startsWith(prefix)) {\n this.buckets.delete(key)\n }\n }\n for (const key of this.inFlight.keys()) {\n if (key.startsWith(prefix)) {\n this.inFlight.delete(key)\n }\n }\n }\n\n /**\n * Cleans up all expired cache buckets to prevent memory accumulation.\n * メモリー蓄積を防ぐため、期限切れのキャッシュバケットをすべてクリーンアップする処理。\n */\n private cleanExpired(): void {\n const now = Date.now()\n // 期限切れのキーを削除\n for (const [key, bucket] of this.buckets.entries()) {\n if (bucket.expireAt <= now) {\n this.buckets.delete(key)\n }\n }\n }\n}\n","/**\n * Business date normalization and formatting helper functions.\n * 営業日の正規化とフォーマットを支援するユーティリティ群。\n */\n\ntype BusinessDateInput = string | Date | null | undefined\n\nconst ISO_DATE_PATTERN = /^\\d{4}-\\d{2}-\\d{2}$/\nconst SLASH_DATE_PATTERN = /^\\d{4}\\/\\d{2}\\/\\d{2}$/\n\nconst zeroPad = (value: number): string => {\n return value < 10 ? `0${value}` : `${value}`\n}\n\nconst normalizeFromDate = (value: Date): string | null => {\n if (Number.isNaN(value.getTime())) {\n return null\n }\n\n const year = value.getFullYear()\n const month = zeroPad(value.getMonth() + 1)\n const day = zeroPad(value.getDate())\n return `${year}-${month}-${day}`\n}\n\n/**\n * Normalizes business date input into YYYY-MM-DD format string.\n * 営業日を YYYY-MM-DD 形式の文字列へ正規化する。\n */\nexport const normalizeBusinessDateKey = (value: BusinessDateInput): string | null => {\n if (value === null || value === undefined) {\n return null\n }\n\n if (value instanceof Date) {\n return normalizeFromDate(value)\n }\n\n const trimmed = value.trim()\n if (trimmed === \"\") {\n return null\n }\n\n if (ISO_DATE_PATTERN.test(trimmed)) {\n return trimmed\n }\n\n if (SLASH_DATE_PATTERN.test(trimmed)) {\n return trimmed.replaceAll(\"/\", \"-\")\n }\n\n const parsed = new Date(trimmed)\n return normalizeFromDate(parsed)\n}\n\n/**\n * Converts normalized business date into display format YYYY-MM-DD.\n * 正規化済みの営業日を YYYY-MM-DD 形式の表示文字列へ変換する。\n */\nexport const formatBusinessDateDisplay = (value: BusinessDateInput): string | null => {\n const normalized = normalizeBusinessDateKey(value)\n if (!normalized) {\n return null\n }\n return normalized\n}\n\n/**\n * Parses normalized business date into Date object.\n * 正規化した営業日を Date オブジェクトへ変換する。\n */\nexport const parseBusinessDateKeyToDate = (value: BusinessDateInput): Date | null => {\n const normalized = normalizeBusinessDateKey(value)\n if (!normalized) {\n return null\n }\n\n const [year, month, day] = normalized.split(\"-\").map((part) => Number.parseInt(part, 10))\n if ([year, month, day].some((component) => Number.isNaN(component))) {\n return null\n }\n\n return new Date(year, month - 1, day)\n}\n","/**\n * Minimal level-filtered logger used internally by the daily-report package.\n * daily-report パッケージ内部で使う最小のレベルフィルタ付きロガー。\n *\n * 消費アプリのロガー実装 (console 互換の debug/info/warn/error) を注入でき、\n * 未注入時は console にフォールバックする。\n */\n\n/** 出力フィルタリング用のログレベル。 */\nexport enum LogLevel {\n DEBUG = 0,\n INFO = 1,\n WARN = 2,\n ERROR = 3,\n NONE = 4,\n}\n\n/** Console 互換のロガーインターフェース。 */\nexport interface DailyReportLogger {\n debug(message?: unknown, ...optionalParams: unknown[]): void\n info(message?: unknown, ...optionalParams: unknown[]): void\n warn(message?: unknown, ...optionalParams: unknown[]): void\n error(message?: unknown, ...optionalParams: unknown[]): void\n}\n\n/**\n * Creates a prefixed, level-filtered logger delegating to the given implementation.\n * 指定実装へ委譲するプレフィックス付き・レベルフィルタ付きロガーを生成する処理。\n */\nexport const createLogger = (level: LogLevel, prefix: string, impl: DailyReportLogger = console): DailyReportLogger => {\n // 文字列メッセージはプレフィックスを連結、それ以外は先頭引数として付加する\n const format = (message: unknown): unknown[] => (typeof message === \"string\" ? [`${prefix} ${message}`] : [prefix, message])\n return {\n debug: (message?: unknown, ...rest: unknown[]) => {\n if (level <= LogLevel.DEBUG) impl.debug(...format(message), ...rest)\n },\n info: (message?: unknown, ...rest: unknown[]) => {\n if (level <= LogLevel.INFO) impl.info(...format(message), ...rest)\n },\n warn: (message?: unknown, ...rest: unknown[]) => {\n if (level <= LogLevel.WARN) impl.warn(...format(message), ...rest)\n },\n error: (message?: unknown, ...rest: unknown[]) => {\n if (level <= LogLevel.ERROR) impl.error(...format(message), ...rest)\n },\n }\n}\n","/**\n * Zod schemas for daily-report SSE messages (client/server shared contract).\n * 日報 SSE メッセージの zod スキーマ (client / server 共有契約)。\n */\nimport { z } from \"zod\"\nimport type { DailyReportCommentItem as DeclaredComment, DailyReportDetail as DeclaredDetail } from \"./types\"\n\n// --- サブスキーマ(types.ts の型と構造的に一致させる) ---\n\nexport const dailyReportInterviewerSchema = z.object({\n name: z.string().nullish(),\n affiliation: z.string().nullish(),\n})\n\nexport const dailyReportCommentSchema = z.object({\n name: z.string().nullish(),\n text: z.string().nullish(),\n color: z.string().nullish(),\n})\n\nexport const dailyReportLabelDefSchema = z.object({\n id: z.number(),\n name: z.string().nullish(),\n color: z.string().nullish(),\n})\n\nexport const dailyReportCommentItemSchema = z.object({\n id: z.number(),\n userId: z.string().nullish(),\n userName: z.string().nullish(),\n content: z.string().nullish(),\n createdAt: z.string().nullish(),\n isMine: z.boolean(),\n})\n\nexport const dailyReportDetailSchema = z.object({\n reportHubId: z.number(),\n date: z.string().nullish(),\n createdAt: z.string().nullish(),\n author: z.string().nullish(),\n userId: z.string().nullish(),\n sourceType: z.string().nullish(),\n employeeName: z.string().nullish(),\n updatedBy: z.string().nullish(),\n updatedAt: z.string().nullish(),\n category: z.string().nullish(),\n creationCategory: z.string().nullish(),\n visitTimeFrom: z.string().nullish(),\n visitTimeTo: z.string().nullish(),\n customerName: z.string().nullish(),\n interviewers: z.array(dailyReportInterviewerSchema),\n subject: z.string().nullish(),\n content: z.string().nullish(),\n comments: z.array(dailyReportCommentSchema),\n isRead: z.boolean(),\n isStarred: z.boolean(),\n labels: z.array(dailyReportLabelDefSchema),\n commentItems: z.array(dailyReportCommentItemSchema),\n})\n\n// --- SSE メッセージスキーマ(8種) ---\n\nexport const connectedMessageSchema = z.object({\n type: z.literal(\"connected\"),\n})\n\nexport const statusUpdateMessageSchema = z.object({\n type: z.literal(\"status-update\"),\n reportHubId: z.number(),\n statusType: z.enum([\"star\", \"read\"]),\n value: z.boolean(),\n clientTempId: z.string(),\n // recipientRawUserId はサーバーサイドフィルタリング専用。\n // SSE ルートが raw JSON から取得してフィルタ後に除去する。\n // クライアントには到達しないが、サーバー側の publish で .parse() を通すため定義が必要。\n recipientRawUserId: z.number().optional(),\n})\n\nexport const commentAddMessageSchema = z.object({\n type: z.literal(\"comment-add\"),\n reportHubId: z.number(),\n comment: dailyReportCommentItemSchema,\n clientTempId: z.string(),\n})\n\nexport const commentDeleteMessageSchema = z.object({\n type: z.literal(\"comment-delete\"),\n reportHubId: z.number(),\n commentId: z.number(),\n clientTempId: z.string(),\n})\n\nexport const reportCreateMessageSchema = z.object({\n type: z.literal(\"report-create\"),\n reportHubId: z.number(),\n report: dailyReportDetailSchema,\n clientTempId: z.string(),\n recipientRawUserId: z.number().optional(),\n})\n\nexport const reportUpdateMessageSchema = z.object({\n type: z.literal(\"report-update\"),\n reportHubId: z.number(),\n report: dailyReportDetailSchema,\n clientTempId: z.string(),\n recipientRawUserId: z.number().optional(),\n})\n\nexport const reportPublishMessageSchema = z.object({\n type: z.literal(\"report-publish\"),\n reportHubId: z.number(),\n report: dailyReportDetailSchema,\n clientTempId: z.string(),\n recipientRawUserId: z.number().optional(),\n})\n\nexport const reportDeleteMessageSchema = z.object({\n type: z.literal(\"report-delete\"),\n reportHubId: z.number(),\n clientTempId: z.string(),\n})\n\n// --- Discriminated Union ---\n\nexport const dailyReportSseMessageSchema = z.discriminatedUnion(\"type\", [\n connectedMessageSchema,\n statusUpdateMessageSchema,\n commentAddMessageSchema,\n commentDeleteMessageSchema,\n reportCreateMessageSchema,\n reportUpdateMessageSchema,\n reportPublishMessageSchema,\n reportDeleteMessageSchema,\n])\n\n// --- 型エクスポート ---\n\nexport type DailyReportSseMessage = z.infer<typeof dailyReportSseMessageSchema>\n\n// --- コンパイル時の型互換チェック ---\n// Zod推論型が既存の型定義に代入可能であることを保証する。\n// ここでエラーが出たらスキーマと types.ts がずれている。\n\ntype _AssertDetailCompat = z.infer<typeof dailyReportDetailSchema> extends DeclaredDetail ? true : never\ntype _AssertCommentCompat = z.infer<typeof dailyReportCommentItemSchema> extends DeclaredComment ? true : never\nconst _detailCheck: _AssertDetailCompat = true\nconst _commentCheck: _AssertCommentCompat = true\nvoid _detailCheck\nvoid _commentCheck\n","/**\n * ETag generation helper for JSON payloads.\n * JSON ペイロード向けの ETag 生成ヘルパー。\n */\nimport { createHash } from \"node:crypto\"\n\n/**\n * Generates a SHA-256 ETag for the given data.\n * 指定されたデータの SHA-256 ETag を生成します。\n *\n * @param data The data to hash (will be JSON stringified).\n * @returns The ETag string (wrapped in quotes).\n */\nexport const generateETag = (data: unknown): string => {\n const json = JSON.stringify(data)\n const hash = createHash(\"sha256\").update(json).digest(\"hex\")\n return `\"${hash}\"`\n}\n","/**\n * JSON response helper with shared security headers and ETag support.\n * 共有セキュリティヘッダーと ETag 対応を備えた JSON レスポンスヘルパー。\n */\nimport { createLogger, type DailyReportLogger, LogLevel } from \"../shared/logger\"\nimport { generateETag } from \"./etag\"\n\nconst defaultLogger = createLogger(LogLevel.INFO, \"[Response]\")\n\n/**\n * Creates JSON response with shared security headers and ETag support.\n * 共有のセキュリティヘッダーと ETag サポート付き JSON レスポンスを生成。\n */\nexport const jsonResponseWithETag = (request: Request, cookie: string | null, payload: Record<string, unknown>, status = 200, logger: DailyReportLogger = defaultLogger): Response => {\n const etag = generateETag(payload)\n const ifNoneMatch = request.headers.get(\"If-None-Match\")\n\n logger.info(`[jsonResponseWithETag] ETag: ${etag}, If-None-Match: ${ifNoneMatch}`)\n\n const headers = new Headers({\n \"Content-Type\": \"application/json\",\n \"Cache-Control\": \"private, max-age=0, must-revalidate\",\n \"X-Content-Type-Options\": \"nosniff\",\n \"X-Frame-Options\": \"DENY\",\n \"Content-Security-Policy\": \"default-src 'none'\",\n ETag: etag,\n })\n if (cookie) {\n headers.append(\"Set-Cookie\", cookie)\n }\n\n if (request.method === \"GET\" && status === 200 && ifNoneMatch === etag) {\n return new Response(null, { status: 304, headers })\n }\n\n return new Response(JSON.stringify(payload), { status, headers })\n}\n","/**\n * Fan-Out shared reader for the daily report SSE Redis Stream.\n * 日報 SSE 用 Redis Stream の共有リーダーモジュール。\n *\n * 単一の xRead BLOCK ループで全接続にメッセージをブロードキャストし、\n * 接続ごとに Redis TCP を張る問題を解消する。\n */\n\nimport { EventEmitter } from \"node:events\"\nimport { createLogger, type DailyReportLogger, LogLevel } from \"../shared/logger\"\nimport type { SqlResultCache } from \"./cache\"\nimport type { DailyReportRedisBlockingClient, DailyReportRedisProvider } from \"./ports\"\n\n/**\n * Compares two Redis Stream IDs numerically.\n * Redis Stream ID を数値比較し、a <= b なら true を返す。\n *\n * Stream ID は `<timestamp_ms>-<sequence>` 形式。\n * 辞書順比較では sequence が可変長のとき不正確になるため、数値分割で比較する。\n */\nexport const isStreamIdLte = (a: string, b: string): boolean => {\n const [aMs, aSeq] = a.split(\"-\").map(Number)\n const [bMs, bSeq] = b.split(\"-\").map(Number)\n if (aMs !== bMs) return aMs < bMs\n return aSeq <= bSeq\n}\n\n/** xRead 結果の 1 エントリに相当する型。 */\nexport type StreamEntry = {\n id: string\n message: Record<string, string>\n}\n\nexport type DailyReportSseReaderConfig = {\n /** ブロッキング xRead 用の専用クライアントを生成する redis プロバイダー。 */\n redis?: DailyReportRedisProvider\n /** SSE Redis Stream キー。 */\n streamKey: string\n /** 受信メッセージに応じてローカル SQL キャッシュを無効化する対象キャッシュ。 */\n cache: SqlResultCache\n /** ロガー (既定は console ベース)。 */\n logger?: DailyReportLogger\n}\n\n/**\n * Shared xRead loop that fans out entries to all active SSE connections.\n * 全 SSE 接続に対してメッセージをファンアウトする共有 xRead ループ。\n *\n * - \"entry\" イベント: 新着エントリ (StreamEntry) をブロードキャスト\n * - \"error\" イベント: 回復不能エラーをブロードキャスト\n * */\nexport class DailyReportSseReader {\n private _emitter = new EventEmitter()\n private _state: \"idle\" | \"running\" | \"stopping\" = \"idle\"\n private _refCount = 0\n private _lastId = \"0-0\"\n private _client: DailyReportRedisBlockingClient | undefined\n private readonly _logger: DailyReportLogger\n\n constructor(private readonly config: DailyReportSseReaderConfig) {\n // リスナー上限を緩和 (接続数分)\n this._emitter.setMaxListeners(0)\n this._logger = config.logger ?? createLogger(LogLevel.INFO, \"[SSE Reader]\")\n }\n\n /**\n * Subscribes to the shared reader. Starts the loop on first subscriber.\n * 共有リーダーを購読する。最初の購読者でループを開始する。\n *\n * @param onEntry - 新着エントリのコールバック\n * @param onError - 回復不能エラーのコールバック (省略可)\n * @returns unsubscribe 関数。呼び出すと両リスナーを解除し、最後の購読者解除でループを停止する。\n */\n subscribe(onEntry: (entry: StreamEntry) => void, onError?: (err: Error) => void): () => void {\n this._emitter.on(\"entry\", onEntry)\n if (onError) {\n this._emitter.on(\"error\", onError)\n }\n this._refCount++\n\n // 最初の購読者でループ開始\n if (this._refCount === 1) {\n void this._startLoop()\n }\n\n // unsubscribe\n return () => {\n this._emitter.removeListener(\"entry\", onEntry)\n if (onError) {\n this._emitter.removeListener(\"error\", onError)\n }\n this._refCount--\n if (this._refCount <= 0) {\n this._refCount = 0\n void this._stopLoop()\n }\n }\n }\n\n /**\n * Destroys the reader for test cleanup.\n * テストクリーンアップ用のデストラクタ。\n */\n async destroy(): Promise<void> {\n this._refCount = 0\n await this._stopLoop()\n this._emitter.removeAllListeners()\n this._lastId = \"0-0\"\n }\n\n /**\n * Internal xRead BLOCK loop.\n * 内部 xRead BLOCK ループ。\n */\n private async _startLoop(): Promise<void> {\n // stopping 中なら完了を待つ\n while (this._state === \"stopping\") {\n await new Promise((resolve) => setTimeout(resolve, 50))\n }\n if (this._state === \"running\") return\n\n this._state = \"running\"\n\n // 専用クライアント取得。createClient が reject した場合も _state を idle へ戻し、\n // 再購読でループを再開できるようにする (注入 redis 実装が例外を投げても state-stuck しない)。\n try {\n this._client = await this.config.redis?.createClient()\n } catch (err) {\n this._state = \"idle\"\n this._emitter.emit(\"error\", err instanceof Error ? err : new Error(\"Failed to create Redis client for SSE reader\"))\n return\n }\n if (!this._client) {\n this._state = \"idle\"\n this._emitter.emit(\"error\", new Error(\"Failed to create Redis client for SSE reader\"))\n return\n }\n\n // エラーリスナー\n this._client.on(\"error\", (err) => {\n this._logger.error(\"Redis client error:\", err)\n })\n\n try {\n while (this._state === \"running\" && this._client?.isOpen) {\n const results = await this._client.xRead([{ key: this.config.streamKey, id: this._lastId }], { BLOCK: 5000, COUNT: 100 })\n if (!results) continue\n\n for (const stream of results) {\n for (const msg of stream.messages) {\n this._lastId = msg.id\n // epoch check の補完: SSE 経由でもローカルキャッシュを即座にクリア\n // (epoch check が主防御線。SSE は Redis ダウン時や非 epoch 対象キャッシュへのフォールバック)\n try {\n const parsed = msg.message?.data ? JSON.parse(msg.message.data) : null\n const msgType = parsed?.type\n if (msgType === \"report-create\" || msgType === \"report-delete\" || msgType === \"report-publish\") {\n this.config.cache.invalidatePrefix(\"daily-report:ids\")\n }\n // コメント・ステータス変更時は対象レポートの詳細キャッシュを無効化\n if (msgType === \"comment-add\" || msgType === \"comment-delete\" || msgType === \"status-update\") {\n const reportHubId = parsed?.reportHubId\n if (typeof reportHubId === \"number\") {\n this.config.cache.invalidate(`daily-report:detail:${reportHubId}`)\n this.config.cache.invalidatePrefix(`daily-report:detail:${reportHubId}:user:`)\n }\n }\n } catch {\n // JSON パースエラーは無視 (invalidation スキップ = safe 方向)\n }\n this._emitter.emit(\"entry\", { id: msg.id, message: msg.message as Record<string, string> })\n }\n }\n }\n } catch (err) {\n // ClientClosedError は正常停止時に発生する\n const isClientClosed = (err as Error)?.constructor?.name === \"ClientClosedError\"\n if (!isClientClosed && this._state === \"running\") {\n this._logger.error(\"xRead loop error:\", err)\n this._emitter.emit(\"error\", err)\n }\n } finally {\n // クライアント切断\n if (this._client?.isOpen) {\n try {\n await this._client.quit()\n } catch (_e) {\n // 切断エラーは無視\n }\n }\n this._client = undefined\n this._state = \"idle\"\n\n // ループ終了後に購読者が残っていれば自動再起動\n if (this._refCount > 0) {\n void this._startLoop()\n }\n }\n }\n\n /**\n * Stops the xRead loop by closing the client.\n * クライアントを閉じて xRead ループを停止する。\n */\n private async _stopLoop(): Promise<void> {\n if (this._state !== \"running\") return\n this._state = \"stopping\"\n\n if (this._client?.isOpen) {\n try {\n await this._client.quit()\n } catch (_e) {\n // 切断エラーは無視\n }\n }\n\n // idle になるまで待機。_state は別の非同期ループ (connect の run ループ) が \"idle\" に戻すが、\n // TS は await を跨いだクラスフィールドの変化を追えず \"stopping\" に過剰 narrowing するため、宣言型へキャストして比較する。\n while ((this._state as \"idle\" | \"running\" | \"stopping\") !== \"idle\") {\n await new Promise((resolve) => setTimeout(resolve, 50))\n }\n }\n}\n","/**\n * React Router loader/action factories for the daily-report HTTP surface.\n * 日報 HTTP サーフェス向けの React Router loader / action 工場。\n *\n * - index.loader: 画面ルートの認証 + 難読化ユーザー ID 解決\n * - api.loader / api.action: `:endpoint` パラメータ式の認証付き API ルーター\n * - sse.loader: Redis Streams ベースのリアルタイム更新 SSE エンドポイント\n */\nimport { normalizeBusinessDateKey } from \"../shared/business-date\"\nimport { createLogger, type DailyReportLogger, LogLevel } from \"../shared/logger\"\nimport { dailyReportSseMessageSchema } from \"../shared/sse-schema\"\nimport type { DailyReportAuthenticate, DailyReportEncodeUserId, DailyReportRedisProvider } from \"./ports\"\nimport { jsonResponseWithETag } from \"./response\"\nimport type { DailyReportService } from \"./service\"\nimport type { DailyReportSseReader, StreamEntry } from \"./sse-reader\"\nimport { isStreamIdLte } from \"./sse-reader\"\n\ntype LoaderArgs = { request: Request; params: Record<string, string | undefined> }\n\n/** `data()` 相当の JSON レスポンス生成 (react-router 非依存)。 */\nconst jsonData = (payload: unknown, init?: { status?: number }): Response =>\n new Response(JSON.stringify(payload), {\n status: init?.status ?? 200,\n headers: { \"Content-Type\": \"application/json\" },\n })\n\nexport type DailyReportHandlersConfig = {\n /** リクエスト認証ポート。 */\n authenticate: DailyReportAuthenticate\n /** データアクセスサービス。 */\n service: DailyReportService\n /** 内部数値 ID の難読化ポート。 */\n encodeUserId: DailyReportEncodeUserId\n /** SSE 用 redis プロバイダー (catch-up の xRange / xRevRange に使用)。 */\n redis?: DailyReportRedisProvider\n /** SSE Fan-Out 共有リーダー。 */\n sseReader: DailyReportSseReader\n /** SSE Redis Stream キー。 */\n streamKey: string\n /** 未ログイン時のリダイレクト先 (index.loader 用、既定 \"/auth/login\")。 */\n loginRedirectPath?: string\n /** ロガー (既定は console ベース)。 */\n logger?: DailyReportLogger\n}\n\n/**\n * Creates the daily-report loaders/actions bound to the injected dependencies.\n * 注入依存に束縛された日報 loader / action 群を生成する処理。\n */\nexport function createDailyReportHandlers(config: DailyReportHandlersConfig) {\n const { authenticate, service, encodeUserId, redis, sseReader, streamKey } = config\n const loginRedirectPath = config.loginRedirectPath ?? \"/auth/login\"\n const apiLogger = config.logger ?? createLogger(LogLevel.ERROR, \"[DailyReportAPI]\")\n const sseLogger = config.logger ?? createLogger(LogLevel.INFO, \"[DailyReportSSE]\")\n\n // ---------------- index (画面ルート) ----------------\n\n /**\n * Document loader that authenticates and resolves the obfuscated internal user id.\n * 認証と難読化済み内部ユーザー ID の解決を行うドキュメントローダー。\n */\n const indexLoader = async ({ request }: LoaderArgs) => {\n const { user, cookie } = await authenticate(request, { failureRedirect: loginRedirectPath })\n const headers = new Headers()\n if (cookie) {\n headers.append(\"Set-Cookie\", cookie)\n }\n\n let userId: number | null = null\n if (user) {\n userId = await service.getUserIdByExternalId(user.id)\n }\n\n const hashedUserId = userId ? encodeUserId(userId) : null\n\n return { data: { user, userId: hashedUserId }, headers }\n }\n\n // ---------------- api (:endpoint ルーター) ----------------\n\n type User = { id: string }\n type EndpointHandler = (url: URL, cookie: string | null, request: Request, user: User) => Promise<Response>\n\n /**\n * Handlers for each API endpoint.\n * 各 API エンドポイントのハンドラー定義。\n */\n const endpointHandlers: Record<string, EndpointHandler> = {\n /**\n * Retrieves daily reports for a specific business date.\n * 指定された営業日の日報一覧を取得する。\n */\n \"business-date\": async (url, cookie, request, user) => {\n const normalizedBusinessDate = normalizeBusinessDateKey(url.searchParams.get(\"businessDate\"))\n const forceRefresh = url.searchParams.get(\"forceRefresh\") === \"true\"\n\n if (!normalizedBusinessDate) {\n return jsonResponseWithETag(request, cookie, { error: { message: \"Invalid business date\" } }, 400)\n }\n const reports = await service.getDailyReportsByBusinessDateByExternalId(normalizedBusinessDate, user.id, { forceRefresh })\n return jsonResponseWithETag(request, cookie, { businessDate: normalizedBusinessDate, reports }, 200)\n },\n /**\n * Retrieves a list of all daily report IDs.\n * 全ての日報 ID の一覧を取得する。\n */\n ids: async (url, cookie, request, user) => {\n const forceRefresh = url.searchParams.get(\"forceRefresh\") === \"true\"\n const dailyReportIds = await service.getDailyReportIdsByExternalId(user.id, { forceRefresh })\n return jsonResponseWithETag(request, cookie, { ids: dailyReportIds }, 200)\n },\n /**\n * Retrieves details for a specific daily report.\n * 指定された日報の詳細情報を取得する。\n */\n report: async (url, cookie, request, user) => {\n const param = url.searchParams.get(\"reportHubId\")\n const forceRefresh = url.searchParams.get(\"forceRefresh\") === \"true\"\n const parsedId = param ? Number.parseInt(param, 10) : NaN\n\n if (!Number.isFinite(parsedId) || parsedId <= 0) {\n return jsonResponseWithETag(request, cookie, { error: { message: \"Invalid reportHubId\" } }, 400)\n }\n const detail = await service.getDailyReportDetailByIdByExternalId(parsedId, user.id, { snapshot: true, forceRefresh })\n if (!detail) {\n return jsonResponseWithETag(request, cookie, { error: { message: \"Report not found\" } }, 404)\n }\n return jsonResponseWithETag(request, cookie, { report: detail }, 200)\n },\n }\n\n /**\n * Routes authenticated daily report API requests by endpoint.\n * エンドポイントごとに認証済みの日報 API リクエストを振り分ける。\n */\n const apiLoader = async ({ request, params }: LoaderArgs) => {\n const { user, cookie } = await authenticate(request, { failureRedirect: null })\n const sanitizedCookie = cookie ?? null\n\n if (!user) {\n return jsonResponseWithETag(request, sanitizedCookie, { error: { message: \"Unauthorized\" } }, 401)\n }\n\n if (request.method !== \"GET\") {\n return jsonResponseWithETag(request, sanitizedCookie, { error: { message: \"Method not allowed\" } }, 405)\n }\n const endpoint = params.endpoint ?? \"\"\n const handler = endpointHandlers[endpoint]\n if (!handler) {\n return jsonResponseWithETag(request, sanitizedCookie, { error: { message: \"Unknown endpoint\" } }, 404)\n }\n const startTime = Date.now()\n try {\n const url = new URL(request.url)\n return await handler(url, sanitizedCookie, request, user)\n } catch (error) {\n const elapsed = Date.now() - startTime\n const err = error instanceof Error ? error : new Error(String(error))\n apiLogger.error(`500 endpoint=${endpoint} elapsed=${elapsed}ms name=${err.name} code=${\"code\" in err ? (err as { code: unknown }).code : \"N/A\"} message=${err.message}`)\n apiLogger.error(\"Stack:\", err.stack)\n return jsonResponseWithETag(request, sanitizedCookie, { error: { message: \"Internal Server Error\" } }, 500)\n }\n }\n\n /**\n * Handles data mutations for daily reports.\n * 日報データの変更操作を処理するアクション。\n */\n const apiAction = async ({ request, params }: LoaderArgs) => {\n const { user } = await authenticate(request, { failureRedirect: null })\n if (!user) {\n return jsonData({ error: \"Unauthorized\" }, { status: 401 })\n }\n\n const endpoint = params.endpoint\n if (endpoint !== \"action\") {\n return jsonData({ error: \"Unknown endpoint\" }, { status: 404 })\n }\n\n const userId = await service.getUserIdByExternalId(user.id)\n if (!userId) {\n return jsonData({ error: \"User not found\" }, { status: 404 })\n }\n\n const formData = await request.formData()\n const intent = formData.get(\"intent\")\n const reportHubIdRaw = formData.get(\"reportHubId\")\n const reportHubId = reportHubIdRaw ? Number(reportHubIdRaw) : NaN\n const businessDate = formData.get(\"businessDate\") as string | null\n const operationTimestamp = Number(formData.get(\"operationTimestamp\"))\n const clientTempId = formData.get(\"clientTempId\") as string | null\n\n if (intent === \"clearCache\") {\n await service.clearCache()\n return jsonData({ status: \"OK\", intent: \"clearCache\" })\n }\n\n if (!clientTempId) {\n return jsonData({ error: \"clientTempId required\" }, { status: 400 })\n }\n\n if (intent !== \"create\" && (!reportHubId || Number.isNaN(reportHubId))) {\n return jsonData({ error: \"Invalid reportHubId\" }, { status: 400 })\n }\n\n switch (intent) {\n case \"create\": {\n if (!businessDate) {\n return jsonData({ error: \"businessDate required\" }, { status: 400 })\n }\n const newReport = await service.createDailyReport(userId, businessDate, clientTempId)\n return jsonData({\n status: \"OK\",\n intent: \"create\",\n report: newReport,\n reportHubId: String(newReport.reportHubId),\n clientTempId, // Echo back for validation\n })\n }\n case \"update\": {\n const title = formData.get(\"title\") as string | undefined\n const content = formData.get(\"content\") as string | undefined\n try {\n await service.updateDailyReport(reportHubId, userId, { title, content }, clientTempId)\n return jsonData({ status: \"OK\", intent: \"update\", reportHubId: String(reportHubId), clientTempId })\n } catch (e: unknown) {\n if (e instanceof Error) {\n if (e.message === \"Unauthorized\") {\n return jsonData({ error: \"Unauthorized\" }, { status: 403 })\n }\n if (e.message === \"Not Found\") {\n return jsonData({ error: \"Report not found\" }, { status: 404 })\n }\n }\n throw e\n }\n }\n case \"publish\": {\n try {\n const publishedReport = await service.publishDailyReport(reportHubId, userId, clientTempId)\n return jsonData({ status: \"OK\", intent: \"publish\", reportHubId: String(reportHubId), clientTempId, report: publishedReport })\n } catch (e: unknown) {\n if (e instanceof Error) {\n if (e.message === \"Unauthorized\") {\n return jsonData({ error: \"Unauthorized\" }, { status: 403 })\n }\n if (e.message === \"Not Found\") {\n return jsonData({ error: \"Report not found\" }, { status: 404 })\n }\n }\n throw e\n }\n }\n case \"delete\": {\n try {\n await service.deleteDailyReport(reportHubId, userId, clientTempId)\n return jsonData({\n status: \"OK\",\n intent: \"delete\",\n reportHubId: String(reportHubId),\n clientTempId, // Echo back for validation even on delete\n })\n } catch (e: unknown) {\n if (e instanceof Error) {\n if (e.message === \"Unauthorized\") {\n return jsonData({ error: \"Unauthorized\" }, { status: 403 })\n }\n if (e.message === \"Not Found\") {\n return jsonData({ error: \"Report not found\" }, { status: 404 })\n }\n }\n throw e\n }\n }\n case \"toggleStar\": {\n const isStarredRaw = formData.get(\"isStarred\")\n if (isStarredRaw === null) {\n return jsonData({ error: \"isStarred required\" }, { status: 400 })\n }\n const isStarred = isStarredRaw === \"true\"\n const updatedStatus = await service.setStarStatus(userId, reportHubId, businessDate, isStarred, clientTempId)\n // クライアントは isStarred/isRead のみ参照する。生内部 ID (userId/created_by/updated_by) を\n // 含む行全体は返さず、必要なフラグだけに絞る (内部 ID の自己開示防止)。\n return jsonData({ status: \"OK\", intent: \"toggleStar\", updatedStatus: { isStarred: updatedStatus.isStarred, isRead: updatedStatus.isRead }, operationTimestamp, reportHubId: String(reportHubId), clientTempId })\n }\n case \"toggleRead\": {\n const isReadRaw = formData.get(\"isRead\")\n if (isReadRaw === null) {\n return jsonData({ error: \"isRead required\" }, { status: 400 })\n }\n const isRead = isReadRaw === \"true\"\n const updatedStatus = await service.setReadStatus(userId, reportHubId, businessDate, isRead, clientTempId)\n // クライアントは isStarred/isRead のみ参照する (内部 ID の自己開示防止)。\n return jsonData({ status: \"OK\", intent: \"toggleRead\", updatedStatus: { isStarred: updatedStatus.isStarred, isRead: updatedStatus.isRead }, operationTimestamp, reportHubId: String(reportHubId), clientTempId })\n }\n case \"addComment\": {\n const content = formData.get(\"content\") as string\n if (!content) {\n return jsonData({ error: \"Content required\" }, { status: 400 })\n }\n const newComment = await service.addComment(userId, reportHubId, content, businessDate, clientTempId)\n const safeComment = { ...newComment, userId: newComment.userId }\n return jsonData({ status: \"OK\", intent: \"addComment\", newComment: safeComment, reportHubId: String(reportHubId), clientTempId })\n }\n case \"deleteComment\": {\n const commentId = Number(formData.get(\"commentId\"))\n if (!commentId || Number.isNaN(commentId)) {\n return jsonData({ error: \"Invalid commentId\" }, { status: 400 })\n }\n try {\n await service.deleteComment(userId, reportHubId, commentId, businessDate, clientTempId)\n return jsonData({ status: \"OK\", reportHubId: String(reportHubId), deletedCommentId: String(commentId), clientTempId })\n } catch (e: unknown) {\n if (e instanceof Error) {\n if (e.message === \"Unauthorized\") {\n return jsonData({ error: \"Unauthorized\" }, { status: 403 })\n }\n if (e.message === \"Not Found\") {\n return jsonData({ error: \"Comment not found\" }, { status: 404 })\n }\n }\n throw e\n }\n }\n default:\n return jsonData({ error: \"Invalid intent\" }, { status: 400 })\n }\n }\n\n // ---------------- sse (リアルタイム更新) ----------------\n\n /**\n * SSE endpoint for real-time daily report updates via Redis Streams.\n * Redis Streams を使用した日報リアルタイム更新の SSE エンドポイント。\n */\n const sseLoader = async ({ request, params }: LoaderArgs) => {\n // 認証チェック\n const { user } = await authenticate(request, { failureRedirect: null })\n if (!user) {\n return new Response(\"Unauthorized\", { status: 401 })\n }\n\n // 内部ユーザー ID の解決\n const internalUserId = await service.getUserIdByExternalId(user.id)\n if (!internalUserId) {\n return new Response(\"Forbidden\", { status: 403 })\n }\n\n // エンドポイントの検証\n if (params.endpoint !== \"updates\") {\n return new Response(\"Not Found\", { status: 404 })\n }\n\n // URL から lastEventId を取得 (再接続時の catch-up 用)\n // クエリパラメータ優先、EventSource 自動再接続時の Last-Event-ID ヘッダーにフォールバック\n const url = new URL(request.url)\n const lastEventId = url.searchParams.get(\"lastEventId\") || request.headers.get(\"Last-Event-ID\")\n\n const encoder = new TextEncoder()\n let controllerRef: ReadableStreamDefaultController<Uint8Array> | null = null\n let keepAliveInterval: ReturnType<typeof setInterval> | null = null\n let unsubscribe: (() => void) | null = null\n let isCleaningUp = false\n\n // クリーンアップ処理\n const cleanup = () => {\n if (isCleaningUp) return\n isCleaningUp = true\n\n // keep-alive タイマー停止\n if (keepAliveInterval) {\n clearInterval(keepAliveInterval)\n keepAliveInterval = null\n }\n\n // Fan-Out 購読解除 (entry / error 両リスナーを解除)\n if (unsubscribe) {\n unsubscribe()\n unsubscribe = null\n }\n\n // SSE ストリームの終了\n if (controllerRef) {\n try {\n if (controllerRef.desiredSize !== null) {\n controllerRef.close()\n }\n } catch (_e) {\n // ストリームが既に閉じている場合は無視\n } finally {\n controllerRef = null\n }\n }\n\n isCleaningUp = false\n }\n\n /**\n * Processes a single Redis Stream entry and sends it to the SSE client.\n * Redis Stream のエントリを処理し、SSE クライアントに送信する。\n */\n const processEntry = (entryId: string, fields: Record<string, string>) => {\n // data フィールドの存在チェック\n if (!fields.data) return\n\n try {\n const raw = JSON.parse(fields.data)\n const result = dailyReportSseMessageSchema.safeParse(raw)\n if (!result.success) {\n sseLogger.error(\"SSE message validation failed:\", result.error.format())\n return // Fail-Closed: 不正なメッセージは送信しない\n }\n const parsed = result.data\n\n // recipientRawUserId によるフィルタリング\n const recipientRawUserId = typeof raw.recipientRawUserId === \"number\" ? raw.recipientRawUserId : undefined\n\n if (parsed.type === \"status-update\") {\n if (recipientRawUserId === undefined || recipientRawUserId !== internalUserId) {\n return\n }\n } else if (recipientRawUserId !== undefined && recipientRawUserId !== internalUserId) {\n return\n }\n\n // recipientRawUserId をクライアントに送信しない (内部 ID 漏洩防止)\n let sanitizedMessage: string\n if (\"recipientRawUserId\" in raw) {\n const { recipientRawUserId: _, ...rest } = raw\n sanitizedMessage = JSON.stringify(rest)\n } else {\n sanitizedMessage = fields.data\n }\n\n // SSE フォーマットで送信 (id フィールド付き)\n if (controllerRef && controllerRef.desiredSize !== null) {\n controllerRef.enqueue(encoder.encode(`id: ${entryId}\\ndata: ${sanitizedMessage}\\n\\n`))\n }\n } catch (e) {\n sseLogger.error(`[SSE:${internalUserId}] processEntry error:`, e)\n }\n }\n\n const stream = new ReadableStream({\n async start(controller) {\n // 1. controllerRef 設定\n controllerRef = controller\n\n // 2. Keep-Alive: 5 秒ごとにコメントを送信して接続維持\n keepAliveInterval = setInterval(() => {\n try {\n if (controllerRef && controllerRef.desiredSize !== null) {\n controllerRef.enqueue(encoder.encode(\": keep-alive\\n\\n\"))\n } else {\n cleanup()\n }\n } catch (_e) {\n cleanup()\n }\n }, 5000)\n\n // 3. クライアント切断時のクリーンアップ (全 await の前に登録)\n request.signal.addEventListener(\"abort\", () => {\n cleanup()\n })\n\n // catch-up の基準 ID (Fan-Out エントリとの重複排除に使用)\n let lastProcessedId = lastEventId || \"0-0\"\n\n // 4. Fan-Out 共有リーダーを購読 (catch-up より先に登録して取りこぼしを防止)\n unsubscribe = sseReader.subscribe(\n (entry: StreamEntry) => {\n // catch-up 済みエントリはスキップ (重複排除)\n if (isStreamIdLte(entry.id, lastProcessedId)) return\n lastProcessedId = entry.id\n processEntry(entry.id, entry.message)\n },\n (err: Error) => {\n sseLogger.error(`[SSE:${internalUserId}] Fan-Out reader error:`, err)\n cleanup()\n },\n )\n\n // 5. 初回接続時: 最新 Stream ID を取得して connected の id: に使用\n // (再接続時は lastEventId が既にあるためスキップ)\n if (!lastEventId) {\n try {\n const client = await redis?.getClient()\n if (client) {\n const latest = await client.xRevRange(streamKey, \"+\", \"-\", { COUNT: 1 })\n // Fan-Out が既に lastProcessedId を進めている場合は巻き戻さない\n if (latest.length > 0 && !isStreamIdLte(latest[0].id, lastProcessedId)) {\n lastProcessedId = latest[0].id\n }\n }\n } catch {\n /* 取得失敗時は現在の lastProcessedId を維持 */\n }\n }\n\n // 6. 接続完了通知 (id: 付きで再接続時の catch-up アンカーを提供)\n if (controllerRef && controllerRef.desiredSize !== null) {\n const connectedPayload = `data: ${JSON.stringify({ type: \"connected\" })}\\n\\n`\n const sseMessage = lastProcessedId !== \"0-0\" ? `id: ${lastProcessedId}\\n${connectedPayload}` : connectedPayload\n controllerRef.enqueue(encoder.encode(sseMessage))\n }\n\n // 7. catch-up: lastEventId が指定されている場合、それ以降のエントリを一括取得\n if (lastEventId) {\n try {\n const client = await redis?.getClient()\n if (client) {\n const catchUpEntries = await client.xRange(streamKey, lastEventId, \"+\", { COUNT: 1000 })\n for (const entry of catchUpEntries) {\n // xRange は inclusive なので lastEventId 自身はスキップ\n if (entry.id === lastEventId) continue\n // Fan-Out が xRange await 中に処理済みのエントリはスキップ (重複排除)\n if (isStreamIdLte(entry.id, lastProcessedId)) continue\n lastProcessedId = entry.id\n processEntry(entry.id, entry.message as Record<string, string>)\n }\n }\n } catch (e) {\n sseLogger.error(`[SSE:${internalUserId}] catch-up xRange error:`, e)\n // catch-up 失敗時は Fan-Out のみで継続\n }\n }\n },\n cancel() {\n cleanup()\n },\n })\n\n return new Response(stream, {\n headers: {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n },\n })\n }\n\n return {\n index: { loader: indexLoader },\n api: { loader: apiLoader, action: apiAction },\n sse: { loader: sseLoader },\n }\n}\n","/**\n * Data-access service for daily reports (drizzle mssql), fully DI-configured.\n * 日報データアクセスサービス (drizzle mssql)。依存はすべて DI で注入する。\n *\n * キャッシュキー・無効化順序・SSE publish 順序を含むデータアクセスロジックの中核。\n */\nimport { aliasedTable, and, asc, desc, eq, getColumns, inArray, isNull, or, sql } from \"drizzle-orm\"\nimport { normalizeBusinessDateKey } from \"../shared/business-date\"\nimport { createLogger, type DailyReportLogger, LogLevel } from \"../shared/logger\"\nimport { commentAddMessageSchema, commentDeleteMessageSchema, reportCreateMessageSchema, reportDeleteMessageSchema, reportPublishMessageSchema, reportUpdateMessageSchema, statusUpdateMessageSchema } from \"../shared/sse-schema\"\nimport type { DailyReportComment, DailyReportCommentItem, DailyReportDetail, DailyReportInterviewer, DailyReportItem, DailyReportLabelDef } from \"../shared/types\"\nimport type { EpochStore, SqlResultCache, SqlResultCacheQueryOptions } from \"./cache\"\nimport type { DailyReportExternalSource } from \"./external-source\"\nimport type { DailyReportEncodeUserId, DailyReportRedisProvider, DailyReportResolveUserId } from \"./ports\"\nimport type { DailyReportTables, DailyReportUserTable } from \"./schema\"\n\nexport type { SqlResultCacheQueryOptions }\n\n// ---- 行型 (drizzle $inferSelect の構造的置き換え) ----\n\n/** DailyReportHub の行型。 */\nexport type DailyReportHubRow = {\n id: number\n sourceType: string\n sourceId: string\n sourceIdNum: number | null\n businessDate: Date | string | null\n userId: number | null\n title: string | null\n summary: string | null\n createdAt: Date | string\n createdBy: string\n updatedAt: Date | string\n updatedBy: string\n deletedAt: Date | string | null\n deletedBy: string | null\n}\n\n/** DailyReportInternal の行型。 */\nexport type DailyReportInternalRow = {\n hubId: number\n body: string | null\n metadata: string | null\n createdAt: Date | string\n createdBy: string\n updatedAt: Date | string\n updatedBy: string\n}\n\n/** DailyReportUserStatus の行型。 */\nexport type DailyReportUserStatusRow = {\n hubId: number\n userId: number\n isRead: boolean\n isStarred: boolean\n createdAt: Date | string\n createdBy: string\n updatedAt: Date | string\n updatedBy: string\n}\n\n/** DailyReportComment の行型。 */\nexport type DailyReportCommentRow = {\n id: number\n hubId: number\n userId: number\n body: string\n createdAt: Date | string\n createdBy: string\n updatedAt: Date | string\n updatedBy: string\n}\n\n// ---- 必要最小の drizzle ビルダ形 (公開契約はサービス関数のシグネチャで厳格化) ----\n\ntype Rows<T> = PromiseLike<T[]>\ninterface SelectChain<T> extends Rows<T> {\n from(t: unknown): SelectChain<T>\n innerJoin(t: unknown, on: unknown): SelectChain<T>\n leftJoin(t: unknown, on: unknown): SelectChain<T>\n where(cond: unknown): SelectChain<T>\n orderBy(...cols: unknown[]): SelectChain<T>\n top(n: number): SelectChain<T>\n}\ninterface InsertChain<T> {\n output(): { values(v: unknown): PromiseLike<T[]> }\n values(v: unknown): PromiseLike<unknown>\n}\ninterface UpdateChain<T> {\n set(v: unknown): {\n where(cond: unknown): PromiseLike<unknown>\n output(): { where(cond: unknown): PromiseLike<T[]> }\n }\n}\ninterface DeleteChain {\n where(cond: unknown): PromiseLike<unknown>\n}\n\n/** サービスが要求する drizzle mssql データベースの最小面 (トランザクション込み)。 */\nexport interface DailyReportDb {\n select<T = Record<string, unknown>>(fields?: unknown): SelectChain<T>\n insert<T = Record<string, unknown>>(t: unknown): InsertChain<T>\n update<T = Record<string, unknown>>(t: unknown): UpdateChain<T>\n delete(t: unknown): DeleteChain\n transaction<T>(fn: (tx: DailyReportDb) => Promise<T>, config?: unknown): Promise<T>\n}\n\n/** getColumns へ構造型テーブルを渡すための境界キャスト。 */\nconst cols = (t: unknown) => getColumns(t as Parameters<typeof getColumns>[0])\n\n// ---- サービス設定 ----\n\nexport type DailyReportServiceConfig = {\n /** drizzle mssql データベースハンドル。 */\n db: DailyReportDb\n /** 日報 6 テーブル (アプリ既存モデルまたは defineDailyReportSchema 生成物)。 */\n tables: DailyReportTables\n /** 表示名解決に使う外部ユーザーテーブル ({ id, displayName })。 */\n userTable: DailyReportUserTable\n /** 外部ユーザー ID → 内部数値 ID の解決ポート。 */\n resolveUserId: DailyReportResolveUserId\n /** 内部数値 ID の難読化ポート。 */\n encodeUserId: DailyReportEncodeUserId\n /** SSE publish / epoch 用 redis (省略時は SSE publish スキップ・epoch 無効)。 */\n redis?: DailyReportRedisProvider\n /** レガシー等の外部ソースアダプタ群。 */\n externalSources?: DailyReportExternalSource[]\n /** SQL 結果キャッシュ (facade が生成して注入)。 */\n cache: SqlResultCache\n /** クロスワーカー epoch ストア (facade が生成して注入)。 */\n epochs: EpochStore\n /**\n * 下書きラベル名 (単一または配列で指定可能)。\n * 消費アプリ側で自 DB のラベル名や作成区分の候補名 (\"下書き\", \"DRAFT\" など) を注入できる。\n */\n draftLabelName?: string\n draftLabelNames?: string[]\n /** SSE Redis Stream キー (既定 \"daily-report:sse-stream\")。 */\n streamKey?: string\n /** SSE Stream の MAXLEN (既定 10000)。 */\n streamMaxLen?: number\n /** IDs 一覧キャッシュ TTL (既定 180,000ms)。 */\n idsTtlMs?: number\n /** 営業日別キャッシュ TTL (既定 300,000ms)。 */\n businessDateTtlMs?: number\n /** 外部 ID → 内部 ID のプロセス内キャッシュを無効化 (テスト用)。 */\n disableUserIdCache?: boolean\n /** ロガー (既定は console ベース)。 */\n logger?: DailyReportLogger\n}\n\n/** createDailyReportService の返却型。 */\nexport type DailyReportService = ReturnType<typeof createDailyReportService>\n\n/**\n * Creates the daily-report data-access service bound to the injected dependencies.\n * 注入された依存に束縛された日報データアクセスサービスを生成する処理。\n */\nexport function createDailyReportService(config: DailyReportServiceConfig) {\n const { db, tables, userTable: users, resolveUserId, encodeUserId, redis, cache: sqlResultCache, epochs } = config\n const { hub: DailyReportHub, internal: DailyReportInternal, comment: DailyReportCommentModel, label: DailyReportLabel, hubLabel: DailyReportHub_Label, userStatus: DailyReportUserStatus } = tables\n const externalSources = config.externalSources ?? []\n const draftLabelNames = config.draftLabelNames && config.draftLabelNames.length > 0 ? config.draftLabelNames : [config.draftLabelName ?? \"下書き\"]\n const draftLabelName = draftLabelNames[0] ?? \"下書き\"\n const logger = config.logger ?? createLogger(LogLevel.INFO, \"[DailyReportService]\")\n\n const DAILY_REPORT_IDS_CACHE_KEY = \"daily-report:ids\"\n const DAILY_REPORT_IDS_TTL_MS = config.idsTtlMs ?? 180_000\n const DAILY_REPORT_IDS_EPOCH_KEY = \"daily-report:ids:epoch\"\n\n const DAILY_REPORT_BUSINESS_DATE_CACHE_PREFIX = \"daily-report:business-date:\"\n const DAILY_REPORT_BUSINESS_DATE_TTL_MS = config.businessDateTtlMs ?? 300_000\n\n /** 営業日キャッシュのクロスワーカー epoch プレフィックス */\n const DAILY_REPORT_DATE_EPOCH_PREFIX = \"daily-report:date-epoch:\"\n /** レポート詳細キャッシュのクロスワーカー epoch プレフィックス */\n const DAILY_REPORT_DETAIL_EPOCH_PREFIX = \"daily-report:detail-epoch:\"\n\n const DAILY_REPORT_SSE_STREAM_KEY = config.streamKey ?? \"daily-report:sse-stream\"\n const DAILY_REPORT_SSE_STREAM_MAXLEN = config.streamMaxLen ?? 10000\n\n const incrementRedisEpoch = (key: string) => epochs.incrementEpoch(key)\n\n /**\n * Publishes a message to the SSE Redis Stream.\n * SSE 用の Redis Stream にメッセージを追加する。\n */\n const publishToSseStream = async (message: Record<string, unknown>, callerName: string): Promise<void> => {\n const client = await redis?.getClient()\n if (!client) {\n logger.warn(`[SSE] Redis client unavailable (${callerName})`)\n return\n }\n const publishStartMs = Date.now()\n try {\n await client.xAdd(DAILY_REPORT_SSE_STREAM_KEY, \"*\", { data: JSON.stringify(message) }, { TRIM: { strategy: \"MAXLEN\", strategyModifier: \"~\", threshold: DAILY_REPORT_SSE_STREAM_MAXLEN } })\n const publishDurationMs = Date.now() - publishStartMs\n if (publishDurationMs > 1000) {\n logger.warn(`[SSE] Slow publish (${callerName}): ${publishDurationMs}ms`)\n }\n } catch (e) {\n logger.error(`[SSE] Redis publish failed (${callerName}):`, e)\n }\n }\n\n type DatePattern = \"YYYY-MM-DD HH:mm:ss\" | \"YYYY-MM-DD\"\n\n /**\n * Formats date-like values to a given pattern (ja-JP locale semantics preserved).\n * 日付相当の値を指定パターンに整形する処理。\n */\n const formatDateValue = (value: Date | string | null | undefined, pattern: DatePattern): string | null => {\n if (!value) {\n return null\n }\n\n const dateValue = value instanceof Date ? value : new Date(value)\n if (Number.isNaN(dateValue.getTime())) {\n return typeof value === \"string\" ? value : null\n }\n\n // ja-JP ロケールの YYYY/MM/DD (HH:mm:ss) を生成しハイフン区切りへ正規化する\n const opts: Intl.DateTimeFormatOptions = pattern === \"YYYY-MM-DD HH:mm:ss\" ? { year: \"numeric\", month: \"2-digit\", day: \"2-digit\", hour: \"2-digit\", minute: \"2-digit\", second: \"2-digit\" } : { year: \"numeric\", month: \"2-digit\", day: \"2-digit\" }\n const formatted = pattern === \"YYYY-MM-DD HH:mm:ss\" ? dateValue.toLocaleString(\"ja-JP\", opts) : dateValue.toLocaleDateString(\"ja-JP\", opts)\n return formatted.replace(/\\//g, \"-\")\n }\n\n /**\n * Masks a raw audit-actor value (created_by / updated_by) before client exposure.\n * 監査列 (created_by / updated_by) をクライアント公開前にマスクする処理。\n *\n * Internal 由来の値は String(userId) の生内部 ID のため、数値なら encodeUserId で難読化する。\n * 非数値 (外部ソース由来の氏名やシステム文字列) はそのまま返す。null / 空文字は null。\n * userId フィールドと同じ難読化を監査列にも適用し、内部 ID の横流し漏洩を防ぐ。\n */\n const maskAuditActor = (value: string | null | undefined): string | null => {\n if (value == null || value === \"\") return null\n return /^\\d+$/.test(value) ? encodeUserId(Number(value)) : value\n }\n\n type RawJsonComment = {\n id: number\n content: string\n createdAt: string\n userId: number\n userName: string\n }\n\n type RawJsonLabel = {\n id: number\n name: string\n color: string | null\n }\n\n /** 詳細クエリ 1 行 (外部ソース列は `ext_<sourceType>` キーで同居)。 */\n type HubQueryRow = {\n hub: DailyReportHubRow\n internal: DailyReportInternalRow | null\n isRead?: boolean | null\n isStarred?: boolean | null\n creatorName?: string | null\n } & Record<string, unknown>\n\n type HubRecord = HubQueryRow & {\n labels?: RawJsonLabel[]\n comments?: RawJsonComment[]\n }\n\n /** 外部ソースアダプタの select 追加フィールドを構築する。 */\n const externalSelections = (): Record<string, unknown> => {\n const out: Record<string, unknown> = {}\n for (const adapter of externalSources) {\n out[`ext_${adapter.sourceType}`] = cols(adapter.table)\n }\n return out\n }\n\n /** 外部ソースアダプタの LEFT JOIN を select チェーンへ適用する。 */\n const applyExternalJoins = <T>(chain: SelectChain<T>): SelectChain<T> => {\n let c = chain\n for (const adapter of externalSources) {\n c = c.leftJoin(adapter.table, and(eq(DailyReportHub.sourceType, adapter.sourceType), eq(DailyReportHub.sourceIdNum, adapter.idColumn)))\n }\n return c\n }\n\n /**\n * Converts a Hub record into a DailyReportDetail structure.\n * Hub レコードを DailyReportDetail に変換する処理。\n */\n const mapHubRecord = (row: HubRecord, currentUserId?: number): DailyReportDetail => {\n const { hub, internal } = row\n\n let content = hub.summary\n let interviewers: DailyReportInterviewer[] = []\n let category: string | null = null\n let creationCategory: string | null = null\n let visitTimeFrom: string | null = null\n let visitTimeTo: string | null = null\n let customerName: string | null = null\n let employeeName: string | null = null\n let comments: DailyReportComment[] = []\n\n // 外部ソースアダプタ優先 → Internal の順で表示フィールドを解決する\n const adapter = externalSources.find((a) => a.sourceType === hub.sourceType)\n const externalRow = adapter ? (row[`ext_${adapter.sourceType}`] as Record<string, unknown> | null | undefined) : undefined\n if (adapter && externalRow) {\n const fields = adapter.mapRow(externalRow)\n if (fields.content !== undefined) content = fields.content\n if (fields.employeeName !== undefined) employeeName = fields.employeeName\n if (fields.category !== undefined) category = fields.category\n if (fields.creationCategory !== undefined) creationCategory = fields.creationCategory\n if (fields.visitTimeFrom !== undefined) visitTimeFrom = fields.visitTimeFrom\n if (fields.visitTimeTo !== undefined) visitTimeTo = fields.visitTimeTo\n if (fields.customerName !== undefined) customerName = fields.customerName\n if (fields.interviewers !== undefined) interviewers = fields.interviewers\n if (fields.comments !== undefined) comments = fields.comments\n } else if (hub.sourceType === \"Internal\" && internal) {\n content = internal.body\n // Internal specific mappings if any\n }\n\n const labelsRaw = (row.labels ?? []).map((l) => ({\n id: l.id,\n name: l.name,\n color: l.color,\n }))\n const commentItemsRaw = row.comments ?? []\n\n // 作成区分がアプリ設定の下書き候補に合致する場合、ラベル一覧に下書きラベルが含まれるよう補完\n const isCreationCategoryDraft = creationCategory ? draftLabelNames.includes(creationCategory) : false\n if (isCreationCategoryDraft && cachedDraftLabelId && !labelsRaw.some((l) => l.id === cachedDraftLabelId)) {\n labelsRaw.push({\n id: cachedDraftLabelId,\n name: draftLabelName,\n color: null,\n })\n }\n\n return {\n reportHubId: hub.id,\n date: formatDateValue(hub.businessDate, \"YYYY-MM-DD\"),\n author: row.creatorName ?? maskAuditActor(hub.createdBy) ?? \"\",\n userId: hub.userId ? encodeUserId(hub.userId) : \"\",\n sourceType: hub.sourceType ?? \"Internal\",\n createdAt: formatDateValue(hub.createdAt, \"YYYY-MM-DD HH:mm:ss\"),\n updatedAt: formatDateValue(hub.updatedAt, \"YYYY-MM-DD HH:mm:ss\"),\n updatedBy: maskAuditActor(hub.updatedBy),\n employeeName: employeeName ?? row.creatorName ?? maskAuditActor(hub.createdBy),\n category,\n creationCategory,\n visitTimeFrom,\n visitTimeTo,\n customerName,\n interviewers,\n subject: hub.title,\n content,\n comments,\n isRead: row.isRead ?? false,\n isStarred: row.isStarred ?? false,\n labels: labelsRaw,\n commentItems: commentItemsRaw.map((c) => ({\n ...c,\n userId: encodeUserId(c.userId),\n isMine: currentUserId ? c.userId === currentUserId : false,\n })),\n }\n }\n\n /**\n * Fetches daily report IDs alongside normalized business dates.\n * 日報 ID と正規化済み営業日を取得する内部処理。\n */\n const fetchDailyReportIdsByUserId = async (userId: number): Promise<DailyReportItem[]> => {\n const DraftLabelRelation = aliasedTable(DailyReportHub_Label as unknown as Parameters<typeof aliasedTable>[0], \"draft_label\") as unknown as typeof DailyReportHub_Label\n const draftLabelId = await getDraftLabelId()\n\n const result = await db\n .select<{ reportHubId: number; businessDate: Date | string | null; sourceType: string }>({\n reportHubId: DailyReportHub.id,\n businessDate: DailyReportHub.businessDate,\n sourceType: DailyReportHub.sourceType,\n })\n .from(DailyReportHub)\n .leftJoin(DraftLabelRelation, and(eq(DraftLabelRelation.hubId, DailyReportHub.id), eq(DraftLabelRelation.labelId, draftLabelId ?? -1)))\n .where(and(isNull(DailyReportHub.deletedAt), or(eq(DailyReportHub.userId, userId), isNull(DailyReportHub.userId), isNull(DraftLabelRelation.hubId))))\n .orderBy(desc(DailyReportHub.businessDate), desc(DailyReportHub.id))\n\n return result.map((item) => ({\n ...item,\n businessDate: formatDateValue(item.businessDate, \"YYYY-MM-DD\"),\n }))\n }\n\n /**\n * Fetches daily report details for the provided business date.\n * 指定した営業日の日報詳細を取得する内部処理。\n */\n const fetchDailyReportsByBusinessDate = async (normalizedBusinessDate: string): Promise<DailyReportDetail[]> => {\n const rows = await applyExternalJoins(\n db\n .select<HubQueryRow>({\n hub: cols(DailyReportHub),\n internal: cols(DailyReportInternal),\n creatorName: users.displayName,\n ...externalSelections(),\n })\n .from(DailyReportHub),\n )\n .leftJoin(DailyReportInternal, eq(DailyReportHub.id, DailyReportInternal.hubId))\n .leftJoin(users, eq(DailyReportHub.userId, users.id))\n .where(and(eq(DailyReportHub.businessDate, sql`${normalizedBusinessDate}`), isNull(DailyReportHub.deletedAt)))\n .orderBy(desc(DailyReportHub.id))\n\n return rows.map((row) => mapHubRecord(row))\n }\n\n /**\n * Builds a deterministic cache key for business-date caches.\n * 営業日キャッシュ用の一意キーを生成する処理。\n */\n const buildBusinessDateCacheKey = (normalizedBusinessDate: string): string => {\n return `${DAILY_REPORT_BUSINESS_DATE_CACHE_PREFIX}${normalizedBusinessDate}`\n }\n\n /**\n * Resolves the internal user ID from an external ID (with process-local caching).\n * 外部 ID から内部ユーザー ID を解決する (プロセス内キャッシュ付き)。\n */\n const userIdCache = new Map<string, number>()\n\n const getUserIdByExternalId = async (externalId: string): Promise<number | null> => {\n if (!config.disableUserIdCache && userIdCache.has(externalId)) {\n return userIdCache.get(externalId) ?? null\n }\n\n const userId = await resolveUserId(externalId)\n if (userId !== null && !config.disableUserIdCache) {\n userIdCache.set(externalId, userId)\n }\n return userId\n }\n\n /**\n * Retrieves daily report IDs and their normalized business dates.\n * 日報 ID と正規化済み営業日を取得する処理。\n */\n const getDailyReportIdsByExternalId = async (externalId: string, { forceRefresh = false, snapshot = false, ttlMsOverride }: SqlResultCacheQueryOptions = {}): Promise<DailyReportItem[]> => {\n const userId = await getUserIdByExternalId(externalId)\n if (!userId) {\n return []\n }\n\n const cacheKey = `${DAILY_REPORT_IDS_CACHE_KEY}:user:${userId}`\n return sqlResultCache.getOrFetch<DailyReportItem>({\n cacheKey,\n fetcher: async () => {\n const res = await fetchDailyReportIdsByUserId(userId)\n return res\n },\n forceRefresh,\n snapshot,\n ttlMsOverride: ttlMsOverride ?? DAILY_REPORT_IDS_TTL_MS,\n epochKey: DAILY_REPORT_IDS_EPOCH_KEY,\n }) as Promise<DailyReportItem[]>\n }\n\n /**\n * Retrieves daily report details for a business date (⚠️ NOT per-user filtered).\n * 指定した営業日に紐づく日報詳細を取得する処理 (⚠️ ユーザー別フィルタなし)。\n *\n * ⚠️ SECURITY: この関数は下書きラベルによる可視性フィルタ (他ユーザーの下書きを隠す) と\n * ユーザー別の既読/スター状態を **適用しない**。取得結果は全ユーザーの下書きを含み得るため、\n * HTTP レスポンス / SSR loader / SSE へ **直接返してはならない**。ユーザー向け配信には\n * 必ず {@link getDailyReportsByBusinessDateByExternalId} を使うこと。\n * (本関数は管理・バッチ・テスト用途に限定する。)\n */\n const getDailyReportsByBusinessDate = (businessDate: string, { forceRefresh = false, snapshot = false, ttlMsOverride }: SqlResultCacheQueryOptions = {}): Promise<DailyReportDetail[]> => {\n const normalizedBusinessDate = normalizeBusinessDateKey(businessDate)\n if (!normalizedBusinessDate) {\n return Promise.resolve([])\n }\n\n return sqlResultCache.getOrFetch<DailyReportDetail>({\n cacheKey: buildBusinessDateCacheKey(normalizedBusinessDate),\n fetcher: () => fetchDailyReportsByBusinessDate(normalizedBusinessDate),\n forceRefresh,\n snapshot,\n ttlMsOverride: ttlMsOverride ?? DAILY_REPORT_BUSINESS_DATE_TTL_MS,\n }) as Promise<DailyReportDetail[]>\n }\n\n /**\n * Retrieves the ID of the draft label.\n * 下書きラベルの ID を取得する(キャッシュ付き)。\n */\n let cachedDraftLabelId: number | null = null\n const getDraftLabelId = async (): Promise<number | null> => {\n if (cachedDraftLabelId !== null) return cachedDraftLabelId\n const [label] = await db.select<{ id: number }>({ id: DailyReportLabel.id }).top(1).from(DailyReportLabel).where(inArray(DailyReportLabel.name, draftLabelNames))\n\n if (label) {\n cachedDraftLabelId = label.id\n return cachedDraftLabelId\n }\n return null\n }\n\n /**\n * Retrieves daily report details for a business date with user-specific status and relations.\n * 指定した営業日の日報詳細を、ユーザー状態と関連データを含めて取得する。\n */\n const getDailyReportsByBusinessDateByExternalId = async (businessDate: string, externalId: string, { forceRefresh = false, snapshot = false, ttlMsOverride }: SqlResultCacheQueryOptions = {}): Promise<DailyReportDetail[]> => {\n const userId = await getUserIdByExternalId(externalId)\n if (!userId) {\n return []\n }\n\n const normalizedBusinessDate = normalizeBusinessDateKey(businessDate)\n if (!normalizedBusinessDate) {\n return []\n }\n\n const cacheKey = `daily-report:date:${normalizedBusinessDate}:user:${userId}`\n\n return sqlResultCache.getOrFetch<DailyReportDetail>({\n cacheKey,\n fetcher: async () => {\n const draftLabelId = await getDraftLabelId()\n const DraftLabelRelation = aliasedTable(DailyReportHub_Label as unknown as Parameters<typeof aliasedTable>[0], \"draft_label\") as unknown as typeof DailyReportHub_Label\n\n const rows = await applyExternalJoins(\n db\n .select<HubQueryRow>({\n hub: cols(DailyReportHub),\n internal: cols(DailyReportInternal),\n isRead: DailyReportUserStatus.isRead,\n isStarred: DailyReportUserStatus.isStarred,\n creatorName: users.displayName,\n ...externalSelections(),\n })\n .from(DailyReportHub),\n )\n .leftJoin(DailyReportInternal, eq(DailyReportHub.id, DailyReportInternal.hubId))\n .leftJoin(DailyReportUserStatus, and(eq(DailyReportHub.id, DailyReportUserStatus.hubId), eq(DailyReportUserStatus.userId, userId)))\n .leftJoin(DraftLabelRelation, and(eq(DraftLabelRelation.hubId, DailyReportHub.id), eq(DraftLabelRelation.labelId, draftLabelId ?? -1)))\n .leftJoin(users, eq(DailyReportHub.userId, users.id))\n .where(and(eq(DailyReportHub.businessDate, sql`${normalizedBusinessDate}`), isNull(DailyReportHub.deletedAt), or(eq(DailyReportHub.userId, userId), isNull(DraftLabelRelation.hubId))))\n .orderBy(desc(DailyReportHub.id))\n\n if (rows.length === 0) {\n return []\n }\n\n const hubIds = rows.map((r) => r.hub.id)\n\n // Drizzle ORM batch queries to fetch labels and comments\n const allLabels = await db\n .select<{ hubId: number; id: number; name: string; color: string | null }>({\n hubId: DailyReportHub_Label.hubId,\n id: DailyReportLabel.id,\n name: DailyReportLabel.name,\n color: DailyReportLabel.color,\n })\n .from(DailyReportHub_Label)\n .innerJoin(DailyReportLabel, eq(DailyReportHub_Label.labelId, DailyReportLabel.id))\n .where(inArray(DailyReportHub_Label.hubId, hubIds))\n\n const allComments = await db\n .select<{ hubId: number; id: number; body: string; createdAt: Date | string; userId: number; userName: string | null }>({\n hubId: DailyReportCommentModel.hubId,\n id: DailyReportCommentModel.id,\n body: DailyReportCommentModel.body,\n createdAt: DailyReportCommentModel.createdAt,\n userId: DailyReportCommentModel.userId,\n userName: users.displayName,\n })\n .from(DailyReportCommentModel)\n .leftJoin(users, eq(DailyReportCommentModel.userId, users.id))\n .where(inArray(DailyReportCommentModel.hubId, hubIds))\n .orderBy(asc(DailyReportCommentModel.createdAt))\n\n const labelsMap = new Map<number, RawJsonLabel[]>()\n const commentsMap = new Map<number, RawJsonComment[]>()\n\n for (const label of allLabels) {\n let list = labelsMap.get(label.hubId)\n if (!list) {\n list = []\n labelsMap.set(label.hubId, list)\n }\n list.push({\n id: label.id,\n name: label.name,\n color: label.color,\n })\n }\n\n for (const comment of allComments) {\n let list = commentsMap.get(comment.hubId)\n if (!list) {\n list = []\n commentsMap.set(comment.hubId, list)\n }\n list.push({\n id: comment.id,\n content: comment.body,\n createdAt: formatDateValue(comment.createdAt, \"YYYY-MM-DD HH:mm:ss\") || \"\",\n userId: comment.userId,\n userName: comment.userName || \"\",\n })\n }\n\n return rows.map((row) => {\n const hubId = row.hub.id\n return mapHubRecord(\n {\n ...row,\n labels: labelsMap.get(hubId) || [],\n comments: commentsMap.get(hubId) || [],\n },\n userId,\n )\n })\n },\n forceRefresh,\n snapshot,\n ttlMsOverride: ttlMsOverride ?? DAILY_REPORT_BUSINESS_DATE_TTL_MS,\n epochKey: `${DAILY_REPORT_DATE_EPOCH_PREFIX}${normalizedBusinessDate}`,\n }) as Promise<DailyReportDetail[]>\n }\n\n /**\n * Retrieves a single daily report detail by ID with user-specific status and relations.\n * 指定した日報詳細を、ユーザー状態と関連データを含めて取得する。\n */\n const getDailyReportDetailById = async (reportHubId: number, userId: number, { forceRefresh = false, snapshot = false, ttlMsOverride }: SqlResultCacheQueryOptions = {}): Promise<DailyReportDetail | null> => {\n const cacheKey = `daily-report:detail:${reportHubId}:user:${userId}`\n\n const results = (await sqlResultCache.getOrFetch<DailyReportDetail>({\n cacheKey,\n fetcher: async () => {\n const rows = await applyExternalJoins(\n db\n .select<HubQueryRow>({\n hub: cols(DailyReportHub),\n internal: cols(DailyReportInternal),\n isRead: DailyReportUserStatus.isRead,\n isStarred: DailyReportUserStatus.isStarred,\n creatorName: users.displayName,\n ...externalSelections(),\n })\n .from(DailyReportHub),\n )\n .leftJoin(DailyReportInternal, eq(DailyReportHub.id, DailyReportInternal.hubId))\n .leftJoin(DailyReportUserStatus, and(eq(DailyReportHub.id, DailyReportUserStatus.hubId), eq(DailyReportUserStatus.userId, userId)))\n .leftJoin(users, eq(DailyReportHub.userId, users.id))\n .where(and(eq(DailyReportHub.id, reportHubId), isNull(DailyReportHub.deletedAt)))\n\n if (rows.length === 0) {\n return []\n }\n\n const hubId = rows[0].hub.id\n\n // Fetch labels and comments via Drizzle ORM\n const labels = await db\n .select<{ id: number; name: string; color: string | null }>({\n id: DailyReportLabel.id,\n name: DailyReportLabel.name,\n color: DailyReportLabel.color,\n })\n .from(DailyReportHub_Label)\n .innerJoin(DailyReportLabel, eq(DailyReportHub_Label.labelId, DailyReportLabel.id))\n .where(eq(DailyReportHub_Label.hubId, hubId))\n\n const comments = await db\n .select<{ id: number; body: string; createdAt: Date | string; userId: number; userName: string | null }>({\n id: DailyReportCommentModel.id,\n body: DailyReportCommentModel.body,\n createdAt: DailyReportCommentModel.createdAt,\n userId: DailyReportCommentModel.userId,\n userName: users.displayName,\n })\n .from(DailyReportCommentModel)\n .leftJoin(users, eq(DailyReportCommentModel.userId, users.id))\n .where(eq(DailyReportCommentModel.hubId, hubId))\n .orderBy(asc(DailyReportCommentModel.createdAt))\n\n const labelsMapped = labels.map((l) => ({\n id: l.id,\n name: l.name,\n color: l.color,\n }))\n\n const commentsMapped = comments.map((c) => ({\n id: c.id,\n content: c.body,\n createdAt: formatDateValue(c.createdAt, \"YYYY-MM-DD HH:mm:ss\") || \"\",\n userId: c.userId,\n userName: c.userName || \"\",\n }))\n\n return [\n mapHubRecord(\n {\n ...rows[0],\n labels: labelsMapped,\n comments: commentsMapped,\n },\n userId,\n ),\n ]\n },\n forceRefresh,\n snapshot,\n ttlMsOverride: ttlMsOverride ?? DAILY_REPORT_BUSINESS_DATE_TTL_MS,\n epochKey: `${DAILY_REPORT_DETAIL_EPOCH_PREFIX}${reportHubId}`,\n })) as DailyReportDetail[]\n\n return results[0] ?? null\n }\n\n /**\n * Retrieves a single daily report detail by ID with user-specific status and relations.\n * 指定した日報詳細を、ユーザー状態と関連データを含めて取得する。\n */\n const getDailyReportDetailByIdByExternalId = async (reportHubId: number, externalId: string, options: SqlResultCacheQueryOptions = {}): Promise<DailyReportDetail | null> => {\n const userId = await getUserIdByExternalId(externalId)\n if (!userId) {\n return null\n }\n return getDailyReportDetailById(reportHubId, userId, options)\n }\n\n /**\n * Sets the starred status of a daily report for a user.\n * ユーザーの日報スター状態を設定する。\n */\n const setStarStatus = async (userId: number, reportHubId: number, businessDate: string | null, isStarred: boolean, clientTempId: string): Promise<DailyReportUserStatusRow> => {\n const existing = await db\n .select<DailyReportUserStatusRow>()\n .top(1)\n .from(DailyReportUserStatus)\n .where(and(eq(DailyReportUserStatus.hubId, reportHubId), eq(DailyReportUserStatus.userId, userId)))\n\n let updatedStatus: DailyReportUserStatusRow\n\n if (existing.length > 0) {\n if (existing[0].isStarred !== isStarred) {\n const rows = await db\n .update<DailyReportUserStatusRow>(DailyReportUserStatus)\n .set({\n isStarred: isStarred,\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n .output()\n .where(and(eq(DailyReportUserStatus.hubId, reportHubId), eq(DailyReportUserStatus.userId, userId)))\n updatedStatus = rows[0]\n } else {\n updatedStatus = existing[0]\n }\n } else {\n const rows = await db\n .insert<DailyReportUserStatusRow>(DailyReportUserStatus)\n .output()\n .values({\n hubId: reportHubId,\n userId,\n isStarred: isStarred,\n isRead: false,\n createdAt: new Date(),\n createdBy: String(userId),\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n updatedStatus = rows[0]\n }\n\n if (businessDate) {\n const normalizedDate = normalizeBusinessDateKey(businessDate)\n if (normalizedDate) {\n sqlResultCache.invalidate(`daily-report:date:${normalizedDate}:user:${userId}`)\n await incrementRedisEpoch(`${DAILY_REPORT_DATE_EPOCH_PREFIX}${normalizedDate}`)\n }\n }\n sqlResultCache.invalidate(`daily-report:detail:${reportHubId}`)\n sqlResultCache.invalidate(`daily-report:detail:${reportHubId}:user:${userId}`)\n await incrementRedisEpoch(`${DAILY_REPORT_DETAIL_EPOCH_PREFIX}${reportHubId}`)\n\n await publishToSseStream(\n statusUpdateMessageSchema.parse({\n type: \"status-update\",\n reportHubId: reportHubId,\n recipientRawUserId: userId,\n statusType: \"star\",\n value: isStarred,\n clientTempId,\n }),\n \"setStarStatus\",\n )\n\n return updatedStatus\n }\n\n /**\n * Sets the read status of a daily report for a user.\n * ユーザーの日報既読状態を設定する。\n */\n const setReadStatus = async (userId: number, reportHubId: number, businessDate: string | null, isRead: boolean, clientTempId: string): Promise<DailyReportUserStatusRow> => {\n const existing = await db\n .select<DailyReportUserStatusRow>()\n .top(1)\n .from(DailyReportUserStatus)\n .where(and(eq(DailyReportUserStatus.hubId, reportHubId), eq(DailyReportUserStatus.userId, userId)))\n\n let updatedStatus: DailyReportUserStatusRow\n\n if (existing.length > 0) {\n if (existing[0].isRead !== isRead) {\n const rows = await db\n .update<DailyReportUserStatusRow>(DailyReportUserStatus)\n .set({\n isRead: isRead,\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n .output()\n .where(and(eq(DailyReportUserStatus.hubId, reportHubId), eq(DailyReportUserStatus.userId, userId)))\n updatedStatus = rows[0]\n } else {\n updatedStatus = existing[0]\n }\n } else {\n const rows = await db\n .insert<DailyReportUserStatusRow>(DailyReportUserStatus)\n .output()\n .values({\n hubId: reportHubId,\n userId,\n isRead: isRead,\n isStarred: false,\n createdAt: new Date(),\n createdBy: String(userId),\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n updatedStatus = rows[0]\n }\n\n if (businessDate) {\n const normalizedDate = normalizeBusinessDateKey(businessDate)\n if (normalizedDate) {\n sqlResultCache.invalidate(`daily-report:date:${normalizedDate}:user:${userId}`)\n await incrementRedisEpoch(`${DAILY_REPORT_DATE_EPOCH_PREFIX}${normalizedDate}`)\n }\n }\n sqlResultCache.invalidate(`daily-report:detail:${reportHubId}`)\n sqlResultCache.invalidate(`daily-report:detail:${reportHubId}:user:${userId}`)\n await incrementRedisEpoch(`${DAILY_REPORT_DETAIL_EPOCH_PREFIX}${reportHubId}`)\n\n await publishToSseStream(\n statusUpdateMessageSchema.parse({\n type: \"status-update\",\n reportHubId: reportHubId,\n recipientRawUserId: userId,\n statusType: \"read\",\n value: isRead,\n clientTempId,\n }),\n \"setReadStatus\",\n )\n\n return updatedStatus\n }\n\n /**\n * Adds a comment to a daily report.\n * 日報にコメントを追加する。\n */\n const addComment = async (userId: number, reportHubId: number, content: string, businessDate: string | null, clientTempId: string): Promise<DailyReportCommentItem> => {\n const [hub] = await db.select<{ sourceType: string }>({ sourceType: DailyReportHub.sourceType }).top(1).from(DailyReportHub).where(eq(DailyReportHub.id, reportHubId))\n if (hub && hub.sourceType.toLowerCase() !== \"internal\") {\n throw new Error(\"Comments are restricted for external daily report sources\")\n }\n\n const [inserted] = await db\n .insert<DailyReportCommentRow>(DailyReportCommentModel)\n .output()\n .values({\n hubId: reportHubId,\n userId,\n body: content,\n createdAt: new Date(),\n createdBy: String(userId),\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n\n const [user] = await db.select<{ displayName: string | null }>({ displayName: users.displayName }).top(1).from(users).where(eq(users.id, userId))\n const userName = user?.displayName ?? \"Unknown\"\n\n // コメントは全ユーザーに表示されるため、date キャッシュも全ユーザー分を無効化\n if (businessDate) {\n const normalizedDate = normalizeBusinessDateKey(businessDate)\n if (normalizedDate) {\n sqlResultCache.invalidatePrefix(`daily-report:date:${normalizedDate}:user:`)\n await incrementRedisEpoch(`${DAILY_REPORT_DATE_EPOCH_PREFIX}${normalizedDate}`)\n }\n }\n sqlResultCache.invalidate(`daily-report:detail:${reportHubId}`)\n sqlResultCache.invalidatePrefix(`daily-report:detail:${reportHubId}:user:`)\n await incrementRedisEpoch(`${DAILY_REPORT_DETAIL_EPOCH_PREFIX}${reportHubId}`)\n\n const commentItem: DailyReportCommentItem = {\n id: inserted.id,\n userId: encodeUserId(inserted.userId),\n userName: userName,\n content: inserted.body,\n createdAt: formatDateValue(inserted.createdAt, \"YYYY-MM-DD HH:mm:ss\") ?? \"\",\n isMine: true,\n }\n\n await publishToSseStream(\n commentAddMessageSchema.parse({\n type: \"comment-add\",\n reportHubId: reportHubId,\n comment: commentItem,\n clientTempId,\n }),\n \"addComment\",\n )\n\n return commentItem\n }\n\n /**\n * Finds a daily report comment by ID.\n * 日報コメントをIDで検索する。\n */\n const findDailyReportCommentById = async (tx: DailyReportDb, commentId: number) => {\n return await tx.select<DailyReportCommentRow>().top(1).from(DailyReportCommentModel).where(eq(DailyReportCommentModel.id, commentId))\n }\n\n /**\n * Deletes a comment from a daily report.\n * 日報のコメントを削除する。\n */\n const deleteComment = async (userId: number, reportHubId: number, commentId: number, businessDate: string | null, clientTempId: string): Promise<void> => {\n const comments = await findDailyReportCommentById(db, commentId)\n if (comments.length === 0) {\n throw new Error(\"Not Found\")\n }\n if (comments[0].userId !== userId) {\n throw new Error(\"Unauthorized\")\n }\n\n await db.delete(DailyReportCommentModel).where(eq(DailyReportCommentModel.id, commentId))\n\n // コメントは全ユーザーに表示されるため、date キャッシュも全ユーザー分を無効化\n if (businessDate) {\n const normalizedDate = normalizeBusinessDateKey(businessDate)\n if (normalizedDate) {\n sqlResultCache.invalidatePrefix(`daily-report:date:${normalizedDate}:user:`)\n await incrementRedisEpoch(`${DAILY_REPORT_DATE_EPOCH_PREFIX}${normalizedDate}`)\n }\n }\n sqlResultCache.invalidate(`daily-report:detail:${reportHubId}`)\n sqlResultCache.invalidatePrefix(`daily-report:detail:${reportHubId}:user:`)\n await incrementRedisEpoch(`${DAILY_REPORT_DETAIL_EPOCH_PREFIX}${reportHubId}`)\n\n await publishToSseStream(\n commentDeleteMessageSchema.parse({\n type: \"comment-delete\",\n reportHubId: reportHubId,\n commentId,\n clientTempId,\n }),\n \"deleteComment\",\n )\n }\n\n /**\n * Creates a new draft daily report.\n * 新しい日報(下書き)を作成する。\n */\n const createDailyReport = async (userId: number, businessDate: string, clientTempId: string): Promise<DailyReportDetail> => {\n logger.info(\"createDailyReport called\", { userId, businessDate })\n\n // ユーザー名取得\n const [user] = await db.select<{ displayName: string | null }>({ displayName: users.displayName }).from(users).where(eq(users.id, userId))\n const userName = user?.displayName ?? encodeUserId(userId)\n\n try {\n const result = await db.transaction(async (tx) => {\n logger.info(\"Starting transaction\")\n // 1. Hub作成\n const sourceId = `internal-temp-${Date.now()}-${Math.random()}` // 一時的なID\n const [hub] = await tx\n .insert<DailyReportHubRow>(DailyReportHub)\n .output()\n .values({\n sourceType: \"Internal\",\n sourceId: sourceId,\n businessDate: new Date(businessDate),\n userId: userId,\n title: \"(無題)\",\n createdAt: new Date(),\n createdBy: String(userId),\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n\n // sourceId を id と同じ値に更新 (Internal の場合の正規化)\n await tx\n .update(DailyReportHub)\n .set({ sourceId: String(hub.id) })\n .where(eq(DailyReportHub.id, hub.id))\n\n logger.info(\"Hub created\", hub)\n\n // 2. Internal作成\n await tx.insert(DailyReportInternal).values({\n hubId: hub.id,\n body: \"\",\n createdAt: new Date(),\n createdBy: String(userId),\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n logger.info(\"Internal created\")\n\n // 3. ラベル付与\n logger.info(\"Calling getDraftLabelId\")\n const draftLabelId = await getDraftLabelId()\n logger.info(\"draftLabelId\", draftLabelId)\n const labels: DailyReportLabelDef[] = []\n if (draftLabelId) {\n await tx.insert(DailyReportHub_Label).values({\n hubId: hub.id,\n labelId: draftLabelId,\n createdAt: new Date(),\n createdBy: String(userId),\n })\n labels.push({ id: draftLabelId, name: draftLabelName, color: null })\n }\n logger.info(\"Label assigned\")\n\n // 4. 詳細オブジェクト構築\n return {\n reportHubId: hub.id,\n date: businessDate,\n createdAt: formatDateValue(hub.createdAt, \"YYYY-MM-DD HH:mm:ss\"),\n author: userName,\n userId: encodeUserId(userId),\n sourceType: \"Internal\",\n employeeName: userName,\n // 監査列と同様、クライアント公開時は生内部 ID を難読化する (mapHubRecord と整合)\n updatedBy: encodeUserId(userId),\n updatedAt: formatDateValue(hub.updatedAt, \"YYYY-MM-DD HH:mm:ss\"),\n category: null,\n creationCategory: null,\n visitTimeFrom: null,\n visitTimeTo: null,\n customerName: null,\n interviewers: [],\n subject: hub.title,\n content: \"\",\n comments: [],\n isRead: true, // 自分で作ったので既読\n isStarred: false,\n labels: labels,\n commentItems: [],\n }\n })\n\n // キャッシュ無効化\n const normalizedBusinessDate = normalizeBusinessDateKey(businessDate)\n if (normalizedBusinessDate) {\n sqlResultCache.invalidate(`daily-report:date:${normalizedBusinessDate}:user:${userId}`)\n }\n sqlResultCache.invalidatePrefix(DAILY_REPORT_BUSINESS_DATE_CACHE_PREFIX)\n sqlResultCache.invalidate(`${DAILY_REPORT_IDS_CACHE_KEY}:user:${userId}`)\n // ❗ 順序重要: invalidate → incrementRedisEpoch → publishToSseStream\n await incrementRedisEpoch(DAILY_REPORT_IDS_EPOCH_KEY)\n if (normalizedBusinessDate) {\n await incrementRedisEpoch(`${DAILY_REPORT_DATE_EPOCH_PREFIX}${normalizedBusinessDate}`)\n }\n\n const fullDetail = await getDailyReportDetailById(result.reportHubId, userId, { forceRefresh: true })\n\n if (fullDetail) {\n // 下書き判定\n const draftLabelId = await getDraftLabelId()\n const isDraft = draftLabelId ? fullDetail.labels.some((l) => l.id === draftLabelId) : false\n await publishToSseStream(\n reportCreateMessageSchema.parse({\n type: \"report-create\",\n reportHubId: fullDetail.reportHubId,\n report: fullDetail,\n clientTempId,\n recipientRawUserId: isDraft ? userId : undefined,\n }),\n \"createDailyReport\",\n )\n }\n\n return fullDetail ?? result\n } catch (e) {\n logger.error(\"Error in createDailyReport\", e)\n throw e\n }\n }\n\n // --- Internal Repository Layer ---\n\n /**\n * Finds a DailyReportHub by ID.\n * 日報HubをIDで検索する。\n */\n const findDailyReportHubById = async (tx: DailyReportDb, reportHubId: number) => {\n return await tx.select<DailyReportHubRow>().top(1).from(DailyReportHub).where(eq(DailyReportHub.id, reportHubId))\n }\n\n /**\n * Updates a DailyReportHub.\n * 日報Hubを更新する。\n */\n const updateDailyReportHub = async (tx: DailyReportDb, reportHubId: number, data: Partial<DailyReportHubRow>) => {\n await tx.update(DailyReportHub).set(data).where(eq(DailyReportHub.id, reportHubId))\n }\n\n /**\n * Updates a DailyReportInternal.\n * 日報Internalを更新する。\n */\n const updateDailyReportInternal = async (tx: DailyReportDb, hubId: number, data: Partial<DailyReportInternalRow>) => {\n await tx.update(DailyReportInternal).set(data).where(eq(DailyReportInternal.hubId, hubId))\n }\n\n /**\n * Deletes a label from a DailyReportHub.\n * 日報Hubからラベルを削除する。\n */\n const deleteDailyReportLabel = async (tx: DailyReportDb, hubId: number, labelId: number) => {\n await tx.delete(DailyReportHub_Label).where(and(eq(DailyReportHub_Label.hubId, hubId), eq(DailyReportHub_Label.labelId, labelId)))\n }\n\n // --- Service Layer ---\n\n /**\n * Logically deletes a daily report.\n * 日報を論理削除する。\n */\n const deleteDailyReport = async (reportHubId: number, userId: number, clientTempId: string): Promise<void> => {\n const report = await findDailyReportHubById(db, reportHubId)\n if (!report.length || report[0].deletedAt) {\n throw new Error(\"Not Found\")\n }\n if (report[0].userId !== userId) {\n throw new Error(\"Unauthorized\")\n }\n\n await updateDailyReportHub(db, reportHubId, {\n deletedAt: new Date(),\n deletedBy: String(userId),\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n\n // キャッシュ無効化\n sqlResultCache.invalidatePrefix(`daily-report:detail:${reportHubId}:user:`)\n sqlResultCache.invalidatePrefix(DAILY_REPORT_BUSINESS_DATE_CACHE_PREFIX)\n // 削除されたレポートは全ユーザーの一覧から消えるため、全ユーザーの IDs キャッシュを invalidate\n sqlResultCache.invalidatePrefix(DAILY_REPORT_IDS_CACHE_KEY)\n // ❗ 順序重要: invalidatePrefix → incrementRedisEpoch → publishToSseStream\n await incrementRedisEpoch(DAILY_REPORT_IDS_EPOCH_KEY)\n if (report[0].businessDate) {\n const normalizedDate = normalizeBusinessDateKey(formatDateValue(report[0].businessDate, \"YYYY-MM-DD\"))\n if (normalizedDate) {\n await incrementRedisEpoch(`${DAILY_REPORT_DATE_EPOCH_PREFIX}${normalizedDate}`)\n }\n }\n\n await publishToSseStream(\n reportDeleteMessageSchema.parse({\n type: \"report-delete\",\n reportHubId: reportHubId,\n clientTempId,\n }),\n \"deleteDailyReport\",\n )\n }\n\n /**\n * Updates an existing daily report.\n * 日報を更新する。\n */\n const updateDailyReport = async (reportHubId: number, userId: number, data: { title?: string; content?: string }, clientTempId: string): Promise<void> => {\n const report = await findDailyReportHubById(db, reportHubId)\n if (!report.length || report[0].deletedAt) {\n throw new Error(\"Not Found\")\n }\n if (report[0].userId !== userId) {\n throw new Error(\"Unauthorized\")\n }\n\n await db.transaction(async (tx) => {\n if (data.title !== undefined) {\n await updateDailyReportHub(tx, reportHubId, {\n title: data.title,\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n }\n if (data.content !== undefined) {\n await updateDailyReportInternal(tx, reportHubId, {\n body: data.content,\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n }\n })\n\n // キャッシュ無効化\n sqlResultCache.invalidatePrefix(`daily-report:detail:${reportHubId}:user:`)\n sqlResultCache.invalidatePrefix(DAILY_REPORT_BUSINESS_DATE_CACHE_PREFIX)\n\n const fullDetail = await getDailyReportDetailById(reportHubId, userId, { forceRefresh: true })\n if (fullDetail) {\n // 下書き判定\n const draftLabelId = await getDraftLabelId()\n const isDraft = draftLabelId ? fullDetail.labels.some((l) => l.id === draftLabelId) : false\n await publishToSseStream(\n reportUpdateMessageSchema.parse({\n type: \"report-update\",\n reportHubId: fullDetail.reportHubId,\n report: fullDetail,\n clientTempId,\n recipientRawUserId: isDraft ? userId : undefined,\n }),\n \"updateDailyReport\",\n )\n }\n }\n\n /**\n * Publishes a draft daily report.\n * 日報を公開する(下書きラベルを削除)。\n */\n const publishDailyReport = async (reportHubId: number, userId: number, clientTempId: string): Promise<DailyReportDetail | null> => {\n const report = await findDailyReportHubById(db, reportHubId)\n if (!report.length) {\n throw new Error(\"Not Found\")\n }\n if (report[0].userId !== userId) {\n throw new Error(\"Unauthorized\")\n }\n\n const draftLabelId = await getDraftLabelId()\n if (!draftLabelId) return null\n\n await deleteDailyReportLabel(db, reportHubId, draftLabelId)\n\n // キャッシュ無効化\n sqlResultCache.invalidatePrefix(`daily-report:detail:${reportHubId}:user:`)\n sqlResultCache.invalidatePrefix(DAILY_REPORT_BUSINESS_DATE_CACHE_PREFIX)\n // 公開によりドラフトが全ユーザーに可視化されるため、全ユーザーの IDs キャッシュを invalidate\n sqlResultCache.invalidatePrefix(DAILY_REPORT_IDS_CACHE_KEY)\n // ❗ 順序重要: invalidatePrefix → incrementRedisEpoch → publishToSseStream\n await incrementRedisEpoch(DAILY_REPORT_IDS_EPOCH_KEY)\n if (report[0].businessDate) {\n const normalizedDate = normalizeBusinessDateKey(formatDateValue(report[0].businessDate, \"YYYY-MM-DD\"))\n if (normalizedDate) {\n await incrementRedisEpoch(`${DAILY_REPORT_DATE_EPOCH_PREFIX}${normalizedDate}`)\n }\n }\n\n // ユーザーごとの営業日別一覧キャッシュも無効化\n const normalizedBusinessDate = normalizeBusinessDateKey(formatDateValue(report[0].businessDate, \"YYYY-MM-DD\"))\n if (normalizedBusinessDate) {\n sqlResultCache.invalidate(`daily-report:date:${normalizedBusinessDate}:user:${userId}`)\n }\n\n const fullDetail = await getDailyReportDetailById(reportHubId, userId, { forceRefresh: true })\n if (fullDetail) {\n await publishToSseStream(\n reportPublishMessageSchema.parse({\n type: \"report-publish\",\n reportHubId: fullDetail.reportHubId,\n report: fullDetail,\n clientTempId,\n }),\n \"publishDailyReport\",\n )\n }\n return fullDetail ?? null\n }\n\n /**\n * Clears all server-side SQL result and user ID caches.\n * サーバー側のすべての SQL 結果キャッシュおよびユーザー ID キャッシュを全消去する。\n */\n const clearCache = async (): Promise<void> => {\n sqlResultCache.clearAll()\n userIdCache.clear()\n cachedDraftLabelId = null\n if (epochs) {\n await incrementRedisEpoch(DAILY_REPORT_IDS_EPOCH_KEY)\n }\n logger.info(\"[DailyReportService] Server-side DB/SQL caches cleared successfully.\")\n }\n\n return {\n // 定数 (アプリ側の互換 export 用)\n streamKey: DAILY_REPORT_SSE_STREAM_KEY,\n streamMaxLen: DAILY_REPORT_SSE_STREAM_MAXLEN,\n // キャッシュクリア\n clearCache,\n // ユーザー解決\n getUserIdByExternalId,\n // 参照系\n getDailyReportIdsByExternalId,\n getDailyReportsByBusinessDate,\n getDailyReportsByBusinessDateByExternalId,\n getDailyReportDetailById,\n getDailyReportDetailByIdByExternalId,\n getDraftLabelId,\n // 更新系\n setStarStatus,\n setReadStatus,\n addComment,\n deleteComment,\n createDailyReport,\n updateDailyReport,\n publishDailyReport,\n deleteDailyReport,\n // リポジトリヘルパー (統合テスト等からの直接利用向け)\n findDailyReportHubById,\n updateDailyReportHub,\n updateDailyReportInternal,\n deleteDailyReportLabel,\n }\n}\n","/**\n * Authz helper utilities for @aiquants/daily-report: standard resource definitions & auto-seeding.\n * @aiquants/daily-report 用の認可ヘルパー。標準リソース定義および自動シード機能を提供。\n */\nimport { and, eq } from \"drizzle-orm\"\n\nexport type AuthzResourceItem = {\n resourceKey: string\n name: string\n description?: string | null\n}\n\nexport type DailyReportSourceTypeInput = {\n key: string\n name: string\n description?: string | null\n includeCommentResource?: boolean\n}\n\n/**\n * Dynamically builds neutral Authz resource definitions from provided source type inputs.\n * 指定されたソース種別定義から中立な認可リソース定義リストを生成するファクトリ。\n */\nexport function defineDailyReportAuthzResources(sources?: DailyReportSourceTypeInput[]): AuthzResourceItem[] {\n const list: AuthzResourceItem[] = []\n const targetSources =\n sources && sources.length > 0\n ? sources\n : [\n { key: \"internal\", name: \"Internal\", description: \"Internal daily reports\" },\n { key: \"external\", name: \"External\", description: \"External daily reports\" },\n ]\n\n for (const src of targetSources) {\n const keyLower = src.key.toLowerCase().replace(/[^a-z0-9_]/g, \"_\")\n const resourceKey = `daily_report_${keyLower}`\n list.push({\n resourceKey,\n name: src.name,\n description: src.description ?? `${src.name} daily report access`,\n })\n\n if (src.includeCommentResource !== false) {\n list.push({\n resourceKey: `${resourceKey}_comment`,\n name: `${src.name} Comment`,\n description: `${src.name} daily report comment access`,\n })\n }\n }\n\n return list\n}\n\ntype DbSelectChain = {\n from(t: unknown): DbSelectChain\n where(cond: unknown): PromiseLike<unknown[]>\n}\ntype DbInsertChain = {\n values(v: unknown): PromiseLike<unknown>\n}\ntype LooseDb = {\n select(fields?: unknown): DbSelectChain\n insert(t: unknown): DbInsertChain\n}\n\n/**\n * Ensures all daily-report standard resources exist in the authz TMResource table idempotently.\n * authz データベース内に日報機能の標準認可リソースが存在することを自動保証(冪等シード)する処理。\n */\nexport async function seedDailyReportAuthzResources(db: unknown, authzTables: { TMResource: unknown }, opts: { appKey: string; actor?: string; sources?: DailyReportSourceTypeInput[]; resources?: AuthzResourceItem[] }): Promise<void> {\n const d = db as LooseDb\n const TMResource = authzTables.TMResource as Record<string, unknown>\n const appKey = opts.appKey\n const actor = opts.actor ?? \"system:daily-report\"\n const now = new Date()\n\n const resourceList = opts.resources ?? defineDailyReportAuthzResources(opts.sources)\n\n for (const res of resourceList) {\n const existing = await d\n .select()\n .from(TMResource)\n .where(and(eq(TMResource.appKey as never, appKey as never), eq(TMResource.resourceKey as never, res.resourceKey as never)))\n\n if (!existing || existing.length === 0) {\n await d.insert(TMResource).values({\n appKey,\n resourceKey: res.resourceKey,\n name: res.name,\n description: res.description ?? null,\n createdAt: now,\n createdBy: actor,\n updatedAt: now,\n updatedBy: actor,\n })\n }\n }\n}\n","/**\n * External source adapter contract for legacy/foreign daily-report tables.\n * レガシー・外部由来の日報テーブルを取り込む外部ソースアダプタ契約。\n *\n * DailyReportHub.source_type がアダプタの `sourceType` に一致する行は、\n * `table` を `Hub.source_id_num = idColumn` で LEFT JOIN し、その行を\n * `mapRow` で表示フィールドへ変換する (例: 別システムのレガシー日報テーブルの取り込み)。\n */\nimport type { AnyMsSqlColumn } from \"drizzle-orm/mssql-core\"\nimport { createLogger, type DailyReportLogger, LogLevel } from \"../shared/logger\"\nimport type { DailyReportComment, DailyReportInterviewer } from \"../shared/types\"\n\n/** 外部ソース行から詳細表示へ供給するフィールド群。 */\nexport type ExternalReportFields = {\n content?: string | null\n employeeName?: string | null\n category?: string | null\n creationCategory?: string | null\n visitTimeFrom?: string | null\n visitTimeTo?: string | null\n customerName?: string | null\n interviewers?: DailyReportInterviewer[]\n comments?: DailyReportComment[]\n}\n\n/** 外部ソースアダプタ。 */\nexport type DailyReportExternalSource = {\n /** DailyReportHub.source_type の一致値 (例 \"legacy\")。 */\n sourceType: string\n /** LEFT JOIN する drizzle テーブル。 */\n table: unknown\n /** Hub.source_id_num と突き合わせる ID 列。 */\n idColumn: AnyMsSqlColumn\n /** 結合行を表示フィールドへ変換する処理。 */\n mapRow: (row: Record<string, unknown>) => ExternalReportFields\n}\n\nconst defaultLogger = createLogger(LogLevel.INFO, \"[DailyReportExternalSource]\")\n\n/**\n * Parses JSON array payloads and maps each element (fail-soft: returns [] on error).\n * JSON 配列のペイロードを解析し各要素を変換する処理 (エラー時は空配列)。\n */\nexport const transformJsonArray = <TRaw, TResult>(payload: string | null, label: string, mapper: (raw: TRaw) => TResult | null, logger: DailyReportLogger = defaultLogger): TResult[] => {\n if (!payload) {\n return []\n }\n\n try {\n const parsed = JSON.parse(payload)\n if (!Array.isArray(parsed)) {\n logger.warn(`Unexpected ${label} format: not an array`)\n return []\n }\n return (parsed as TRaw[]).map(mapper).filter((entry): entry is TResult => entry !== null)\n } catch (error) {\n logger.warn(`Failed to parse ${label}:`, error)\n return []\n }\n}\n","/**\n * Structural table types + drizzle (mssql) schema factory for the daily-report tables.\n * 日報テーブル群の構造的テーブル型と drizzle (mssql) スキーマファクトリ。\n *\n * 既存アプリは自前のモデル定義をそのまま `DailyReportTables` として注入できる (構造互換)。\n * 新規プロジェクトは `defineDailyReportSchema()` で同一構造のテーブル定義を生成できる。\n */\nimport { desc, sql } from \"drizzle-orm\"\nimport { type AnyMsSqlColumn, bigint, bit, date, datetime2, foreignKey, index, int, mssqlSchema, nvarchar, primaryKey } from \"drizzle-orm/mssql-core\"\n\n/** 注入する外部ユーザーテーブルの最小形 (id / display_name)。 */\nexport type DailyReportUserTable = { id: AnyMsSqlColumn; displayName: AnyMsSqlColumn }\n\n/** DailyReportHub テーブルの構造形。 */\nexport type DailyReportHubTable = {\n id: AnyMsSqlColumn\n sourceType: AnyMsSqlColumn\n sourceId: AnyMsSqlColumn\n sourceIdNum: AnyMsSqlColumn\n businessDate: AnyMsSqlColumn\n userId: AnyMsSqlColumn\n title: AnyMsSqlColumn\n summary: AnyMsSqlColumn\n createdAt: AnyMsSqlColumn\n createdBy: AnyMsSqlColumn\n updatedAt: AnyMsSqlColumn\n updatedBy: AnyMsSqlColumn\n deletedAt: AnyMsSqlColumn\n deletedBy: AnyMsSqlColumn\n}\n\n/** DailyReportInternal テーブルの構造形。 */\nexport type DailyReportInternalTable = {\n hubId: AnyMsSqlColumn\n body: AnyMsSqlColumn\n metadata: AnyMsSqlColumn\n createdAt: AnyMsSqlColumn\n createdBy: AnyMsSqlColumn\n updatedAt: AnyMsSqlColumn\n updatedBy: AnyMsSqlColumn\n}\n\n/** DailyReportComment テーブルの構造形。 */\nexport type DailyReportCommentTable = {\n id: AnyMsSqlColumn\n hubId: AnyMsSqlColumn\n userId: AnyMsSqlColumn\n body: AnyMsSqlColumn\n createdAt: AnyMsSqlColumn\n createdBy: AnyMsSqlColumn\n updatedAt: AnyMsSqlColumn\n updatedBy: AnyMsSqlColumn\n}\n\n/** DailyReportLabel テーブルの構造形。 */\nexport type DailyReportLabelTable = {\n id: AnyMsSqlColumn\n userId: AnyMsSqlColumn\n name: AnyMsSqlColumn\n color: AnyMsSqlColumn\n sortOrder: AnyMsSqlColumn\n createdAt: AnyMsSqlColumn\n createdBy: AnyMsSqlColumn\n updatedAt: AnyMsSqlColumn\n updatedBy: AnyMsSqlColumn\n}\n\n/** DailyReportHub_Label 中間テーブルの構造形。 */\nexport type DailyReportHubLabelTable = {\n hubId: AnyMsSqlColumn\n labelId: AnyMsSqlColumn\n createdAt: AnyMsSqlColumn\n createdBy: AnyMsSqlColumn\n}\n\n/** DailyReportUserStatus テーブルの構造形。 */\nexport type DailyReportUserStatusTable = {\n hubId: AnyMsSqlColumn\n userId: AnyMsSqlColumn\n isRead: AnyMsSqlColumn\n isStarred: AnyMsSqlColumn\n createdAt: AnyMsSqlColumn\n createdBy: AnyMsSqlColumn\n updatedAt: AnyMsSqlColumn\n updatedBy: AnyMsSqlColumn\n}\n\n/** サービスへ注入するテーブル一式 (アプリ既存モデルまたは本ファクトリ生成物)。 */\nexport type DailyReportTables = {\n hub: DailyReportHubTable\n internal: DailyReportInternalTable\n comment: DailyReportCommentTable\n label: DailyReportLabelTable\n hubLabel: DailyReportHubLabelTable\n userStatus: DailyReportUserStatusTable\n}\n\n/**\n * Generic factory: build the six daily-report tables under an arbitrary schema name.\n * 任意のスキーマ名の下に日報 6 テーブルを生成する汎用ファクトリ。\n *\n * 制約・索引名は `${schemaName}_<Table>_...` 規約で生成する。\n * ユーザーテーブルへの FK は注入された `userTable` を参照する。\n */\nexport function defineDailyReportSchema<S extends string>(schemaName: S, opts: { userTable: DailyReportUserTable }) {\n const s = mssqlSchema(schemaName)\n const users = opts.userTable\n\n const hub = s.table(\n \"DailyReportHub\",\n {\n id: bigint(\"id\", { mode: \"number\" }).identity().notNull(),\n sourceType: nvarchar(\"source_type\", { length: 20 }).notNull(),\n sourceId: nvarchar(\"source_id\", { length: 100 }).notNull(),\n sourceIdNum: bigint(\"source_id_num\", { mode: \"number\" }).generatedAlwaysAs(sql`TRY_CAST(source_id AS BIGINT)`),\n businessDate: date(\"business_date\"),\n userId: bigint(\"user_id\", { mode: \"number\" }),\n title: nvarchar(\"title\", { length: 200 }),\n summary: nvarchar(\"summary\", { length: \"max\" }),\n createdAt: datetime2(\"created_at\").notNull(),\n createdBy: nvarchar(\"created_by\", { length: 50 }).notNull(),\n updatedAt: datetime2(\"updated_at\").notNull(),\n updatedBy: nvarchar(\"updated_by\", { length: 50 }).notNull(),\n deletedAt: datetime2(\"deleted_at\"),\n deletedBy: nvarchar(\"deleted_by\", { length: 50 }),\n },\n (table) => [\n primaryKey({ name: `${schemaName}_DailyReportHub_pk`, columns: [table.id] }),\n foreignKey({\n name: `${schemaName}_DailyReportHub_user_id_fk`,\n columns: [table.userId],\n foreignColumns: [users.id],\n }),\n index(`${schemaName}_DailyReportHub_business_date_index`).on(table.businessDate),\n index(`${schemaName}_DailyReportHub_updated_at_index`).on(table.updatedAt),\n index(`${schemaName}_DailyReportHub_source_index`).on(table.sourceType, table.sourceId),\n index(`${schemaName}_DailyReportHub_user_id_index`).on(table.userId),\n index(`${schemaName}_DailyReportHub_business_date_id_index`).on(desc(table.businessDate), desc(table.id)),\n index(`${schemaName}_DailyReportHub_deleted_at_index`).on(table.deletedAt),\n ],\n )\n\n const internal = s.table(\n \"DailyReportInternal\",\n {\n hubId: bigint(\"hub_id\", { mode: \"number\" }).notNull(),\n body: nvarchar(\"body\", { length: \"max\" }),\n metadata: nvarchar(\"metadata\", { length: \"max\" }),\n createdAt: datetime2(\"created_at\").notNull(),\n createdBy: nvarchar(\"created_by\", { length: 50 }).notNull(),\n updatedAt: datetime2(\"updated_at\").notNull(),\n updatedBy: nvarchar(\"updated_by\", { length: 50 }).notNull(),\n },\n (table) => [\n primaryKey({ name: `${schemaName}_DailyReportInternal_pk`, columns: [table.hubId] }),\n foreignKey({\n name: `${schemaName}_DailyReportInternal_hub_id_fk`,\n columns: [table.hubId],\n foreignColumns: [hub.id],\n }),\n ],\n )\n\n const comment = s.table(\n \"DailyReportComment\",\n {\n id: bigint(\"id\", { mode: \"number\" }).identity().notNull(),\n hubId: bigint(\"hub_id\", { mode: \"number\" }).notNull(),\n userId: bigint(\"user_id\", { mode: \"number\" }).notNull(),\n body: nvarchar(\"body\", { length: \"max\" }).notNull(),\n createdAt: datetime2(\"created_at\").notNull(),\n createdBy: nvarchar(\"created_by\", { length: 50 }).notNull(),\n updatedAt: datetime2(\"updated_at\").notNull(),\n updatedBy: nvarchar(\"updated_by\", { length: 50 }).notNull(),\n },\n (table) => [\n primaryKey({ name: `${schemaName}_DailyReportComment_pk`, columns: [table.id] }),\n foreignKey({\n name: `${schemaName}_DailyReportComment_hub_id_fk`,\n columns: [table.hubId],\n foreignColumns: [hub.id],\n }),\n foreignKey({\n name: `${schemaName}_DailyReportComment_user_id_fk`,\n columns: [table.userId],\n foreignColumns: [users.id],\n }),\n index(`${schemaName}_DailyReportComment_hub_id_index`).on(table.hubId),\n ],\n )\n\n const label = s.table(\n \"DailyReportLabel\",\n {\n id: bigint(\"id\", { mode: \"number\" }).identity().notNull(),\n userId: bigint(\"user_id\", { mode: \"number\" }),\n name: nvarchar(\"name\", { length: 50 }).notNull(),\n color: nvarchar(\"color\", { length: 20 }),\n sortOrder: int(\"sort_order\"),\n createdAt: datetime2(\"created_at\").notNull(),\n createdBy: nvarchar(\"created_by\", { length: 50 }).notNull(),\n updatedAt: datetime2(\"updated_at\").notNull(),\n updatedBy: nvarchar(\"updated_by\", { length: 50 }).notNull(),\n },\n (table) => [\n primaryKey({ name: `${schemaName}_DailyReportLabel_pk`, columns: [table.id] }),\n foreignKey({\n name: `${schemaName}_DailyReportLabel_user_id_fk`,\n columns: [table.userId],\n foreignColumns: [users.id],\n }),\n ],\n )\n\n const hubLabel = s.table(\n \"DailyReportHub_Label\",\n {\n hubId: bigint(\"hub_id\", { mode: \"number\" }).notNull(),\n labelId: bigint(\"label_id\", { mode: \"number\" }).notNull(),\n createdAt: datetime2(\"created_at\").notNull(),\n createdBy: nvarchar(\"created_by\", { length: 50 }).notNull(),\n },\n (table) => [\n primaryKey({ name: `${schemaName}_DailyReportHub_Label_pk`, columns: [table.hubId, table.labelId] }),\n foreignKey({\n name: `${schemaName}_DailyReportHub_Label_hub_id_fk`,\n columns: [table.hubId],\n foreignColumns: [hub.id],\n }),\n foreignKey({\n name: `${schemaName}_DailyReportHub_Label_label_id_fk`,\n columns: [table.labelId],\n foreignColumns: [label.id],\n }),\n ],\n )\n\n const userStatus = s.table(\n \"DailyReportUserStatus\",\n {\n hubId: bigint(\"hub_id\", { mode: \"number\" }).notNull(),\n userId: bigint(\"user_id\", { mode: \"number\" }).notNull(),\n isRead: bit(\"is_read\").default(false).notNull(),\n isStarred: bit(\"is_starred\").default(false).notNull(),\n createdAt: datetime2(\"created_at\").notNull(),\n createdBy: nvarchar(\"created_by\", { length: 50 }).notNull(),\n updatedAt: datetime2(\"updated_at\").notNull(),\n updatedBy: nvarchar(\"updated_by\", { length: 50 }).notNull(),\n },\n (table) => [\n primaryKey({ name: `${schemaName}_DailyReportUserStatus_pk`, columns: [table.hubId, table.userId] }),\n foreignKey({\n name: `${schemaName}_DailyReportUserStatus_hub_id_fk`,\n columns: [table.hubId],\n foreignColumns: [hub.id],\n }),\n foreignKey({\n name: `${schemaName}_DailyReportUserStatus_user_id_fk`,\n columns: [table.userId],\n foreignColumns: [users.id],\n }),\n ],\n )\n\n return { hub, internal, comment, label, hubLabel, userStatus } satisfies DailyReportTables\n}\n","/**\n * Server entry of @aiquants/daily-report: schema factory, service, SSE reader, and handler factories.\n * @aiquants/daily-report の server エントリ。スキーマファクトリ・サービス・SSE リーダー・ハンドラ工場を公開。\n */\nimport { createEpochStore, SqlResultCache } from \"./server/cache\"\nimport type { DailyReportExternalSource } from \"./server/external-source\"\nimport { createDailyReportHandlers } from \"./server/handlers\"\nimport type { DailyReportAuthenticate } from \"./server/ports\"\nimport { createDailyReportService, type DailyReportServiceConfig } from \"./server/service\"\nimport { DailyReportSseReader } from \"./server/sse-reader\"\n\nexport * from \"./server/authz\"\nexport * from \"./server/cache\"\nexport { generateETag } from \"./server/etag\"\nexport * from \"./server/external-source\"\nexport * from \"./server/handlers\"\nexport * from \"./server/ports\"\nexport * from \"./server/response\"\nexport * from \"./server/schema\"\nexport * from \"./server/service\"\nexport * from \"./server/sse-reader\"\n\n/** createDailyReportServer の設定 (サービス設定 + 認証ポート + ハンドラ設定)。 */\nexport type DailyReportServerConfig = Omit<DailyReportServiceConfig, \"cache\" | \"epochs\"> & {\n /** リクエスト認証ポート。 */\n authenticate: DailyReportAuthenticate\n /** 未ログイン時のリダイレクト先 (index.loader 用、既定 \"/auth/login\")。 */\n loginRedirectPath?: string\n /** SQL 結果キャッシュの既定 TTL (既定 60,000ms)。 */\n cacheDefaultTtlMs?: number\n /** 外部ソースアダプタ群 (レガシー日報テーブル等)。 */\n externalSources?: DailyReportExternalSource[]\n}\n\n/**\n * One-stop factory wiring cache, service, SSE fan-out reader, and route handlers.\n * キャッシュ・サービス・SSE Fan-Out リーダー・ルートハンドラを一括結線するファクトリ。\n *\n * @example\n * const server = createDailyReportServer({ db, tables, userTable, resolveUserId, encodeUserId, authenticate, redis })\n * // routes:\n * // daily_report._index → server.index.loader (+ data() ラップ)\n * // daily_report.api.$endpoint → server.api.loader / server.api.action\n * // sse.daily_report.$endpoint → server.sse.loader\n */\nexport function createDailyReportServer(config: DailyReportServerConfig) {\n // epoch ストアと SQL 結果キャッシュ (サービスと SSE リーダーで同一インスタンスを共有する)\n const epochs = createEpochStore(config.redis)\n const cache = new SqlResultCache({ defaultTtlMs: config.cacheDefaultTtlMs ?? 60_000 }, epochs)\n\n const service = createDailyReportService({ ...config, cache, epochs })\n\n const sseReader = new DailyReportSseReader({\n redis: config.redis,\n streamKey: service.streamKey,\n cache,\n logger: config.logger,\n })\n\n const handlers = createDailyReportHandlers({\n authenticate: config.authenticate,\n service,\n encodeUserId: config.encodeUserId,\n redis: config.redis,\n sseReader,\n streamKey: service.streamKey,\n loginRedirectPath: config.loginRedirectPath,\n logger: config.logger,\n })\n\n return {\n /** データアクセスサービス (CRUD + キャッシュ + SSE publish)。 */\n service,\n /** SQL 結果キャッシュ (サービス・SSE リーダー共有インスタンス)。 */\n cache,\n /** クロスワーカー epoch ストア。 */\n epochs,\n /** SSE Fan-Out 共有リーダー。 */\n sseReader,\n /** SSE Redis Stream キー。 */\n streamKey: service.streamKey,\n ...handlers,\n }\n}\n"],"mappings":"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","names":["createEpochStore","redis","epochKey","client","val","SqlResultCache","config","epochStore","cacheKey","opts","forceRefresh","snapshot","bucket","currentEpoch","r","shared","promise","records","expireAt","epoch","result","prefix","key","now","ISO_DATE_PATTERN","SLASH_DATE_PATTERN","zeroPad","value","normalizeFromDate","year","month","day","normalizeBusinessDateKey","trimmed","parsed","createLogger","level","prefix","impl","format","message","rest","z","dailyReportInterviewerSchema","dailyReportCommentSchema","dailyReportLabelDefSchema","dailyReportCommentItemSchema","dailyReportDetailSchema","connectedMessageSchema","statusUpdateMessageSchema","commentAddMessageSchema","commentDeleteMessageSchema","reportCreateMessageSchema","reportUpdateMessageSchema","reportPublishMessageSchema","reportDeleteMessageSchema","dailyReportSseMessageSchema","createHash","generateETag","data","json","defaultLogger","createLogger","jsonResponseWithETag","request","cookie","payload","status","logger","etag","generateETag","ifNoneMatch","headers","EventEmitter","isStreamIdLte","a","b","aMs","aSeq","bMs","bSeq","DailyReportSseReader","config","EventEmitter","createLogger","onEntry","onError","resolve","err","results","stream","msg","parsed","msgType","reportHubId","jsonData","payload","init","createDailyReportHandlers","config","authenticate","service","encodeUserId","redis","sseReader","streamKey","loginRedirectPath","apiLogger","createLogger","sseLogger","indexLoader","request","user","cookie","headers","userId","hashedUserId","endpointHandlers","url","normalizedBusinessDate","normalizeBusinessDateKey","forceRefresh","jsonResponseWithETag","reports","dailyReportIds","param","parsedId","detail","params","sanitizedCookie","endpoint","handler","startTime","error","elapsed","err","formData","intent","reportHubIdRaw","reportHubId","businessDate","operationTimestamp","clientTempId","newReport","title","content","e","publishedReport","isStarredRaw","isStarred","updatedStatus","isReadRaw","isRead","newComment","safeComment","commentId","internalUserId","lastEventId","encoder","controllerRef","keepAliveInterval","unsubscribe","isCleaningUp","cleanup","processEntry","entryId","fields","raw","result","dailyReportSseMessageSchema","parsed","recipientRawUserId","sanitizedMessage","_","rest","stream","controller","lastProcessedId","entry","isStreamIdLte","client","latest","connectedPayload","sseMessage","catchUpEntries","aliasedTable","and","asc","desc","eq","getColumns","inArray","isNull","or","sql","cols","t","getColumns","createDailyReportService","config","db","tables","users","resolveUserId","encodeUserId","redis","sqlResultCache","epochs","DailyReportHub","DailyReportInternal","DailyReportCommentModel","DailyReportLabel","DailyReportHub_Label","DailyReportUserStatus","externalSources","draftLabelNames","draftLabelName","logger","createLogger","DAILY_REPORT_IDS_CACHE_KEY","DAILY_REPORT_IDS_TTL_MS","DAILY_REPORT_IDS_EPOCH_KEY","DAILY_REPORT_BUSINESS_DATE_CACHE_PREFIX","DAILY_REPORT_BUSINESS_DATE_TTL_MS","DAILY_REPORT_DATE_EPOCH_PREFIX","DAILY_REPORT_DETAIL_EPOCH_PREFIX","DAILY_REPORT_SSE_STREAM_KEY","DAILY_REPORT_SSE_STREAM_MAXLEN","incrementRedisEpoch","key","publishToSseStream","message","callerName","client","publishStartMs","publishDurationMs","e","formatDateValue","value","pattern","dateValue","opts","maskAuditActor","externalSelections","out","adapter","applyExternalJoins","chain","c","and","eq","mapHubRecord","row","currentUserId","hub","internal","content","interviewers","category","creationCategory","visitTimeFrom","visitTimeTo","customerName","employeeName","comments","a","externalRow","fields","labelsRaw","l","commentItemsRaw","cachedDraftLabelId","fetchDailyReportIdsByUserId","userId","DraftLabelRelation","aliasedTable","draftLabelId","getDraftLabelId","isNull","or","desc","item","fetchDailyReportsByBusinessDate","normalizedBusinessDate","sql","buildBusinessDateCacheKey","userIdCache","getUserIdByExternalId","externalId","getDailyReportIdsByExternalId","forceRefresh","snapshot","ttlMsOverride","cacheKey","getDailyReportsByBusinessDate","businessDate","normalizeBusinessDateKey","label","inArray","getDailyReportsByBusinessDateByExternalId","rows","hubIds","r","allLabels","allComments","asc","labelsMap","commentsMap","list","comment","hubId","getDailyReportDetailById","reportHubId","labels","labelsMapped","commentsMapped","getDailyReportDetailByIdByExternalId","options","setStarStatus","isStarred","clientTempId","existing","updatedStatus","normalizedDate","statusUpdateMessageSchema","setReadStatus","isRead","addComment","inserted","user","userName","commentItem","commentAddMessageSchema","findDailyReportCommentById","tx","commentId","deleteComment","commentDeleteMessageSchema","createDailyReport","result","sourceId","fullDetail","isDraft","reportCreateMessageSchema","findDailyReportHubById","updateDailyReportHub","data","updateDailyReportInternal","deleteDailyReportLabel","labelId","report","reportUpdateMessageSchema","reportPublishMessageSchema","reportDeleteMessageSchema","and","eq","defineDailyReportAuthzResources","sources","list","targetSources","src","resourceKey","seedDailyReportAuthzResources","db","authzTables","opts","d","TMResource","appKey","actor","now","resourceList","res","existing","defaultLogger","createLogger","transformJsonArray","payload","label","mapper","logger","parsed","entry","error","desc","sql","bigint","bit","date","datetime2","foreignKey","index","int","mssqlSchema","nvarchar","primaryKey","defineDailyReportSchema","schemaName","opts","s","users","hub","table","internal","comment","label","hubLabel","userStatus","createDailyReportServer","config","epochs","createEpochStore","cache","SqlResultCache","service","createDailyReportService","sseReader","DailyReportSseReader","handlers","createDailyReportHandlers"]}
1
+ {"version":3,"sources":["../src/server/cache.ts","../src/shared/business-date.ts","../src/shared/logger.ts","../src/shared/sse-schema.ts","../src/server/etag.ts","../src/server/response.ts","../src/server/sse-reader.ts","../src/server/handlers.ts","../src/server/service.ts","../src/server/authz.ts","../src/server/external-source.ts","../src/server/schema.ts","../src/server.ts"],"sourcesContent":["/**\n * In-memory SQL result cache with TTL, snapshot, and cross-worker epoch (via redis) support.\n * TTL・スナップショット・クロスワーカー epoch (redis 経由) を備えた SQL 結果のインメモリキャッシュ。\n *\n * epoch は任意注入の redis ポート経由で取得・更新する (未注入時は常に 0 = epoch 無効)。\n */\nimport type { DailyReportRedisProvider } from \"./ports\"\n\nexport type SqlResultCacheQueryOptions = {\n forceRefresh?: boolean\n snapshot?: boolean\n ttlMsOverride?: number\n epochKey?: string\n}\n\n/**\n * Creates epoch helpers bound to an optional redis provider.\n * 任意の redis プロバイダーに束縛された epoch ヘルパーを生成する処理。\n */\nexport const createEpochStore = (redis?: DailyReportRedisProvider) => ({\n /**\n * Gets the current epoch value from Redis.\n * Redis からエポック値を取得する。エラー時は 0 を返す (常に re-fetch = safe 方向)。\n */\n async getEpoch(epochKey: string): Promise<number> {\n try {\n const client = await redis?.getClient()\n if (!client) return 0\n const val = await client.get(epochKey)\n return val ? Number(val) : 0\n } catch {\n return 0\n }\n },\n /**\n * Increments the epoch counter in Redis.\n * Redis のエポックカウンターをインクリメントする。\n */\n async incrementEpoch(epochKey: string): Promise<void> {\n try {\n const client = await redis?.getClient()\n if (!client) return\n await client.incr(epochKey)\n } catch {\n // Redis エラー時は epoch 更新をスキップ (次回 GET で 0 → stale 扱い → re-fetch)\n }\n },\n})\n\nexport type EpochStore = ReturnType<typeof createEpochStore>\n\n/**\n * In-memory cache for SQL query results with TTL and snapshot support.\n * TTLとスナップショット機能を備えたSQLクエリ結果のインメモリキャッシュ。\n */\nexport class SqlResultCache {\n private readonly buckets = new Map<string, { records: readonly unknown[]; expireAt: number; epoch: number }>()\n private readonly inFlight = new Map<string, Promise<readonly unknown[]>>()\n\n constructor(\n private readonly config: { defaultTtlMs: number },\n private readonly epochStore: EpochStore,\n ) {}\n\n /**\n * Invalidates a specific cache bucket.\n * 指定されたキャッシュバケットを無効化します。\n */\n invalidate = (cacheKey: string): void => {\n this.buckets.delete(cacheKey)\n this.inFlight.delete(cacheKey)\n }\n\n /**\n * Retrieves records from cache or fetches them if expired/missing.\n * キャッシュからレコードを取得するか、期限切れや未存在の場合はフェッチします。\n */\n getOrFetch = async <T>(\n opts: {\n cacheKey: string\n fetcher: () => Promise<readonly T[]>\n } & SqlResultCacheQueryOptions,\n ): Promise<readonly T[]> => {\n const { cacheKey, forceRefresh, snapshot } = opts\n const bucket = this.buckets.get(cacheKey)\n\n // 強制リフレッシュ時はキャッシュと進行中のリクエストをクリア\n if (forceRefresh) {\n this.buckets.delete(cacheKey)\n this.inFlight.delete(cacheKey)\n }\n\n // 有効なキャッシュがあれば返却 (epoch check 込み)\n if (!forceRefresh && bucket && bucket.expireAt > Date.now()) {\n if (opts.epochKey) {\n // epochKey 指定時は Redis の epoch と比較して stale を検出\n const currentEpoch = await this.epochStore.getEpoch(opts.epochKey)\n if (bucket.epoch === currentEpoch) {\n return snapshot ? (bucket.records.map((r) => structuredClone(r)) as T[]) : (bucket.records as T[])\n }\n // epoch 不一致 → stale bucket を除去して re-fetch へ\n this.buckets.delete(cacheKey)\n } else {\n return snapshot ? (bucket.records.map((r) => structuredClone(r)) as T[]) : (bucket.records as T[])\n }\n }\n\n // 進行中のリクエストがあれば相乗り\n if (!forceRefresh && this.inFlight.has(cacheKey)) {\n const shared = (await this.inFlight.get(cacheKey)) as readonly T[]\n return snapshot ? shared.map((r) => structuredClone(r)) : shared\n }\n\n // 新規フェッチとキャッシュ更新\n // 自己参照 (this.inFlight.get(cacheKey) === promise) のため、定義前参照を回避する確定代入アサーション\n let promise!: Promise<readonly T[]>\n promise = (async () => {\n const records = await opts.fetcher()\n // invalidation 中に完了したリクエストはキャッシュに書き込まない(レース防止)\n // ただし forceRefresh の場合は常に書き込む\n if (forceRefresh || this.inFlight.get(cacheKey) === promise) {\n const expireAt = Date.now() + (opts.ttlMsOverride ?? this.config.defaultTtlMs)\n // キャッシュ保存時に現在の epoch を記録\n const epoch = opts.epochKey ? await this.epochStore.getEpoch(opts.epochKey) : 0\n this.buckets.set(cacheKey, { records, expireAt, epoch })\n // 蓄積防止のため、サイズが一定値を超えたら期限切れキャッシュを一括クリーンアップ\n if (this.buckets.size > 500) {\n this.cleanExpired()\n }\n }\n return records\n })()\n\n if (!forceRefresh) this.inFlight.set(cacheKey, promise as Promise<readonly unknown[]>)\n\n try {\n const result = await promise\n return snapshot ? result.map((r) => structuredClone(r)) : result\n } finally {\n this.inFlight.delete(cacheKey)\n }\n }\n\n /**\n * Clears the cache for a specific key.\n * 指定されたキーのキャッシュをクリアします。\n */\n flush = (cacheKey: string): void => {\n this.buckets.delete(cacheKey)\n this.inFlight.delete(cacheKey)\n }\n\n /**\n * Clears all cached buckets and in-flight requests.\n * すべてのキャッシュバケットと進行中のリクエストを全クリアします。\n */\n clearAll = (): void => {\n this.buckets.clear()\n this.inFlight.clear()\n }\n\n /**\n * Invalidates cache buckets matching a prefix.\n * 指定されたプレフィックスに一致するキャッシュバケットを無効化します。\n */\n invalidatePrefix = (prefix: string): void => {\n for (const key of this.buckets.keys()) {\n if (key.startsWith(prefix)) {\n this.buckets.delete(key)\n }\n }\n for (const key of this.inFlight.keys()) {\n if (key.startsWith(prefix)) {\n this.inFlight.delete(key)\n }\n }\n }\n\n /**\n * Cleans up all expired cache buckets to prevent memory accumulation.\n * メモリー蓄積を防ぐため、期限切れのキャッシュバケットをすべてクリーンアップする処理。\n */\n private cleanExpired(): void {\n const now = Date.now()\n // 期限切れのキーを削除\n for (const [key, bucket] of this.buckets.entries()) {\n if (bucket.expireAt <= now) {\n this.buckets.delete(key)\n }\n }\n }\n}\n","/**\n * Business date normalization and formatting helper functions.\n * 営業日の正規化とフォーマットを支援するユーティリティ群。\n */\n\ntype BusinessDateInput = string | Date | null | undefined\n\nconst ISO_DATE_PATTERN = /^\\d{4}-\\d{2}-\\d{2}$/\nconst SLASH_DATE_PATTERN = /^\\d{4}\\/\\d{2}\\/\\d{2}$/\n\nconst zeroPad = (value: number): string => {\n return value < 10 ? `0${value}` : `${value}`\n}\n\nconst normalizeFromDate = (value: Date): string | null => {\n if (Number.isNaN(value.getTime())) {\n return null\n }\n\n const year = value.getFullYear()\n const month = zeroPad(value.getMonth() + 1)\n const day = zeroPad(value.getDate())\n return `${year}-${month}-${day}`\n}\n\n/**\n * Normalizes business date input into YYYY-MM-DD format string.\n * 営業日を YYYY-MM-DD 形式の文字列へ正規化する。\n */\nexport const normalizeBusinessDateKey = (value: BusinessDateInput): string | null => {\n if (value === null || value === undefined) {\n return null\n }\n\n if (value instanceof Date) {\n return normalizeFromDate(value)\n }\n\n const trimmed = value.trim()\n if (trimmed === \"\") {\n return null\n }\n\n if (ISO_DATE_PATTERN.test(trimmed)) {\n return trimmed\n }\n\n if (SLASH_DATE_PATTERN.test(trimmed)) {\n return trimmed.replaceAll(\"/\", \"-\")\n }\n\n const parsed = new Date(trimmed)\n return normalizeFromDate(parsed)\n}\n\n/**\n * Converts normalized business date into display format YYYY-MM-DD.\n * 正規化済みの営業日を YYYY-MM-DD 形式の表示文字列へ変換する。\n */\nexport const formatBusinessDateDisplay = (value: BusinessDateInput): string | null => {\n const normalized = normalizeBusinessDateKey(value)\n if (!normalized) {\n return null\n }\n return normalized\n}\n\n/**\n * Parses normalized business date into Date object.\n * 正規化した営業日を Date オブジェクトへ変換する。\n */\nexport const parseBusinessDateKeyToDate = (value: BusinessDateInput): Date | null => {\n const normalized = normalizeBusinessDateKey(value)\n if (!normalized) {\n return null\n }\n\n const [year, month, day] = normalized.split(\"-\").map((part) => Number.parseInt(part, 10))\n if ([year, month, day].some((component) => Number.isNaN(component))) {\n return null\n }\n\n return new Date(year, month - 1, day)\n}\n","/**\n * Minimal level-filtered logger used internally by the daily-report package.\n * daily-report パッケージ内部で使う最小のレベルフィルタ付きロガー。\n *\n * 消費アプリのロガー実装 (console 互換の debug/info/warn/error) を注入でき、\n * 未注入時は console にフォールバックする。\n */\n\n/** 出力フィルタリング用のログレベル。 */\nexport enum LogLevel {\n DEBUG = 0,\n INFO = 1,\n WARN = 2,\n ERROR = 3,\n NONE = 4,\n}\n\n/** Console 互換のロガーインターフェース。 */\nexport interface DailyReportLogger {\n debug(message?: unknown, ...optionalParams: unknown[]): void\n info(message?: unknown, ...optionalParams: unknown[]): void\n warn(message?: unknown, ...optionalParams: unknown[]): void\n error(message?: unknown, ...optionalParams: unknown[]): void\n}\n\n/**\n * Creates a prefixed, level-filtered logger delegating to the given implementation.\n * 指定実装へ委譲するプレフィックス付き・レベルフィルタ付きロガーを生成する処理。\n */\nexport const createLogger = (level: LogLevel, prefix: string, impl: DailyReportLogger = console): DailyReportLogger => {\n // 文字列メッセージはプレフィックスを連結、それ以外は先頭引数として付加する\n const format = (message: unknown): unknown[] => (typeof message === \"string\" ? [`${prefix} ${message}`] : [prefix, message])\n return {\n debug: (message?: unknown, ...rest: unknown[]) => {\n if (level <= LogLevel.DEBUG) impl.debug(...format(message), ...rest)\n },\n info: (message?: unknown, ...rest: unknown[]) => {\n if (level <= LogLevel.INFO) impl.info(...format(message), ...rest)\n },\n warn: (message?: unknown, ...rest: unknown[]) => {\n if (level <= LogLevel.WARN) impl.warn(...format(message), ...rest)\n },\n error: (message?: unknown, ...rest: unknown[]) => {\n if (level <= LogLevel.ERROR) impl.error(...format(message), ...rest)\n },\n }\n}\n","/**\n * Zod schemas for daily-report SSE messages (client/server shared contract).\n * 日報 SSE メッセージの zod スキーマ (client / server 共有契約)。\n */\nimport { z } from \"zod\"\nimport type { DailyReportCommentItem as DeclaredComment, DailyReportDetail as DeclaredDetail } from \"./types\"\n\n// --- サブスキーマ(types.ts の型と構造的に一致させる) ---\n\nexport const dailyReportInterviewerSchema = z.object({\n name: z.string().nullish(),\n affiliation: z.string().nullish(),\n})\n\nexport const dailyReportCommentSchema = z.object({\n name: z.string().nullish(),\n text: z.string().nullish(),\n color: z.string().nullish(),\n})\n\nexport const dailyReportLabelDefSchema = z.object({\n id: z.number(),\n name: z.string().nullish(),\n color: z.string().nullish(),\n})\n\nexport const dailyReportCommentItemSchema = z.object({\n id: z.number(),\n userId: z.string().nullish(),\n userName: z.string().nullish(),\n content: z.string().nullish(),\n createdAt: z.string().nullish(),\n isMine: z.boolean(),\n})\n\n/**\n * 添付ファイル 1 件のスキーマ。\n *\n * 周囲のフィールドは `.nullish()` だが、ここは意図して `.nullable()` を使う。\n * `.nullish()` は推論キーを任意 (`?`) にするため、`types.ts` 側の必須 `fileType: string | null`\n * へ代入できず、ファイル末尾の `_AssertDetailCompat` が `never` に落ちてコンパイルエラーになる。\n * 既存の `.nullish()` フィールドはいずれも `types.ts` 側が `?` 付きの任意キーとペアになっており、\n * 「`.nullish()` ↔ 任意キー」「`.nullable()` ↔ 必須 `| null`」の対応で揃っている。\n * `| undefined` を混ぜないことは、詳細型を消費する DuckDB ステージ行の要請でもある。\n */\nexport const dailyReportAttachmentItemSchema = z.object({\n id: z.string(),\n fileName: z.string(),\n fileType: z.string().nullable(),\n fileSize: z.number().nullable(),\n createdAt: z.string().nullable(),\n state: z.enum([\"present\", \"absent\", \"unknown\"]),\n})\n\nexport const dailyReportDetailSchema = z.object({\n reportHubId: z.number(),\n date: z.string().nullish(),\n createdAt: z.string().nullish(),\n author: z.string().nullish(),\n userId: z.string().nullish(),\n sourceType: z.string().nullish(),\n employeeName: z.string().nullish(),\n updatedBy: z.string().nullish(),\n updatedAt: z.string().nullish(),\n category: z.string().nullish(),\n creationCategory: z.string().nullish(),\n visitTimeFrom: z.string().nullish(),\n visitTimeTo: z.string().nullish(),\n customerName: z.string().nullish(),\n interviewers: z.array(dailyReportInterviewerSchema),\n subject: z.string().nullish(),\n content: z.string().nullish(),\n comments: z.array(dailyReportCommentSchema),\n isRead: z.boolean(),\n isStarred: z.boolean(),\n labels: z.array(dailyReportLabelDefSchema),\n commentItems: z.array(dailyReportCommentItemSchema),\n // ❗ `.default([])` は必須。SSE の実体は永続 Redis Stream (MAXLEN 保持) であり、\n // 配備をまたいで**旧形式のエントリが残る**。共有リーダーはプロセス起動時に先頭から\n // 再生し、再接続クライアントの catch-up も同じエントリを読み直すため、\n // このキーを必須にすると添付機能を入れる前に publish された全エントリが\n // fail-closed で捨てられ、再接続タブへ更新が永久に届かなくなる。\n // 送信側の詰め忘れ検知は types.ts の必須フィールドと `_AssertDetailCompat`\n // (出力型は既定値適用後なので必須のまま) が担っており、ここを緩めても失われない。\n attachments: z.array(dailyReportAttachmentItemSchema).default([]),\n})\n\n// --- SSE メッセージスキーマ(8種) ---\n\nexport const connectedMessageSchema = z.object({\n type: z.literal(\"connected\"),\n})\n\nexport const statusUpdateMessageSchema = z.object({\n type: z.literal(\"status-update\"),\n reportHubId: z.number(),\n statusType: z.enum([\"star\", \"read\"]),\n value: z.boolean(),\n clientTempId: z.string(),\n // recipientRawUserId はサーバーサイドフィルタリング専用。\n // SSE ルートが raw JSON から取得してフィルタ後に除去する。\n // クライアントには到達しないが、サーバー側の publish で .parse() を通すため定義が必要。\n recipientRawUserId: z.number().optional(),\n})\n\nexport const commentAddMessageSchema = z.object({\n type: z.literal(\"comment-add\"),\n reportHubId: z.number(),\n comment: dailyReportCommentItemSchema,\n clientTempId: z.string(),\n})\n\nexport const commentDeleteMessageSchema = z.object({\n type: z.literal(\"comment-delete\"),\n reportHubId: z.number(),\n commentId: z.number(),\n clientTempId: z.string(),\n})\n\nexport const reportCreateMessageSchema = z.object({\n type: z.literal(\"report-create\"),\n reportHubId: z.number(),\n report: dailyReportDetailSchema,\n clientTempId: z.string(),\n recipientRawUserId: z.number().optional(),\n})\n\nexport const reportUpdateMessageSchema = z.object({\n type: z.literal(\"report-update\"),\n reportHubId: z.number(),\n report: dailyReportDetailSchema,\n clientTempId: z.string(),\n recipientRawUserId: z.number().optional(),\n})\n\nexport const reportPublishMessageSchema = z.object({\n type: z.literal(\"report-publish\"),\n reportHubId: z.number(),\n report: dailyReportDetailSchema,\n clientTempId: z.string(),\n recipientRawUserId: z.number().optional(),\n})\n\nexport const reportDeleteMessageSchema = z.object({\n type: z.literal(\"report-delete\"),\n reportHubId: z.number(),\n clientTempId: z.string(),\n})\n\n// --- Discriminated Union ---\n\nexport const dailyReportSseMessageSchema = z.discriminatedUnion(\"type\", [\n connectedMessageSchema,\n statusUpdateMessageSchema,\n commentAddMessageSchema,\n commentDeleteMessageSchema,\n reportCreateMessageSchema,\n reportUpdateMessageSchema,\n reportPublishMessageSchema,\n reportDeleteMessageSchema,\n])\n\n// --- 型エクスポート ---\n\nexport type DailyReportSseMessage = z.infer<typeof dailyReportSseMessageSchema>\n\n// --- コンパイル時の型互換チェック ---\n// Zod推論型が既存の型定義に代入可能であることを保証する。\n// ここでエラーが出たらスキーマと types.ts がずれている。\n\ntype _AssertDetailCompat = z.infer<typeof dailyReportDetailSchema> extends DeclaredDetail ? true : never\ntype _AssertCommentCompat = z.infer<typeof dailyReportCommentItemSchema> extends DeclaredComment ? true : never\nconst _detailCheck: _AssertDetailCompat = true\nconst _commentCheck: _AssertCommentCompat = true\nvoid _detailCheck\nvoid _commentCheck\n","/**\n * ETag generation helper for JSON payloads.\n * JSON ペイロード向けの ETag 生成ヘルパー。\n */\nimport { createHash } from \"node:crypto\"\n\n/**\n * Generates a SHA-256 ETag for the given data.\n * 指定されたデータの SHA-256 ETag を生成します。\n *\n * @param data The data to hash (will be JSON stringified).\n * @returns The ETag string (wrapped in quotes).\n */\nexport const generateETag = (data: unknown): string => {\n const json = JSON.stringify(data)\n const hash = createHash(\"sha256\").update(json).digest(\"hex\")\n return `\"${hash}\"`\n}\n","/**\n * JSON response helper with shared security headers and ETag support.\n * 共有セキュリティヘッダーと ETag 対応を備えた JSON レスポンスヘルパー。\n */\nimport { createLogger, type DailyReportLogger, LogLevel } from \"../shared/logger\"\nimport { generateETag } from \"./etag\"\n\nconst defaultLogger = createLogger(LogLevel.INFO, \"[Response]\")\n\n/**\n * Creates JSON response with shared security headers and ETag support.\n * 共有のセキュリティヘッダーと ETag サポート付き JSON レスポンスを生成。\n */\nexport const jsonResponseWithETag = (request: Request, cookie: string | null, payload: Record<string, unknown>, status = 200, logger: DailyReportLogger = defaultLogger): Response => {\n const etag = generateETag(payload)\n const ifNoneMatch = request.headers.get(\"If-None-Match\")\n\n logger.info(`[jsonResponseWithETag] ETag: ${etag}, If-None-Match: ${ifNoneMatch}`)\n\n const headers = new Headers({\n \"Content-Type\": \"application/json\",\n \"Cache-Control\": \"private, max-age=0, must-revalidate\",\n \"X-Content-Type-Options\": \"nosniff\",\n \"X-Frame-Options\": \"DENY\",\n \"Content-Security-Policy\": \"default-src 'none'\",\n ETag: etag,\n })\n if (cookie) {\n headers.append(\"Set-Cookie\", cookie)\n }\n\n if (request.method === \"GET\" && status === 200 && ifNoneMatch === etag) {\n return new Response(null, { status: 304, headers })\n }\n\n return new Response(JSON.stringify(payload), { status, headers })\n}\n","/**\n * Fan-Out shared reader for the daily report SSE Redis Stream.\n * 日報 SSE 用 Redis Stream の共有リーダーモジュール。\n *\n * 単一の xRead BLOCK ループで全接続にメッセージをブロードキャストし、\n * 接続ごとに Redis TCP を張る問題を解消する。\n */\n\nimport { EventEmitter } from \"node:events\"\nimport { createLogger, type DailyReportLogger, LogLevel } from \"../shared/logger\"\nimport type { SqlResultCache } from \"./cache\"\nimport type { DailyReportRedisBlockingClient, DailyReportRedisProvider } from \"./ports\"\n\n/**\n * Compares two Redis Stream IDs numerically.\n * Redis Stream ID を数値比較し、a <= b なら true を返す。\n *\n * Stream ID は `<timestamp_ms>-<sequence>` 形式。\n * 辞書順比較では sequence が可変長のとき不正確になるため、数値分割で比較する。\n */\nexport const isStreamIdLte = (a: string, b: string): boolean => {\n const [aMs, aSeq] = a.split(\"-\").map(Number)\n const [bMs, bSeq] = b.split(\"-\").map(Number)\n if (aMs !== bMs) return aMs < bMs\n return aSeq <= bSeq\n}\n\n/** xRead 結果の 1 エントリに相当する型。 */\nexport type StreamEntry = {\n id: string\n message: Record<string, string>\n}\n\nexport type DailyReportSseReaderConfig = {\n /** ブロッキング xRead 用の専用クライアントを生成する redis プロバイダー。 */\n redis?: DailyReportRedisProvider\n /** SSE Redis Stream キー。 */\n streamKey: string\n /** 受信メッセージに応じてローカル SQL キャッシュを無効化する対象キャッシュ。 */\n cache: SqlResultCache\n /** ロガー (既定は console ベース)。 */\n logger?: DailyReportLogger\n}\n\n/**\n * Shared xRead loop that fans out entries to all active SSE connections.\n * 全 SSE 接続に対してメッセージをファンアウトする共有 xRead ループ。\n *\n * - \"entry\" イベント: 新着エントリ (StreamEntry) をブロードキャスト\n * - \"error\" イベント: 回復不能エラーをブロードキャスト\n * */\nexport class DailyReportSseReader {\n private _emitter = new EventEmitter()\n private _state: \"idle\" | \"running\" | \"stopping\" = \"idle\"\n private _refCount = 0\n private _lastId = \"0-0\"\n private _client: DailyReportRedisBlockingClient | undefined\n private readonly _logger: DailyReportLogger\n\n constructor(private readonly config: DailyReportSseReaderConfig) {\n // リスナー上限を緩和 (接続数分)\n this._emitter.setMaxListeners(0)\n this._logger = config.logger ?? createLogger(LogLevel.INFO, \"[SSE Reader]\")\n }\n\n /**\n * Subscribes to the shared reader. Starts the loop on first subscriber.\n * 共有リーダーを購読する。最初の購読者でループを開始する。\n *\n * @param onEntry - 新着エントリのコールバック\n * @param onError - 回復不能エラーのコールバック (省略可)\n * @returns unsubscribe 関数。呼び出すと両リスナーを解除し、最後の購読者解除でループを停止する。\n */\n subscribe(onEntry: (entry: StreamEntry) => void, onError?: (err: Error) => void): () => void {\n this._emitter.on(\"entry\", onEntry)\n if (onError) {\n this._emitter.on(\"error\", onError)\n }\n this._refCount++\n\n // 最初の購読者でループ開始\n if (this._refCount === 1) {\n void this._startLoop()\n }\n\n // unsubscribe\n return () => {\n this._emitter.removeListener(\"entry\", onEntry)\n if (onError) {\n this._emitter.removeListener(\"error\", onError)\n }\n this._refCount--\n if (this._refCount <= 0) {\n this._refCount = 0\n void this._stopLoop()\n }\n }\n }\n\n /**\n * Destroys the reader for test cleanup.\n * テストクリーンアップ用のデストラクタ。\n */\n async destroy(): Promise<void> {\n this._refCount = 0\n await this._stopLoop()\n this._emitter.removeAllListeners()\n this._lastId = \"0-0\"\n }\n\n /**\n * Internal xRead BLOCK loop.\n * 内部 xRead BLOCK ループ。\n */\n private async _startLoop(): Promise<void> {\n // stopping 中なら完了を待つ\n while (this._state === \"stopping\") {\n await new Promise((resolve) => setTimeout(resolve, 50))\n }\n if (this._state === \"running\") return\n\n this._state = \"running\"\n\n // 専用クライアント取得。createClient が reject した場合も _state を idle へ戻し、\n // 再購読でループを再開できるようにする (注入 redis 実装が例外を投げても state-stuck しない)。\n try {\n this._client = await this.config.redis?.createClient()\n } catch (err) {\n this._state = \"idle\"\n this._emitter.emit(\"error\", err instanceof Error ? err : new Error(\"Failed to create Redis client for SSE reader\"))\n return\n }\n if (!this._client) {\n this._state = \"idle\"\n this._emitter.emit(\"error\", new Error(\"Failed to create Redis client for SSE reader\"))\n return\n }\n\n // エラーリスナー\n this._client.on(\"error\", (err) => {\n this._logger.error(\"Redis client error:\", err)\n })\n\n try {\n while (this._state === \"running\" && this._client?.isOpen) {\n const results = await this._client.xRead([{ key: this.config.streamKey, id: this._lastId }], { BLOCK: 5000, COUNT: 100 })\n if (!results) continue\n\n for (const stream of results) {\n for (const msg of stream.messages) {\n this._lastId = msg.id\n // epoch check の補完: SSE 経由でもローカルキャッシュを即座にクリア\n // (epoch check が主防御線。SSE は Redis ダウン時や非 epoch 対象キャッシュへのフォールバック)\n try {\n const parsed = msg.message?.data ? JSON.parse(msg.message.data) : null\n const msgType = parsed?.type\n if (msgType === \"report-create\" || msgType === \"report-delete\" || msgType === \"report-publish\") {\n this.config.cache.invalidatePrefix(\"daily-report:ids\")\n }\n // コメント・ステータス変更時は対象レポートの詳細キャッシュを無効化\n if (msgType === \"comment-add\" || msgType === \"comment-delete\" || msgType === \"status-update\") {\n const reportHubId = parsed?.reportHubId\n if (typeof reportHubId === \"number\") {\n this.config.cache.invalidate(`daily-report:detail:${reportHubId}`)\n this.config.cache.invalidatePrefix(`daily-report:detail:${reportHubId}:user:`)\n }\n }\n } catch {\n // JSON パースエラーは無視 (invalidation スキップ = safe 方向)\n }\n this._emitter.emit(\"entry\", { id: msg.id, message: msg.message as Record<string, string> })\n }\n }\n }\n } catch (err) {\n // ClientClosedError は正常停止時に発生する\n const isClientClosed = (err as Error)?.constructor?.name === \"ClientClosedError\"\n if (!isClientClosed && this._state === \"running\") {\n this._logger.error(\"xRead loop error:\", err)\n this._emitter.emit(\"error\", err)\n }\n } finally {\n // クライアント切断\n if (this._client?.isOpen) {\n try {\n await this._client.quit()\n } catch (_e) {\n // 切断エラーは無視\n }\n }\n this._client = undefined\n this._state = \"idle\"\n\n // ループ終了後に購読者が残っていれば自動再起動\n if (this._refCount > 0) {\n void this._startLoop()\n }\n }\n }\n\n /**\n * Stops the xRead loop by closing the client.\n * クライアントを閉じて xRead ループを停止する。\n */\n private async _stopLoop(): Promise<void> {\n if (this._state !== \"running\") return\n this._state = \"stopping\"\n\n if (this._client?.isOpen) {\n try {\n await this._client.quit()\n } catch (_e) {\n // 切断エラーは無視\n }\n }\n\n // idle になるまで待機。_state は別の非同期ループ (connect の run ループ) が \"idle\" に戻すが、\n // TS は await を跨いだクラスフィールドの変化を追えず \"stopping\" に過剰 narrowing するため、宣言型へキャストして比較する。\n while ((this._state as \"idle\" | \"running\" | \"stopping\") !== \"idle\") {\n await new Promise((resolve) => setTimeout(resolve, 50))\n }\n }\n}\n","/**\n * React Router loader/action factories for the daily-report HTTP surface.\n * 日報 HTTP サーフェス向けの React Router loader / action 工場。\n *\n * - index.loader: 画面ルートの認証 + 難読化ユーザー ID 解決\n * - api.loader / api.action: `:endpoint` パラメータ式の認証付き API ルーター\n * - sse.loader: Redis Streams ベースのリアルタイム更新 SSE エンドポイント\n */\nimport { normalizeBusinessDateKey } from \"../shared/business-date\"\nimport { createLogger, type DailyReportLogger, LogLevel } from \"../shared/logger\"\nimport { dailyReportSseMessageSchema } from \"../shared/sse-schema\"\nimport type { DailyReportAttachmentFailure, DailyReportAuthenticate, DailyReportEncodeUserId, DailyReportIdCodec, DailyReportReadAttachment, DailyReportRedisProvider } from \"./ports\"\nimport { jsonResponseWithETag } from \"./response\"\nimport type { DailyReportService } from \"./service\"\nimport type { DailyReportSseReader, StreamEntry } from \"./sse-reader\"\nimport { isStreamIdLte } from \"./sse-reader\"\n\ntype LoaderArgs = { request: Request; params: Record<string, string | undefined> }\n\n/** `data()` 相当の JSON レスポンス生成 (react-router 非依存)。 */\nconst jsonData = (payload: unknown, init?: { status?: number }): Response =>\n new Response(JSON.stringify(payload), {\n status: init?.status ?? 200,\n headers: { \"Content-Type\": \"application/json\" },\n })\n\n// ---------------- 添付配信の定数とヘルパー (純粋・モジュールスコープ) ----------------\n\n/** 添付 1 件あたりの既定上限。サービス側のワイヤ上限 (既定 64 MiB) より必ず低く保つ。 */\nconst DEFAULT_ATTACHMENT_MAX_BYTES = 32 * 1024 * 1024\n\n/**\n * Default per-user rate limit (calls per minute), enforced per process.\n * ユーザー単位の既定レート上限 (1 分あたり)。ただし**プロセス単位**で計上する。\n *\n * バケットはハンドラーのクロージャに置く素の `Map` であり、プロセス間で共有されない。\n * 消費アプリが Node Cluster 等で複数ワーカーを起動する場合、コンテナ全体の実効上限は\n * この値のワーカー数倍になる。全体で厳密に効かせたいなら共有ストア (Redis 等) が要る。\n */\nconst DEFAULT_ATTACHMENT_RATE_LIMIT_PER_MINUTE = 60\n\n/**\n * Default simultaneous-read limit, enforced per process.\n * 既定の同時実行上限。**プロセス単位**で計上する。\n *\n * 1 リクエストが上限バイト数をヒープへ載せるため小さく保つ。ヒープ見積りは\n * 「この値 × `attachmentMaxBytes` × ワーカー数」であり、ワーカー数を掛け忘れると\n * コンテナのメモリ上限を実際の数倍で見誤る。消費アプリはワーカー数を織り込んだ値を\n * `attachmentConcurrency` で明示注入すること。\n *\n * スロットは**本文をクライアントへ送出し終えるまで**保持する。読み取り完了時点で\n * 解放すると、前の応答のバイト列がヒープに載ったまま次の読み取りが始まるため、\n * 上の見積りが成立しなくなる (実際の同時保持数は無制限になる)。\n */\nconst DEFAULT_ATTACHMENT_CONCURRENCY = 4\n\n/**\n * Backstop for releasing a concurrency slot when the client never drains the body.\n * クライアントが本文を読み切らない場合に同時実行スロットを解放するバックストップ。\n *\n * スロットを送出完了まで保持する以上、接続を張ったまま読み止めたクライアントが\n * スロットを永久に占有できてしまう。これは締め切りではなく枯渇防止の保険であり、\n * 超過しても応答は中断しない (単にゲートの会計上、保持されていない扱いにするだけ)。\n */\nconst ATTACHMENT_BODY_FLUSH_TIMEOUT_MS = 120_000\n\n/** レート制限テーブルの上限件数。無制限 Map はプロセス寿命の間だけ単調増加するリークになる。 */\nconst RATE_LIMITER_MAX_ENTRIES = 1024\n\n/** レート上限超過時に提示する再試行間隔 (秒)。 */\nconst RATE_LIMIT_RETRY_AFTER_SECONDS = 60\n\n/** 同時実行上限に達したときに提示する再試行間隔 (秒)。 */\nconst CONCURRENCY_RETRY_AFTER_SECONDS = 5\n\n/**\n * Media types allowed to render inline in the browser.\n * ブラウザーへインライン表示してよい media type。\n *\n * これ以外は必ず添付ダウンロードにし、Content-Type も `application/octet-stream` へ落とす。\n * `file_type` は外部システム由来の未検証値であり、`text/html` や `image/svg+xml` を\n * 自オリジンでインライン配信するとセッション Cookie を持つ文脈でスクリプトが動く。\n * `X-Content-Type-Options: nosniff` は「宣言型から外れた推測」を止めるだけで、\n * 宣言された型の実行は止めない。\n */\nconst INLINE_SAFE_MEDIA_TYPES: ReadonlySet<string> = new Set([\"image/png\", \"image/jpeg\", \"image/gif\", \"image/webp\", \"application/pdf\", \"text/plain\"])\n\n/** ヘッダー値として安全で、かつ MIME 型の形をしている値だけを通す。 */\nconst HEADER_SAFE_PATTERN = /^[\\x20-\\x7E]+$/\nconst MEDIA_TYPE_PATTERN = /^[\\w.+-]+\\/[\\w.+-]+/\n\n/**\n * Accepts a media type only when it is header-safe and well-formed.\n * ヘッダー安全かつ MIME 型の形をしている場合のみ採用する処理。\n *\n * CR/LF や 0x00-0xFF 外の文字が混ざった値を `new Headers()` へ渡すと TypeError になり、\n * 認可済みのダウンロードが 500 になる。\n *\n * **これは第 2 層である。** 現在の配信経路では、注入値はインライン許可リストに完全一致しないため\n * 必ず `application/octet-stream` へ落ち、ヘッダーには到達しない。つまり本関数を外しても\n * 現状の挙動は変わらない (ミューテーションテストで確認済み)。\n * 許可判定を前方一致や「DB の型を信じる」形へ変えた瞬間に効き始める防御なので、\n * 到達不能なまま放置せず **直接の単体テストで固定する**目的でエクスポートしている。\n *\n * @param value Candidate media type. 候補となる media type。\n * @returns The value when acceptable, otherwise null. 採用可なら値、それ以外は null。\n */\nexport const sanitizeMediaType = (value: string | null | undefined): string | null => (value && HEADER_SAFE_PATTERN.test(value) && MEDIA_TYPE_PATTERN.test(value) ? value : null)\n\n/**\n * Percent-encodes a file name for the RFC 8187 `filename*` parameter.\n * RFC 8187 の `filename*` 用にファイル名をパーセントエンコードする処理。\n *\n * `encodeURIComponent` は `'` `(` `)` `*` を残すが、いずれも RFC 8187 の attr-char ではない。\n *\n * @param name Raw file name. 生のファイル名。\n * @returns Encoded value. エンコード済みの値。\n */\nexport const encodeRfc8187 = (name: string): string => encodeURIComponent(name).replace(/['()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`)\n\n/**\n * Builds the ASCII fallback used by the plain `filename` parameter.\n * 素の `filename` パラメータ用の ASCII フォールバックを組み立てる処理。\n *\n * @param name Raw file name. 生のファイル名。\n * @returns ASCII-only, quote-free name. ASCII のみで引用符を含まない名前。\n */\nexport const asciiFallbackFileName = (name: string): string => name.replace(/[^\\x20-\\x7E]/g, \"_\").replace(/[\"\\\\]/g, \"_\")\n\n/**\n * Maps a storage read failure onto the HTTP status the endpoint returns.\n * ストレージ読み取りの失敗種別を、エンドポイントが返す HTTP ステータスへ写像する処理。\n *\n * `not_found` と `denied` はどちらも 404 にする。認可判定は既に SQL 側で完了しているため\n * 横断的な情報漏洩は無いが、実体の有無を 403/404 で区別すると存在オラクルになる。\n *\n * @param reason Failure kind reported by the port. ポートが報告した失敗種別。\n * @returns HTTP status code. HTTP ステータスコード。\n */\nconst attachmentFailureToStatus = (reason: DailyReportAttachmentFailure): number => {\n switch (reason) {\n case \"not_found\":\n case \"denied\":\n case \"invalid_path\":\n return 404\n case \"too_large\":\n return 413\n default:\n return 502\n }\n}\n\n/**\n * Client-facing messages per failure status.\n * 失敗ステータスごとのクライアント向けメッセージ。\n *\n * 404 は「存在しない」「認可されない」「実体が消えた」の合流点なので、\n * どの経路から来ても同一の文面にする (差分が存在オラクルになるため)。\n */\nconst ATTACHMENT_FAILURE_MESSAGES: Readonly<Record<number, string>> = {\n 404: \"Attachment not found\",\n 413: \"Attachment too large\",\n 502: \"Attachment storage unavailable\",\n}\n\n/** レート制限 1 ユーザー分の状態。 */\ntype RateBucket = { tokens: number; lastRefillMs: number }\n\n/**\n * Creates a bounded per-user token-bucket rate limiter (process-local).\n * ユーザー単位のトークンバケット方式レート制限を、件数上限付きで生成する処理 (プロセス内限定)。\n *\n * 上限を設ける理由: 素の `Map` で保持するとプロセス寿命の間だけ単調増加し、\n * 「サーバー側の状態は上限を持つ」という設計方針 (SqlResultCache の >500 GC) に反する。\n * 上限到達時は挿入順が最も古いエントリから落とす。\n *\n * 状態はプロセス内に閉じるため、マルチワーカー配備では実効上限がワーカー数倍になる\n * (`DEFAULT_ATTACHMENT_RATE_LIMIT_PER_MINUTE` 参照)。\n *\n * @param limitPerMinute Allowed calls per minute. 1 分あたりの許可回数。\n * @returns A function that consumes one token and reports whether it was allowed. トークンを 1 つ消費し可否を返す関数。\n */\nconst createRateLimiter = (limitPerMinute: number) => {\n const buckets = new Map<number, RateBucket>()\n return (userId: number, nowMs: number): boolean => {\n const existing = buckets.get(userId)\n // アクセスのたびに再挿入して LRU 順序にする (Map は挿入順を保つ)\n if (existing) buckets.delete(userId)\n\n const bucket = existing ?? { tokens: limitPerMinute, lastRefillMs: nowMs }\n const elapsedMs = Math.max(0, nowMs - bucket.lastRefillMs)\n if (elapsedMs > 0) {\n bucket.tokens = Math.min(limitPerMinute, bucket.tokens + (elapsedMs * limitPerMinute) / 60_000)\n bucket.lastRefillMs = nowMs\n }\n\n const allowed = bucket.tokens >= 1\n if (allowed) bucket.tokens -= 1\n\n buckets.set(userId, bucket)\n while (buckets.size > RATE_LIMITER_MAX_ENTRIES) {\n const oldest = buckets.keys().next()\n if (oldest.done) break\n buckets.delete(oldest.value)\n }\n return allowed\n }\n}\n\n/**\n * Creates a fail-fast concurrency gate (no queueing, process-local).\n * 待ち行列を持たない即時失敗型の同時実行ゲートを生成する処理 (プロセス内限定)。\n *\n * 待たせるとリクエストを掴んだまま滞留するだけなので、上限超過は即座に 503 を返す。\n * カウンタはプロセス内に閉じるため、マルチワーカー配備ではコンテナ全体の同時読み取り数が\n * 上限のワーカー数倍になる (`DEFAULT_ATTACHMENT_CONCURRENCY` 参照)。\n *\n * @param limit Maximum simultaneous holders. 同時保持数の上限。\n * @returns Acquire/release pair. 取得と解放の組。\n */\nconst createConcurrencyGate = (limit: number) => {\n let active = 0\n return {\n tryAcquire: (): boolean => {\n if (active >= limit) return false\n active += 1\n return true\n },\n release: (): void => {\n if (active > 0) active -= 1\n },\n }\n}\n\n/**\n * Streams already-materialized bytes and releases the held slot once the body is drained.\n * materialize 済みのバイト列をストリームとして送出し、送出完了時に保持中のスロットを解放する処理。\n *\n * 読み取り完了時点で解放してはならない。その時点でバイト列はまだヒープ上にあり、\n * 応答が流し切られるまで参照が保持されるため、「同時実行上限 × 上限バイト数」という\n * ヒープ見積りが成立しなくなる。ここで送出完了まで保持することで見積りを実際に成立させる。\n *\n * 解放は必ず 1 回だけ行う (正常終了・キャンセル・バックストップのいずれか最初の 1 回)。\n *\n * @param bytes Body bytes to send. 送出する本文のバイト列。\n * @param release Idempotent slot release. 冪等なスロット解放関数。\n * @param timeoutMs Backstop before force-releasing. 強制解放までのバックストップ時間。\n * @returns A stream that emits the bytes once. バイト列を 1 度だけ流すストリーム。\n */\nconst streamAndRelease = (bytes: Uint8Array<ArrayBuffer>, release: () => void, timeoutMs: number): ReadableStream<Uint8Array> => {\n const timer: ReturnType<typeof setTimeout> = setTimeout(release, timeoutMs)\n // タイマーがプロセス終了を待たせないようにする (Node 以外では unref を持たない)\n ;(timer as unknown as { unref?: () => void }).unref?.()\n const finish = () => {\n clearTimeout(timer)\n release()\n }\n\n let sent = false\n return new ReadableStream<Uint8Array>({\n pull(controller) {\n // 1 回目の pull で本文を積み、消費された後の 2 回目の pull で閉じて解放する\n if (sent) {\n controller.close()\n finish()\n return\n }\n sent = true\n controller.enqueue(bytes)\n },\n cancel() {\n finish()\n },\n })\n}\n\nexport type DailyReportHandlersConfig = {\n /** リクエスト認証ポート。 */\n authenticate: DailyReportAuthenticate\n /** データアクセスサービス。 */\n service: DailyReportService\n /** 内部数値 ID の難読化ポート。 */\n encodeUserId: DailyReportEncodeUserId\n /**\n * 添付 ID の難読化コーデック。未注入なら添付エンドポイントは常に 404。\n * ユーザー ID 用とは別インスタンスを渡すこと (ID 空間の分離)。\n */\n attachmentIdCodec?: DailyReportIdCodec\n /** 添付ファイル読み取りポート。未注入なら添付エンドポイントは常に 404。 */\n readAttachment?: DailyReportReadAttachment\n /** 添付ファイルの最大バイト数 (既定 32 MiB)。サービス側のワイヤ上限より低く保つこと。 */\n attachmentMaxBytes?: number\n /** 添付エンドポイントの 1 分あたり呼び出し上限 (ユーザー単位・既定 60)。 */\n attachmentRateLimitPerMinute?: number\n /** 添付読み取りの同時実行上限 (プロセス単位・既定 4)。 */\n attachmentConcurrency?: number\n /** SSE 用 redis プロバイダー (catch-up の xRange / xRevRange に使用)。 */\n redis?: DailyReportRedisProvider\n /** SSE Fan-Out 共有リーダー。 */\n sseReader: DailyReportSseReader\n /** SSE Redis Stream キー。 */\n streamKey: string\n /** 未ログイン時のリダイレクト先 (index.loader 用、既定 \"/auth/login\")。 */\n loginRedirectPath?: string\n /** ロガー (既定は console ベース)。 */\n logger?: DailyReportLogger\n}\n\n/**\n * Creates the daily-report loaders/actions bound to the injected dependencies.\n * 注入依存に束縛された日報 loader / action 群を生成する処理。\n */\nexport function createDailyReportHandlers(config: DailyReportHandlersConfig) {\n const { authenticate, service, encodeUserId, redis, sseReader, streamKey } = config\n const loginRedirectPath = config.loginRedirectPath ?? \"/auth/login\"\n const apiLogger = config.logger ?? createLogger(LogLevel.ERROR, \"[DailyReportAPI]\")\n const sseLogger = config.logger ?? createLogger(LogLevel.INFO, \"[DailyReportSSE]\")\n const attachmentLogger = config.logger ?? createLogger(LogLevel.INFO, \"[DailyReportAttachment]\")\n\n // 添付配信のプロセス内状態。ファクトリ 1 インスタンスにつき 1 組。\n const attachmentMaxBytes = config.attachmentMaxBytes ?? DEFAULT_ATTACHMENT_MAX_BYTES\n const consumeAttachmentRateToken = createRateLimiter(config.attachmentRateLimitPerMinute ?? DEFAULT_ATTACHMENT_RATE_LIMIT_PER_MINUTE)\n const attachmentGate = createConcurrencyGate(config.attachmentConcurrency ?? DEFAULT_ATTACHMENT_CONCURRENCY)\n\n // ---------------- index (画面ルート) ----------------\n\n /**\n * Document loader that authenticates and resolves the obfuscated internal user id.\n * 認証と難読化済み内部ユーザー ID の解決を行うドキュメントローダー。\n */\n const indexLoader = async ({ request }: LoaderArgs) => {\n const { user, cookie } = await authenticate(request, { failureRedirect: loginRedirectPath })\n const headers = new Headers()\n if (cookie) {\n headers.append(\"Set-Cookie\", cookie)\n }\n\n let userId: number | null = null\n if (user) {\n userId = await service.getUserIdByExternalId(user.id)\n }\n\n const hashedUserId = userId ? encodeUserId(userId) : null\n\n return { data: { user, userId: hashedUserId }, headers }\n }\n\n // ---------------- api (:endpoint ルーター) ----------------\n\n type User = { id: string }\n type EndpointHandler = (url: URL, cookie: string | null, request: Request, user: User) => Promise<Response>\n\n /**\n * Handlers for each API endpoint.\n * 各 API エンドポイントのハンドラー定義。\n */\n const endpointHandlers: Record<string, EndpointHandler> = {\n /**\n * Retrieves daily reports for a specific business date.\n * 指定された営業日の日報一覧を取得する。\n */\n \"business-date\": async (url, cookie, request, user) => {\n const normalizedBusinessDate = normalizeBusinessDateKey(url.searchParams.get(\"businessDate\"))\n const forceRefresh = url.searchParams.get(\"forceRefresh\") === \"true\"\n\n if (!normalizedBusinessDate) {\n return jsonResponseWithETag(request, cookie, { error: { message: \"Invalid business date\" } }, 400)\n }\n const reports = await service.getDailyReportsByBusinessDateByExternalId(normalizedBusinessDate, user.id, { forceRefresh })\n return jsonResponseWithETag(request, cookie, { businessDate: normalizedBusinessDate, reports }, 200)\n },\n /**\n * Retrieves a list of all daily report IDs.\n * 全ての日報 ID の一覧を取得する。\n */\n ids: async (url, cookie, request, user) => {\n const forceRefresh = url.searchParams.get(\"forceRefresh\") === \"true\"\n const dailyReportIds = await service.getDailyReportIdsByExternalId(user.id, { forceRefresh })\n return jsonResponseWithETag(request, cookie, { ids: dailyReportIds }, 200)\n },\n /**\n * Retrieves details for a specific daily report.\n * 指定された日報の詳細情報を取得する。\n */\n report: async (url, cookie, request, user) => {\n const param = url.searchParams.get(\"reportHubId\")\n const forceRefresh = url.searchParams.get(\"forceRefresh\") === \"true\"\n const parsedId = param ? Number.parseInt(param, 10) : NaN\n\n if (!Number.isFinite(parsedId) || parsedId <= 0) {\n return jsonResponseWithETag(request, cookie, { error: { message: \"Invalid reportHubId\" } }, 400)\n }\n const detail = await service.getDailyReportDetailByIdByExternalId(parsedId, user.id, { snapshot: true, forceRefresh })\n if (!detail) {\n return jsonResponseWithETag(request, cookie, { error: { message: \"Report not found\" } }, 404)\n }\n return jsonResponseWithETag(request, cookie, { report: detail }, 200)\n },\n }\n\n /**\n * Routes authenticated daily report API requests by endpoint.\n * エンドポイントごとに認証済みの日報 API リクエストを振り分ける。\n */\n const apiLoader = async ({ request, params }: LoaderArgs) => {\n const { user, cookie } = await authenticate(request, { failureRedirect: null })\n const sanitizedCookie = cookie ?? null\n\n if (!user) {\n return jsonResponseWithETag(request, sanitizedCookie, { error: { message: \"Unauthorized\" } }, 401)\n }\n\n if (request.method !== \"GET\") {\n return jsonResponseWithETag(request, sanitizedCookie, { error: { message: \"Method not allowed\" } }, 405)\n }\n const endpoint = params.endpoint ?? \"\"\n const handler = endpointHandlers[endpoint]\n if (!handler) {\n return jsonResponseWithETag(request, sanitizedCookie, { error: { message: \"Unknown endpoint\" } }, 404)\n }\n const startTime = Date.now()\n try {\n const url = new URL(request.url)\n return await handler(url, sanitizedCookie, request, user)\n } catch (error) {\n const elapsed = Date.now() - startTime\n const err = error instanceof Error ? error : new Error(String(error))\n apiLogger.error(`500 endpoint=${endpoint} elapsed=${elapsed}ms name=${err.name} code=${\"code\" in err ? (err as { code: unknown }).code : \"N/A\"} message=${err.message}`)\n apiLogger.error(\"Stack:\", err.stack)\n return jsonResponseWithETag(request, sanitizedCookie, { error: { message: \"Internal Server Error\" } }, 500)\n }\n }\n\n /**\n * Handles data mutations for daily reports.\n * 日報データの変更操作を処理するアクション。\n */\n const apiAction = async ({ request, params }: LoaderArgs) => {\n const { user } = await authenticate(request, { failureRedirect: null })\n if (!user) {\n return jsonData({ error: \"Unauthorized\" }, { status: 401 })\n }\n\n const endpoint = params.endpoint\n if (endpoint !== \"action\") {\n return jsonData({ error: \"Unknown endpoint\" }, { status: 404 })\n }\n\n const userId = await service.getUserIdByExternalId(user.id)\n if (!userId) {\n return jsonData({ error: \"User not found\" }, { status: 404 })\n }\n\n const formData = await request.formData()\n const intent = formData.get(\"intent\")\n const reportHubIdRaw = formData.get(\"reportHubId\")\n const reportHubId = reportHubIdRaw ? Number(reportHubIdRaw) : NaN\n const businessDate = formData.get(\"businessDate\") as string | null\n const operationTimestamp = Number(formData.get(\"operationTimestamp\"))\n const clientTempId = formData.get(\"clientTempId\") as string | null\n\n if (intent === \"clearCache\") {\n await service.clearCache()\n return jsonData({ status: \"OK\", intent: \"clearCache\" })\n }\n\n if (!clientTempId) {\n return jsonData({ error: \"clientTempId required\" }, { status: 400 })\n }\n\n if (intent !== \"create\" && (!reportHubId || Number.isNaN(reportHubId))) {\n return jsonData({ error: \"Invalid reportHubId\" }, { status: 400 })\n }\n\n switch (intent) {\n case \"create\": {\n if (!businessDate) {\n return jsonData({ error: \"businessDate required\" }, { status: 400 })\n }\n const newReport = await service.createDailyReport(userId, businessDate, clientTempId)\n return jsonData({\n status: \"OK\",\n intent: \"create\",\n report: newReport,\n reportHubId: String(newReport.reportHubId),\n clientTempId, // Echo back for validation\n })\n }\n case \"update\": {\n const title = formData.get(\"title\") as string | undefined\n const content = formData.get(\"content\") as string | undefined\n try {\n await service.updateDailyReport(reportHubId, userId, { title, content }, clientTempId)\n return jsonData({ status: \"OK\", intent: \"update\", reportHubId: String(reportHubId), clientTempId })\n } catch (e: unknown) {\n if (e instanceof Error) {\n if (e.message === \"Unauthorized\") {\n return jsonData({ error: \"Unauthorized\" }, { status: 403 })\n }\n if (e.message === \"Not Found\") {\n return jsonData({ error: \"Report not found\" }, { status: 404 })\n }\n }\n throw e\n }\n }\n case \"publish\": {\n try {\n const publishedReport = await service.publishDailyReport(reportHubId, userId, clientTempId)\n return jsonData({ status: \"OK\", intent: \"publish\", reportHubId: String(reportHubId), clientTempId, report: publishedReport })\n } catch (e: unknown) {\n if (e instanceof Error) {\n if (e.message === \"Unauthorized\") {\n return jsonData({ error: \"Unauthorized\" }, { status: 403 })\n }\n if (e.message === \"Not Found\") {\n return jsonData({ error: \"Report not found\" }, { status: 404 })\n }\n }\n throw e\n }\n }\n case \"delete\": {\n try {\n await service.deleteDailyReport(reportHubId, userId, clientTempId)\n return jsonData({\n status: \"OK\",\n intent: \"delete\",\n reportHubId: String(reportHubId),\n clientTempId, // Echo back for validation even on delete\n })\n } catch (e: unknown) {\n if (e instanceof Error) {\n if (e.message === \"Unauthorized\") {\n return jsonData({ error: \"Unauthorized\" }, { status: 403 })\n }\n if (e.message === \"Not Found\") {\n return jsonData({ error: \"Report not found\" }, { status: 404 })\n }\n }\n throw e\n }\n }\n case \"toggleStar\": {\n const isStarredRaw = formData.get(\"isStarred\")\n if (isStarredRaw === null) {\n return jsonData({ error: \"isStarred required\" }, { status: 400 })\n }\n const isStarred = isStarredRaw === \"true\"\n const updatedStatus = await service.setStarStatus(userId, reportHubId, businessDate, isStarred, clientTempId)\n // クライアントは isStarred/isRead のみ参照する。生内部 ID (userId/created_by/updated_by) を\n // 含む行全体は返さず、必要なフラグだけに絞る (内部 ID の自己開示防止)。\n return jsonData({ status: \"OK\", intent: \"toggleStar\", updatedStatus: { isStarred: updatedStatus.isStarred, isRead: updatedStatus.isRead }, operationTimestamp, reportHubId: String(reportHubId), clientTempId })\n }\n case \"toggleRead\": {\n const isReadRaw = formData.get(\"isRead\")\n if (isReadRaw === null) {\n return jsonData({ error: \"isRead required\" }, { status: 400 })\n }\n const isRead = isReadRaw === \"true\"\n const updatedStatus = await service.setReadStatus(userId, reportHubId, businessDate, isRead, clientTempId)\n // クライアントは isStarred/isRead のみ参照する (内部 ID の自己開示防止)。\n return jsonData({ status: \"OK\", intent: \"toggleRead\", updatedStatus: { isStarred: updatedStatus.isStarred, isRead: updatedStatus.isRead }, operationTimestamp, reportHubId: String(reportHubId), clientTempId })\n }\n case \"addComment\": {\n const content = formData.get(\"content\") as string\n if (!content) {\n return jsonData({ error: \"Content required\" }, { status: 400 })\n }\n const newComment = await service.addComment(userId, reportHubId, content, businessDate, clientTempId)\n const safeComment = { ...newComment, userId: newComment.userId }\n return jsonData({ status: \"OK\", intent: \"addComment\", newComment: safeComment, reportHubId: String(reportHubId), clientTempId })\n }\n case \"deleteComment\": {\n const commentId = Number(formData.get(\"commentId\"))\n if (!commentId || Number.isNaN(commentId)) {\n return jsonData({ error: \"Invalid commentId\" }, { status: 400 })\n }\n try {\n await service.deleteComment(userId, reportHubId, commentId, businessDate, clientTempId)\n return jsonData({ status: \"OK\", reportHubId: String(reportHubId), deletedCommentId: String(commentId), clientTempId })\n } catch (e: unknown) {\n if (e instanceof Error) {\n if (e.message === \"Unauthorized\") {\n return jsonData({ error: \"Unauthorized\" }, { status: 403 })\n }\n if (e.message === \"Not Found\") {\n return jsonData({ error: \"Comment not found\" }, { status: 404 })\n }\n }\n throw e\n }\n }\n default:\n return jsonData({ error: \"Invalid intent\" }, { status: 400 })\n }\n }\n\n // ---------------- sse (リアルタイム更新) ----------------\n\n /**\n * SSE endpoint for real-time daily report updates via Redis Streams.\n * Redis Streams を使用した日報リアルタイム更新の SSE エンドポイント。\n */\n const sseLoader = async ({ request, params }: LoaderArgs) => {\n // 認証チェック\n const { user } = await authenticate(request, { failureRedirect: null })\n if (!user) {\n return new Response(\"Unauthorized\", { status: 401 })\n }\n\n // 内部ユーザー ID の解決\n const internalUserId = await service.getUserIdByExternalId(user.id)\n if (!internalUserId) {\n return new Response(\"Forbidden\", { status: 403 })\n }\n\n // エンドポイントの検証\n if (params.endpoint !== \"updates\") {\n return new Response(\"Not Found\", { status: 404 })\n }\n\n // URL から lastEventId を取得 (再接続時の catch-up 用)\n // クエリパラメータ優先、EventSource 自動再接続時の Last-Event-ID ヘッダーにフォールバック\n const url = new URL(request.url)\n const lastEventId = url.searchParams.get(\"lastEventId\") || request.headers.get(\"Last-Event-ID\")\n\n const encoder = new TextEncoder()\n let controllerRef: ReadableStreamDefaultController<Uint8Array> | null = null\n let keepAliveInterval: ReturnType<typeof setInterval> | null = null\n let unsubscribe: (() => void) | null = null\n let isCleaningUp = false\n\n // クリーンアップ処理\n const cleanup = () => {\n if (isCleaningUp) return\n isCleaningUp = true\n\n // keep-alive タイマー停止\n if (keepAliveInterval) {\n clearInterval(keepAliveInterval)\n keepAliveInterval = null\n }\n\n // Fan-Out 購読解除 (entry / error 両リスナーを解除)\n if (unsubscribe) {\n unsubscribe()\n unsubscribe = null\n }\n\n // SSE ストリームの終了\n if (controllerRef) {\n try {\n if (controllerRef.desiredSize !== null) {\n controllerRef.close()\n }\n } catch (_e) {\n // ストリームが既に閉じている場合は無視\n } finally {\n controllerRef = null\n }\n }\n\n isCleaningUp = false\n }\n\n /**\n * Processes a single Redis Stream entry and sends it to the SSE client.\n * Redis Stream のエントリを処理し、SSE クライアントに送信する。\n */\n const processEntry = (entryId: string, fields: Record<string, string>) => {\n // data フィールドの存在チェック\n if (!fields.data) return\n\n try {\n const raw = JSON.parse(fields.data)\n const result = dailyReportSseMessageSchema.safeParse(raw)\n if (!result.success) {\n sseLogger.error(\"SSE message validation failed:\", result.error.format())\n return // Fail-Closed: 不正なメッセージは送信しない\n }\n const parsed = result.data\n\n // recipientRawUserId によるフィルタリング\n const recipientRawUserId = typeof raw.recipientRawUserId === \"number\" ? raw.recipientRawUserId : undefined\n\n if (parsed.type === \"status-update\") {\n if (recipientRawUserId === undefined || recipientRawUserId !== internalUserId) {\n return\n }\n } else if (recipientRawUserId !== undefined && recipientRawUserId !== internalUserId) {\n return\n }\n\n // recipientRawUserId をクライアントに送信しない (内部 ID 漏洩防止)\n let sanitizedMessage: string\n if (\"recipientRawUserId\" in raw) {\n const { recipientRawUserId: _, ...rest } = raw\n sanitizedMessage = JSON.stringify(rest)\n } else {\n sanitizedMessage = fields.data\n }\n\n // SSE フォーマットで送信 (id フィールド付き)\n if (controllerRef && controllerRef.desiredSize !== null) {\n controllerRef.enqueue(encoder.encode(`id: ${entryId}\\ndata: ${sanitizedMessage}\\n\\n`))\n }\n } catch (e) {\n sseLogger.error(`[SSE:${internalUserId}] processEntry error:`, e)\n }\n }\n\n const stream = new ReadableStream({\n async start(controller) {\n // 1. controllerRef 設定\n controllerRef = controller\n\n // 2. Keep-Alive: 5 秒ごとにコメントを送信して接続維持\n keepAliveInterval = setInterval(() => {\n try {\n if (controllerRef && controllerRef.desiredSize !== null) {\n controllerRef.enqueue(encoder.encode(\": keep-alive\\n\\n\"))\n } else {\n cleanup()\n }\n } catch (_e) {\n cleanup()\n }\n }, 5000)\n\n // 3. クライアント切断時のクリーンアップ (全 await の前に登録)\n request.signal.addEventListener(\"abort\", () => {\n cleanup()\n })\n\n // catch-up の基準 ID (Fan-Out エントリとの重複排除に使用)\n let lastProcessedId = lastEventId || \"0-0\"\n\n // 4. Fan-Out 共有リーダーを購読 (catch-up より先に登録して取りこぼしを防止)\n unsubscribe = sseReader.subscribe(\n (entry: StreamEntry) => {\n // catch-up 済みエントリはスキップ (重複排除)\n if (isStreamIdLte(entry.id, lastProcessedId)) return\n lastProcessedId = entry.id\n processEntry(entry.id, entry.message)\n },\n (err: Error) => {\n sseLogger.error(`[SSE:${internalUserId}] Fan-Out reader error:`, err)\n cleanup()\n },\n )\n\n // 5. 初回接続時: 最新 Stream ID を取得して connected の id: に使用\n // (再接続時は lastEventId が既にあるためスキップ)\n if (!lastEventId) {\n try {\n const client = await redis?.getClient()\n if (client) {\n const latest = await client.xRevRange(streamKey, \"+\", \"-\", { COUNT: 1 })\n // Fan-Out が既に lastProcessedId を進めている場合は巻き戻さない\n if (latest.length > 0 && !isStreamIdLte(latest[0].id, lastProcessedId)) {\n lastProcessedId = latest[0].id\n }\n }\n } catch {\n /* 取得失敗時は現在の lastProcessedId を維持 */\n }\n }\n\n // 6. 接続完了通知 (id: 付きで再接続時の catch-up アンカーを提供)\n if (controllerRef && controllerRef.desiredSize !== null) {\n const connectedPayload = `data: ${JSON.stringify({ type: \"connected\" })}\\n\\n`\n const sseMessage = lastProcessedId !== \"0-0\" ? `id: ${lastProcessedId}\\n${connectedPayload}` : connectedPayload\n controllerRef.enqueue(encoder.encode(sseMessage))\n }\n\n // 7. catch-up: lastEventId が指定されている場合、それ以降のエントリを一括取得\n if (lastEventId) {\n try {\n const client = await redis?.getClient()\n if (client) {\n const catchUpEntries = await client.xRange(streamKey, lastEventId, \"+\", { COUNT: 1000 })\n for (const entry of catchUpEntries) {\n // xRange は inclusive なので lastEventId 自身はスキップ\n if (entry.id === lastEventId) continue\n // Fan-Out が xRange await 中に処理済みのエントリはスキップ (重複排除)\n if (isStreamIdLte(entry.id, lastProcessedId)) continue\n lastProcessedId = entry.id\n processEntry(entry.id, entry.message as Record<string, string>)\n }\n }\n } catch (e) {\n sseLogger.error(`[SSE:${internalUserId}] catch-up xRange error:`, e)\n // catch-up 失敗時は Fan-Out のみで継続\n }\n }\n },\n cancel() {\n cleanup()\n },\n })\n\n return new Response(stream, {\n headers: {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache, no-transform\",\n Connection: \"keep-alive\",\n },\n })\n }\n\n // ---------------- attachment (認可済みバイト配信) ----------------\n\n /**\n * Streams an authorized attachment's bytes to the client.\n * 認可済みの添付ファイルをバイト列としてクライアントへ返すローダー。\n *\n * 認可は `service.getAttachmentForUser` の SQL 述語で完結させる。ストレージ側の\n * 資格情報はサービス共通のものであり、利用者間の分離を一切提供しない。\n * したがってこの述語が唯一の認可境界であり、ここに欠陥があれば全テナント横断の読み取りになる。\n *\n * 「存在しない」と「認可されない」は区別せず 404 にする (存在オラクル回避)。\n *\n * 本体は必ず try/catch で包む。ここで漏らした例外はフレームワークの最終防衛線に落ち、\n * JSON ではないプレーンテキストの 500 になるうえ `Set-Cookie` (セッション延長) も失われる。\n * **認証ポートの呼び出しも try の内側**に置くこと。認証実装はセッション復号や warmup の\n * 失敗で素の例外を投げうるため、外に置くと同じ穴が残る。ただしリダイレクトは `Response` を\n * throw する正常な制御フローなので、catch では素通しする。\n */\n const attachmentLoader = async ({ request, params }: LoaderArgs) => {\n let sanitizedCookie: string | null = null\n\n /** エラー応答。`Set-Cookie` はすべての分岐で伝播させる。 */\n const fail = (status: number, message: string, retryAfterSeconds?: number): Response => {\n const headers = new Headers({ \"Content-Type\": \"application/json\" })\n if (retryAfterSeconds !== undefined) headers.set(\"Retry-After\", String(retryAfterSeconds))\n if (sanitizedCookie) headers.append(\"Set-Cookie\", sanitizedCookie)\n return new Response(JSON.stringify({ error: { message } }), { status, headers })\n }\n\n try {\n const { user, cookie } = await authenticate(request, { failureRedirect: null })\n sanitizedCookie = cookie ?? null\n\n if (!user) return fail(401, \"Unauthorized\")\n if (request.method !== \"GET\" && request.method !== \"HEAD\") return fail(405, \"Method not allowed\")\n\n const codec = config.attachmentIdCodec\n const readAttachment = config.readAttachment\n // ポート未注入は「この配備に添付機能が無い」であり、認可失敗と区別しない\n if (!(codec && readAttachment)) return fail(404, \"Attachment not found\")\n\n const token = params.token ?? \"\"\n const attachmentId = codec.decode(token)\n // `Number.isSafeInteger` であること。可逆難読化は入力トークンの正準性を検証しないため、\n // 正規トークンと同じ長さの入力から 2^53 を超える値が復号されうる。それを SQL パラメータへ\n // 渡すとドライバーの範囲検証が TypeError を投げ、400 のはずが 500 になる。\n if (attachmentId === null || !Number.isSafeInteger(attachmentId) || attachmentId <= 0) {\n return fail(400, \"Invalid attachment token\")\n }\n\n const internalUserId = await service.getUserIdByExternalId(user.id)\n if (!internalUserId) return fail(403, \"Forbidden\")\n\n // レート制限。トークンは列挙可能なので、これが実効的な唯一の緩和策\n if (!consumeAttachmentRateToken(internalUserId, Date.now())) {\n attachmentLogger.error(`429 attachment=${attachmentId} viewer=${internalUserId} reason=rate_limit`)\n return fail(429, \"Too many requests\", RATE_LIMIT_RETRY_AFTER_SECONDS)\n }\n\n // 認可判定 (SQL 側で完結)。null は「存在しない」「認可されない」「親が論理削除済み」の合流\n const attachment = await service.getAttachmentForUser(attachmentId, internalUserId)\n if (!attachment) {\n attachmentLogger.error(`404 attachment=${attachmentId} viewer=${internalUserId} reason=not_visible`)\n return fail(404, \"Attachment not found\")\n }\n\n // DB が既知サイズを持つ場合は読み取り前に弾く (サイズを報告しない取込元の行はここを通過する)\n if (attachment.fileSize != null && attachment.fileSize > attachmentMaxBytes) {\n attachmentLogger.error(`413 attachment=${attachmentId} viewer=${internalUserId} reason=db_size`)\n return fail(413, \"Attachment too large\")\n }\n\n if (!attachmentGate.tryAcquire()) {\n attachmentLogger.error(`503 attachment=${attachmentId} viewer=${internalUserId} reason=concurrency`)\n return fail(503, \"Too many concurrent downloads\", CONCURRENCY_RETRY_AFTER_SECONDS)\n }\n\n // スロットの解放は冪等にする。早期 return・例外・送出完了のどの経路からでも\n // ちょうど 1 回だけ返す必要がある\n let gateReleased = false\n const releaseGate = () => {\n if (gateReleased) return\n gateReleased = true\n attachmentGate.release()\n }\n // 本文の送出完了まで保持する経路に入ったかどうか (入った場合のみ finally での解放を見送る)\n let releaseDeferredToStream = false\n\n try {\n const isHead = request.method === \"HEAD\"\n const result = await readAttachment(attachment.filePath, { maxBytes: attachmentMaxBytes, head: isHead, principal: encodeUserId(internalUserId) })\n\n if (!result.ok) {\n const status = attachmentFailureToStatus(result.reason)\n // 実体消失は次回描画へ反映する。記録の失敗は配信結果に影響させない\n // (await しないので、拒否を捕まえないと未処理 Promise 拒否になる)\n if (result.reason === \"not_found\") {\n void service.markAttachmentMissing(attachmentId).catch((error: unknown) => {\n attachmentLogger.error(`markAttachmentMissing failed attachment=${attachmentId} message=${error instanceof Error ? error.message : \"unknown\"}`)\n })\n }\n // file_path / URI は絶対にログへ出さない (内部パス非露出の方針と整合させる)\n attachmentLogger.error(`${status} attachment=${attachmentId} viewer=${internalUserId} reason=${result.reason} code=${result.code ?? \"-\"}`)\n return fail(status, ATTACHMENT_FAILURE_MESSAGES[status] ?? \"Attachment storage unavailable\")\n }\n\n // インライン許可リスト外は宣言型ごと octet-stream へ落とす。構文検証だけでは\n // text/html や image/svg+xml の自オリジン実行を止められない\n const declared = sanitizeMediaType(attachment.fileType) ?? sanitizeMediaType(result.contentType) ?? \"application/octet-stream\"\n const inlineSafe = INLINE_SAFE_MEDIA_TYPES.has(declared)\n const wantsDownload = new URL(request.url).searchParams.get(\"download\") === \"1\"\n const contentType = inlineSafe ? declared : \"application/octet-stream\"\n const disposition = inlineSafe && !wantsDownload ? \"inline\" : \"attachment\"\n\n // HEAD は本体を読まないので `bytes` が空になる。RFC 9110 §9.3.2 が要求する\n // 「GET と同じ Content-Length」を満たすため、ポートが申告した実サイズを優先する\n const contentLength = result.size ?? result.bytes.byteLength\n const headers = new Headers({\n \"Content-Type\": contentType,\n \"Content-Length\": String(contentLength),\n \"Content-Disposition\": `${disposition}; filename=\"${asciiFallbackFileName(attachment.fileName)}\"; filename*=UTF-8''${encodeRfc8187(attachment.fileName)}`,\n \"Cache-Control\": \"private, no-store\",\n \"X-Content-Type-Options\": \"nosniff\",\n \"Content-Security-Policy\": \"default-src 'none'; sandbox\",\n \"X-Frame-Options\": \"SAMEORIGIN\",\n })\n if (sanitizedCookie) headers.append(\"Set-Cookie\", sanitizedCookie)\n\n // 実体を確認できたので消失記録を解除する。これが唯一の自動回復経路であり、\n // 片方向のままだと一時障害で立った absent を利用者が自力で戻せない\n // (UI は absent の添付にリンクを描画しないため再取得の手段が消える)\n void service.markAttachmentPresent(attachmentId).catch((error: unknown) => {\n attachmentLogger.error(`markAttachmentPresent failed attachment=${attachmentId} message=${error instanceof Error ? error.message : \"unknown\"}`)\n })\n\n attachmentLogger.info(`200 attachment=${attachmentId} viewer=${internalUserId} type=${contentType} disposition=${disposition} bytes=${contentLength}`)\n // HEAD は本文を持たないので即座に解放する。GET は送出完了まで保持し、\n // 「同時実行上限 × 上限バイト数」というヒープ見積りを実際に成立させる\n if (isHead) {\n return new Response(null, { status: 200, headers })\n }\n releaseDeferredToStream = true\n return new Response(streamAndRelease(result.bytes, releaseGate, ATTACHMENT_BODY_FLUSH_TIMEOUT_MS), { status: 200, headers })\n } finally {\n // 送出へ引き渡した場合を除き、早期 return でも例外でも必ずここで返す\n if (!releaseDeferredToStream) releaseGate()\n }\n } catch (error) {\n // 認証ポートはリダイレクトを Response として throw する。これは正常な制御フローなので素通しする\n if (error instanceof Response) throw error\n attachmentLogger.error(`500 attachment=? viewer=? reason=unexpected message=${error instanceof Error ? error.message : \"unknown\"}`)\n return fail(500, \"Attachment request failed\")\n }\n }\n\n return {\n index: { loader: indexLoader },\n api: { loader: apiLoader, action: apiAction },\n sse: { loader: sseLoader },\n attachment: { loader: attachmentLoader },\n }\n}\n","/**\n * Data-access service for daily reports (drizzle mssql), fully DI-configured.\n * 日報データアクセスサービス (drizzle mssql)。依存はすべて DI で注入する。\n *\n * キャッシュキー・無効化順序・SSE publish 順序を含むデータアクセスロジックの中核。\n */\nimport { aliasedTable, and, asc, desc, eq, getColumns, inArray, isNull, or, sql } from \"drizzle-orm\"\nimport { normalizeBusinessDateKey } from \"../shared/business-date\"\nimport { createLogger, type DailyReportLogger, LogLevel } from \"../shared/logger\"\nimport { commentAddMessageSchema, commentDeleteMessageSchema, reportCreateMessageSchema, reportDeleteMessageSchema, reportPublishMessageSchema, reportUpdateMessageSchema, statusUpdateMessageSchema } from \"../shared/sse-schema\"\nimport type { DailyReportAttachmentSummary, DailyReportComment, DailyReportCommentItem, DailyReportDetail, DailyReportInterviewer, DailyReportItem, DailyReportLabelDef } from \"../shared/types\"\nimport type { EpochStore, SqlResultCache, SqlResultCacheQueryOptions } from \"./cache\"\nimport type { DailyReportExternalSource } from \"./external-source\"\nimport type { DailyReportEncodeUserId, DailyReportIdCodec, DailyReportRedisProvider, DailyReportResolveUserId } from \"./ports\"\nimport type { DailyReportTables, DailyReportUserTable } from \"./schema\"\n\nexport type { SqlResultCacheQueryOptions }\n\n// ---- 行型 (drizzle $inferSelect の構造的置き換え) ----\n\n/** DailyReportHub の行型。 */\nexport type DailyReportHubRow = {\n id: number\n sourceType: string\n sourceId: string\n sourceIdNum: number | null\n businessDate: Date | string | null\n userId: number | null\n title: string | null\n summary: string | null\n createdAt: Date | string\n createdBy: string\n updatedAt: Date | string\n updatedBy: string\n deletedAt: Date | string | null\n deletedBy: string | null\n}\n\n/** DailyReportInternal の行型。 */\nexport type DailyReportInternalRow = {\n hubId: number\n body: string | null\n metadata: string | null\n createdAt: Date | string\n createdBy: string\n updatedAt: Date | string\n updatedBy: string\n}\n\n/** DailyReportUserStatus の行型。 */\nexport type DailyReportUserStatusRow = {\n hubId: number\n userId: number\n isRead: boolean\n isStarred: boolean\n createdAt: Date | string\n createdBy: string\n updatedAt: Date | string\n updatedBy: string\n}\n\n/** DailyReportComment の行型。 */\nexport type DailyReportCommentRow = {\n id: number\n hubId: number\n userId: number\n body: string\n createdAt: Date | string\n createdBy: string\n updatedAt: Date | string\n updatedBy: string\n}\n\n// ---- 必要最小の drizzle ビルダ形 (公開契約はサービス関数のシグネチャで厳格化) ----\n\ntype Rows<T> = PromiseLike<T[]>\ninterface SelectChain<T> extends Rows<T> {\n from(t: unknown): SelectChain<T>\n innerJoin(t: unknown, on: unknown): SelectChain<T>\n leftJoin(t: unknown, on: unknown): SelectChain<T>\n where(cond: unknown): SelectChain<T>\n orderBy(...cols: unknown[]): SelectChain<T>\n top(n: number): SelectChain<T>\n}\ninterface InsertChain<T> {\n output(): { values(v: unknown): PromiseLike<T[]> }\n values(v: unknown): PromiseLike<unknown>\n}\ninterface UpdateChain<T> {\n set(v: unknown): {\n where(cond: unknown): PromiseLike<unknown>\n output(): { where(cond: unknown): PromiseLike<T[]> }\n }\n}\ninterface DeleteChain {\n where(cond: unknown): PromiseLike<unknown>\n}\n\n/** サービスが要求する drizzle mssql データベースの最小面 (トランザクション込み)。 */\nexport interface DailyReportDb {\n select<T = Record<string, unknown>>(fields?: unknown): SelectChain<T>\n insert<T = Record<string, unknown>>(t: unknown): InsertChain<T>\n update<T = Record<string, unknown>>(t: unknown): UpdateChain<T>\n delete(t: unknown): DeleteChain\n transaction<T>(fn: (tx: DailyReportDb) => Promise<T>, config?: unknown): Promise<T>\n}\n\n/** getColumns へ構造型テーブルを渡すための境界キャスト。 */\nconst cols = (t: unknown) => getColumns(t as Parameters<typeof getColumns>[0])\n\n// ---- サービス設定 ----\n\nexport type DailyReportServiceConfig = {\n /** drizzle mssql データベースハンドル。 */\n db: DailyReportDb\n /** 日報 7 テーブル (アプリ既存モデルまたは defineDailyReportSchema 生成物)。`attachment` のみ任意。 */\n tables: DailyReportTables\n /** 表示名解決に使う外部ユーザーテーブル ({ id, displayName })。 */\n userTable: DailyReportUserTable\n /** 外部ユーザー ID → 内部数値 ID の解決ポート。 */\n resolveUserId: DailyReportResolveUserId\n /** 内部数値 ID の難読化ポート。 */\n encodeUserId: DailyReportEncodeUserId\n /** SSE publish / epoch 用 redis (省略時は SSE publish スキップ・epoch 無効)。 */\n redis?: DailyReportRedisProvider\n /** レガシー等の外部ソースアダプタ群。 */\n externalSources?: DailyReportExternalSource[]\n /** SQL 結果キャッシュ (facade が生成して注入)。 */\n cache: SqlResultCache\n /** クロスワーカー epoch ストア (facade が生成して注入)。 */\n epochs: EpochStore\n /**\n * 下書きラベル名 (単一または配列で指定可能)。\n * 消費アプリ側で自 DB のラベル名や作成区分の候補名 (\"下書き\", \"DRAFT\" など) を注入できる。\n */\n draftLabelName?: string\n draftLabelNames?: string[]\n /** SSE Redis Stream キー (既定 \"daily-report:sse-stream\")。 */\n streamKey?: string\n /** SSE Stream の MAXLEN (既定 10000)。 */\n streamMaxLen?: number\n /** IDs 一覧キャッシュ TTL (既定 180,000ms)。 */\n idsTtlMs?: number\n /** 営業日別キャッシュ TTL (既定 300,000ms)。 */\n businessDateTtlMs?: number\n /** 外部 ID → 内部 ID のプロセス内キャッシュを無効化 (テスト用)。 */\n disableUserIdCache?: boolean\n /**\n * 添付 ID の難読化コーデック。\n *\n * 任意にしているのは、添付テーブルを注入しない消費アプリと既存テストのフィクスチャを\n * 壊さないため。未注入なら添付一覧は常に空になる (縮退動作)。\n * ユーザー ID 用のコーデックとは **別インスタンス**を渡すこと。同一だと\n * レスポンス中の `userId` トークンがそのまま添付トークンとして解釈できてしまう。\n */\n attachmentIdCodec?: DailyReportIdCodec\n /** ロガー (既定は console ベース)。 */\n logger?: DailyReportLogger\n}\n\n/** createDailyReportService の返却型。 */\nexport type DailyReportService = ReturnType<typeof createDailyReportService>\n\n/**\n * Creates the daily-report data-access service bound to the injected dependencies.\n * 注入された依存に束縛された日報データアクセスサービスを生成する処理。\n */\nexport function createDailyReportService(config: DailyReportServiceConfig) {\n const { db, tables, userTable: users, resolveUserId, encodeUserId, redis, cache: sqlResultCache, epochs } = config\n const { hub: DailyReportHub, internal: DailyReportInternal, comment: DailyReportCommentModel, label: DailyReportLabel, hubLabel: DailyReportHub_Label, userStatus: DailyReportUserStatus } = tables\n const externalSources = config.externalSources ?? []\n const draftLabelNames = config.draftLabelNames && config.draftLabelNames.length > 0 ? config.draftLabelNames : [config.draftLabelName ?? \"下書き\"]\n const draftLabelName = draftLabelNames[0] ?? \"下書き\"\n const logger = config.logger ?? createLogger(LogLevel.INFO, \"[DailyReportService]\")\n\n const DAILY_REPORT_IDS_CACHE_KEY = \"daily-report:ids\"\n const DAILY_REPORT_IDS_TTL_MS = config.idsTtlMs ?? 180_000\n const DAILY_REPORT_IDS_EPOCH_KEY = \"daily-report:ids:epoch\"\n\n const DAILY_REPORT_BUSINESS_DATE_CACHE_PREFIX = \"daily-report:business-date:\"\n const DAILY_REPORT_BUSINESS_DATE_TTL_MS = config.businessDateTtlMs ?? 300_000\n\n /** 営業日キャッシュのクロスワーカー epoch プレフィックス */\n const DAILY_REPORT_DATE_EPOCH_PREFIX = \"daily-report:date-epoch:\"\n /** レポート詳細キャッシュのクロスワーカー epoch プレフィックス */\n const DAILY_REPORT_DETAIL_EPOCH_PREFIX = \"daily-report:detail-epoch:\"\n\n const DAILY_REPORT_SSE_STREAM_KEY = config.streamKey ?? \"daily-report:sse-stream\"\n const DAILY_REPORT_SSE_STREAM_MAXLEN = config.streamMaxLen ?? 10000\n\n const incrementRedisEpoch = (key: string) => epochs.incrementEpoch(key)\n\n /**\n * Publishes a message to the SSE Redis Stream.\n * SSE 用の Redis Stream にメッセージを追加する。\n */\n const publishToSseStream = async (message: Record<string, unknown>, callerName: string): Promise<void> => {\n const client = await redis?.getClient()\n if (!client) {\n logger.warn(`[SSE] Redis client unavailable (${callerName})`)\n return\n }\n const publishStartMs = Date.now()\n try {\n await client.xAdd(DAILY_REPORT_SSE_STREAM_KEY, \"*\", { data: JSON.stringify(message) }, { TRIM: { strategy: \"MAXLEN\", strategyModifier: \"~\", threshold: DAILY_REPORT_SSE_STREAM_MAXLEN } })\n const publishDurationMs = Date.now() - publishStartMs\n if (publishDurationMs > 1000) {\n logger.warn(`[SSE] Slow publish (${callerName}): ${publishDurationMs}ms`)\n }\n } catch (e) {\n logger.error(`[SSE] Redis publish failed (${callerName}):`, e)\n }\n }\n\n type DatePattern = \"YYYY-MM-DD HH:mm:ss\" | \"YYYY-MM-DD\"\n\n /**\n * Formats date-like values to a given pattern (ja-JP locale semantics preserved).\n * 日付相当の値を指定パターンに整形する処理。\n */\n const formatDateValue = (value: Date | string | null | undefined, pattern: DatePattern): string | null => {\n if (!value) {\n return null\n }\n\n const dateValue = value instanceof Date ? value : new Date(value)\n if (Number.isNaN(dateValue.getTime())) {\n return typeof value === \"string\" ? value : null\n }\n\n // ja-JP ロケールの YYYY/MM/DD (HH:mm:ss) を生成しハイフン区切りへ正規化する\n const opts: Intl.DateTimeFormatOptions = pattern === \"YYYY-MM-DD HH:mm:ss\" ? { year: \"numeric\", month: \"2-digit\", day: \"2-digit\", hour: \"2-digit\", minute: \"2-digit\", second: \"2-digit\" } : { year: \"numeric\", month: \"2-digit\", day: \"2-digit\" }\n const formatted = pattern === \"YYYY-MM-DD HH:mm:ss\" ? dateValue.toLocaleString(\"ja-JP\", opts) : dateValue.toLocaleDateString(\"ja-JP\", opts)\n return formatted.replace(/\\//g, \"-\")\n }\n\n /**\n * Masks a raw audit-actor value (created_by / updated_by) before client exposure.\n * 監査列 (created_by / updated_by) をクライアント公開前にマスクする処理。\n *\n * Internal 由来の値は String(userId) の生内部 ID のため、数値なら encodeUserId で難読化する。\n * 非数値 (外部ソース由来の氏名やシステム文字列) はそのまま返す。null / 空文字は null。\n * userId フィールドと同じ難読化を監査列にも適用し、内部 ID の横流し漏洩を防ぐ。\n */\n const maskAuditActor = (value: string | null | undefined): string | null => {\n if (value == null || value === \"\") return null\n return /^\\d+$/.test(value) ? encodeUserId(Number(value)) : value\n }\n\n type RawJsonComment = {\n id: number\n content: string\n createdAt: string\n userId: number\n userName: string\n }\n\n type RawJsonLabel = {\n id: number\n name: string\n color: string | null\n }\n\n /** 詳細クエリ 1 行 (外部ソース列は `ext_<sourceType>` キーで同居)。 */\n type HubQueryRow = {\n hub: DailyReportHubRow\n internal: DailyReportInternalRow | null\n isRead?: boolean | null\n isStarred?: boolean | null\n creatorName?: string | null\n } & Record<string, unknown>\n\n type HubRecord = HubQueryRow & {\n labels?: RawJsonLabel[]\n comments?: RawJsonComment[]\n /**\n * 添付ファイル一覧。**任意にしない。**\n * `?` を付けると `mapHubRecord` 側で `?? []` が必要になり、注入を忘れた読み取り経路が\n * 黙って空配列を返す。必須にしておけば注入漏れは全呼び出し箇所でコンパイルエラーになる。\n */\n attachments: DailyReportAttachmentSummary[]\n }\n\n /** 外部ソースアダプタの select 追加フィールドを構築する。 */\n const externalSelections = (): Record<string, unknown> => {\n const out: Record<string, unknown> = {}\n for (const adapter of externalSources) {\n out[`ext_${adapter.sourceType}`] = cols(adapter.table)\n }\n return out\n }\n\n /** 外部ソースアダプタの LEFT JOIN を select チェーンへ適用する。 */\n const applyExternalJoins = <T>(chain: SelectChain<T>): SelectChain<T> => {\n let c = chain\n for (const adapter of externalSources) {\n c = c.leftJoin(adapter.table, and(eq(DailyReportHub.sourceType, adapter.sourceType), eq(DailyReportHub.sourceIdNum, adapter.idColumn)))\n }\n return c\n }\n\n /**\n * Converts a Hub record into a DailyReportDetail structure.\n * Hub レコードを DailyReportDetail に変換する処理。\n */\n const mapHubRecord = (row: HubRecord, currentUserId?: number): DailyReportDetail => {\n const { hub, internal } = row\n\n let content = hub.summary\n let interviewers: DailyReportInterviewer[] = []\n let category: string | null = null\n let creationCategory: string | null = null\n let visitTimeFrom: string | null = null\n let visitTimeTo: string | null = null\n let customerName: string | null = null\n let employeeName: string | null = null\n let comments: DailyReportComment[] = []\n\n // 外部ソースアダプタ優先 → Internal の順で表示フィールドを解決する\n const adapter = externalSources.find((a) => a.sourceType === hub.sourceType)\n const externalRow = adapter ? (row[`ext_${adapter.sourceType}`] as Record<string, unknown> | null | undefined) : undefined\n if (adapter && externalRow) {\n const fields = adapter.mapRow(externalRow)\n if (fields.content !== undefined) content = fields.content\n if (fields.employeeName !== undefined) employeeName = fields.employeeName\n if (fields.category !== undefined) category = fields.category\n if (fields.creationCategory !== undefined) creationCategory = fields.creationCategory\n if (fields.visitTimeFrom !== undefined) visitTimeFrom = fields.visitTimeFrom\n if (fields.visitTimeTo !== undefined) visitTimeTo = fields.visitTimeTo\n if (fields.customerName !== undefined) customerName = fields.customerName\n if (fields.interviewers !== undefined) interviewers = fields.interviewers\n if (fields.comments !== undefined) comments = fields.comments\n } else if (hub.sourceType === \"Internal\" && internal) {\n content = internal.body\n // Internal specific mappings if any\n }\n\n const labelsRaw = (row.labels ?? []).map((l) => ({\n id: l.id,\n name: l.name,\n color: l.color,\n }))\n const commentItemsRaw = row.comments ?? []\n\n // 作成区分がアプリ設定の下書き候補に合致する場合、ラベル一覧に下書きラベルが含まれるよう補完\n const isCreationCategoryDraft = creationCategory ? draftLabelNames.includes(creationCategory) : false\n // 同期関数なので DB へは問い合わせず、既に解決済みのキャッシュがある場合だけ補完する\n const cachedPrimaryDraftLabelId = cachedDraftLabelIds?.[0] ?? null\n if (isCreationCategoryDraft && cachedPrimaryDraftLabelId && !labelsRaw.some((l) => l.id === cachedPrimaryDraftLabelId)) {\n labelsRaw.push({\n id: cachedPrimaryDraftLabelId,\n name: draftLabelName,\n color: null,\n })\n }\n\n return {\n reportHubId: hub.id,\n date: formatDateValue(hub.businessDate, \"YYYY-MM-DD\"),\n author: row.creatorName ?? maskAuditActor(hub.createdBy) ?? \"\",\n userId: hub.userId ? encodeUserId(hub.userId) : \"\",\n sourceType: hub.sourceType ?? \"Internal\",\n createdAt: formatDateValue(hub.createdAt, \"YYYY-MM-DD HH:mm:ss\"),\n updatedAt: formatDateValue(hub.updatedAt, \"YYYY-MM-DD HH:mm:ss\"),\n updatedBy: maskAuditActor(hub.updatedBy),\n employeeName: employeeName ?? row.creatorName ?? maskAuditActor(hub.createdBy),\n category,\n creationCategory,\n visitTimeFrom,\n visitTimeTo,\n customerName,\n interviewers,\n subject: hub.title,\n content,\n comments,\n isRead: row.isRead ?? false,\n isStarred: row.isStarred ?? false,\n labels: labelsRaw,\n commentItems: commentItemsRaw.map((c) => ({\n ...c,\n userId: encodeUserId(c.userId),\n isMine: currentUserId ? c.userId === currentUserId : false,\n })),\n // `?? []` を書かない。HubRecord 側で必須にしてあるため、注入漏れはここではなく\n // 呼び出し箇所でコンパイルエラーになる (それが検知したい事象)。\n attachments: row.attachments,\n }\n }\n\n /**\n * Fetches daily report IDs alongside normalized business dates.\n * 日報 ID と正規化済み営業日を取得する内部処理。\n *\n * ❗ 可視性述語は他の 3 経路 (営業日一覧 / 詳細取得 / 添付取得) と**意図的に 1 つ緩い**。\n * ここだけ `isNull(DailyReportHub.userId)` を追加で許しており、投稿者を持たない\n * 取込元の日報を一覧から消さないための緩和である。返すのは ID・営業日・ソース種別だけで\n * 本文も添付も含まないため、緩めても内容は漏れない。\n * この差分は無自覚なものではなく、`service.spec.ts` が明示的に表明して固定している。\n * 他の 3 経路をこの形に合わせてはならない (合わせると内容が漏れる)。\n */\n const fetchDailyReportIdsByUserId = async (userId: number): Promise<DailyReportItem[]> => {\n const DraftLabelRelation = aliasedTable(DailyReportHub_Label as unknown as Parameters<typeof aliasedTable>[0], \"draft_label\") as unknown as typeof DailyReportHub_Label\n const draftLabelJoinIds = await getDraftLabelJoinIds()\n\n const result = await db\n .select<{ reportHubId: number; businessDate: Date | string | null; sourceType: string }>({\n reportHubId: DailyReportHub.id,\n businessDate: DailyReportHub.businessDate,\n sourceType: DailyReportHub.sourceType,\n })\n .from(DailyReportHub)\n .leftJoin(DraftLabelRelation, and(eq(DraftLabelRelation.hubId, DailyReportHub.id), inArray(DraftLabelRelation.labelId, draftLabelJoinIds)))\n .where(and(isNull(DailyReportHub.deletedAt), or(eq(DailyReportHub.userId, userId), isNull(DailyReportHub.userId), isNull(DraftLabelRelation.hubId))))\n .orderBy(desc(DailyReportHub.businessDate), desc(DailyReportHub.id))\n\n return result.map((item) => ({\n ...item,\n businessDate: formatDateValue(item.businessDate, \"YYYY-MM-DD\"),\n }))\n }\n\n /**\n * Fetches daily report details for the provided business date.\n * 指定した営業日の日報詳細を取得する内部処理。\n */\n const fetchDailyReportsByBusinessDate = async (normalizedBusinessDate: string): Promise<DailyReportDetail[]> => {\n const rows = await applyExternalJoins(\n db\n .select<HubQueryRow>({\n hub: cols(DailyReportHub),\n internal: cols(DailyReportInternal),\n creatorName: users.displayName,\n ...externalSelections(),\n })\n .from(DailyReportHub),\n )\n .leftJoin(DailyReportInternal, eq(DailyReportHub.id, DailyReportInternal.hubId))\n .leftJoin(users, eq(DailyReportHub.userId, users.id))\n .where(and(eq(DailyReportHub.businessDate, sql`${normalizedBusinessDate}`), isNull(DailyReportHub.deletedAt)))\n .orderBy(desc(DailyReportHub.id))\n\n const attachmentsMap = await getAttachmentsByHubIds(rows.map((r) => r.hub.id))\n return rows.map((row) => mapHubRecord({ ...row, attachments: attachmentsMap.get(row.hub.id) ?? [] }))\n }\n\n /**\n * Builds a deterministic cache key for business-date caches.\n * 営業日キャッシュ用の一意キーを生成する処理。\n */\n const buildBusinessDateCacheKey = (normalizedBusinessDate: string): string => {\n return `${DAILY_REPORT_BUSINESS_DATE_CACHE_PREFIX}${normalizedBusinessDate}`\n }\n\n /**\n * Resolves the internal user ID from an external ID (with process-local caching).\n * 外部 ID から内部ユーザー ID を解決する (プロセス内キャッシュ付き)。\n */\n const userIdCache = new Map<string, number>()\n\n const getUserIdByExternalId = async (externalId: string): Promise<number | null> => {\n if (!config.disableUserIdCache && userIdCache.has(externalId)) {\n return userIdCache.get(externalId) ?? null\n }\n\n const userId = await resolveUserId(externalId)\n if (userId !== null && !config.disableUserIdCache) {\n userIdCache.set(externalId, userId)\n }\n return userId\n }\n\n /**\n * Retrieves daily report IDs and their normalized business dates.\n * 日報 ID と正規化済み営業日を取得する処理。\n */\n const getDailyReportIdsByExternalId = async (externalId: string, { forceRefresh = false, snapshot = false, ttlMsOverride }: SqlResultCacheQueryOptions = {}): Promise<DailyReportItem[]> => {\n const userId = await getUserIdByExternalId(externalId)\n if (!userId) {\n return []\n }\n\n const cacheKey = `${DAILY_REPORT_IDS_CACHE_KEY}:user:${userId}`\n return sqlResultCache.getOrFetch<DailyReportItem>({\n cacheKey,\n fetcher: async () => {\n const res = await fetchDailyReportIdsByUserId(userId)\n return res\n },\n forceRefresh,\n snapshot,\n ttlMsOverride: ttlMsOverride ?? DAILY_REPORT_IDS_TTL_MS,\n epochKey: DAILY_REPORT_IDS_EPOCH_KEY,\n }) as Promise<DailyReportItem[]>\n }\n\n /**\n * Retrieves daily report details for a business date (⚠️ NOT per-user filtered).\n * 指定した営業日に紐づく日報詳細を取得する処理 (⚠️ ユーザー別フィルタなし)。\n *\n * ⚠️ SECURITY: この関数は下書きラベルによる可視性フィルタ (他ユーザーの下書きを隠す) と\n * ユーザー別の既読/スター状態を **適用しない**。取得結果は全ユーザーの下書きを含み得るため、\n * HTTP レスポンス / SSR loader / SSE へ **直接返してはならない**。ユーザー向け配信には\n * 必ず {@link getDailyReportsByBusinessDateByExternalId} を使うこと。\n * (本関数は管理・バッチ・テスト用途に限定する。)\n */\n const getDailyReportsByBusinessDate = (businessDate: string, { forceRefresh = false, snapshot = false, ttlMsOverride }: SqlResultCacheQueryOptions = {}): Promise<DailyReportDetail[]> => {\n const normalizedBusinessDate = normalizeBusinessDateKey(businessDate)\n if (!normalizedBusinessDate) {\n return Promise.resolve([])\n }\n\n return sqlResultCache.getOrFetch<DailyReportDetail>({\n cacheKey: buildBusinessDateCacheKey(normalizedBusinessDate),\n fetcher: () => fetchDailyReportsByBusinessDate(normalizedBusinessDate),\n forceRefresh,\n snapshot,\n ttlMsOverride: ttlMsOverride ?? DAILY_REPORT_BUSINESS_DATE_TTL_MS,\n }) as Promise<DailyReportDetail[]>\n }\n\n /**\n * Retrieves every label id that counts as a draft.\n * 下書きとみなすラベル ID を**すべて**取得する処理 (キャッシュ付き)。\n *\n * ❗ 可視性述語は必ずこちら (複数形) を使うこと。`draftLabelNames` には複数のラベル名を\n * 注入できる (取込元ごとに下書きの呼び名が違うため) のに、1 個の ID だけで結合すると\n * **2 つ目以降の下書きラベルが付いた日報が誰にでも可視になる**。\n * 名前が 1 つも解決できない場合は空配列を返し、呼び出し側は結合対象を\n * 「存在しない ID」にして「下書きは存在しない」と解釈する。\n *\n * @returns Draft label ids, ascending. 昇順の下書きラベル ID 群。\n */\n let cachedDraftLabelIds: number[] | null = null\n const getDraftLabelIds = async (): Promise<number[]> => {\n if (cachedDraftLabelIds !== null) return cachedDraftLabelIds\n const rows = await db.select<{ id: number }>({ id: DailyReportLabel.id }).from(DailyReportLabel).where(inArray(DailyReportLabel.name, draftLabelNames)).orderBy(DailyReportLabel.id)\n cachedDraftLabelIds = rows.map((row) => row.id)\n return cachedDraftLabelIds\n }\n\n /**\n * Join targets for the draft-label relation, safe when no draft label exists.\n * 下書きラベル関連の結合対象。下書きラベルが 1 件も無い場合でも安全に働く値。\n *\n * `inArray` に空配列を渡すと方言によっては構文エラーや常に真になりうるため、\n * 実在しない ID (`-1`) を 1 個入れて「どの行にも一致しない」ことを明示する。\n *\n * @returns Ids to join on. 結合に使う ID 群。\n */\n const getDraftLabelJoinIds = async (): Promise<number[]> => {\n const ids = await getDraftLabelIds()\n return ids.length > 0 ? ids : [-1]\n }\n\n /**\n * Retrieves the canonical draft label id used when creating a draft.\n * 下書きを作成するときに付与する代表の下書きラベル ID を取得する処理。\n *\n * 付与は 1 個に決めなければならないため `draftLabelNames` の先頭に対応する ID を返す。\n * **可視性判定には使わないこと** (`getDraftLabelIds` を使う)。\n *\n * @returns The canonical draft label id, or null when it does not exist. 代表の下書きラベル ID (存在しなければ null)。\n */\n const getDraftLabelId = async (): Promise<number | null> => {\n const ids = await getDraftLabelIds()\n if (ids.length === 0) return null\n // 先頭の名前に対応する ID を選ぶ。ID 順ではなく注入順を正とする (代表名は draftLabelName)\n const [primary] = await db.select<{ id: number }>({ id: DailyReportLabel.id }).top(1).from(DailyReportLabel).where(eq(DailyReportLabel.name, draftLabelName))\n return primary?.id ?? ids[0]\n }\n\n /**\n * Derives the tri-state verification status of an attachment's backing object.\n * 添付ファイル実体の検証状態 (三値) を導出する処理。\n *\n * 判定不能 (`unknown`) を `absent` へ丸めない。存在確認そのものが失敗した場合を\n * 偽の真偽で埋めると、一時的な権限・通信障害が「消失」として表示される。\n */\n const deriveAttachmentState = (checkedAt: Date | string | null, missingAt: Date | string | null): DailyReportAttachmentSummary[\"state\"] => {\n if (missingAt != null) return \"absent\"\n if (checkedAt != null) return \"present\"\n return \"unknown\"\n }\n\n /**\n * Fetches attachments for multiple hubs in one query, keyed by hub id.\n * 複数 hub の添付ファイルを 1 クエリで取得し hub ID をキーにまとめる処理。\n *\n * 一覧描画は N 件の日報を同時に返すため、日報ごとに問い合わせると N+1 になる。\n * ラベル・コメントと同じ一括取得の流儀に揃える。\n *\n * @param hubIds Hub ids to fetch attachments for. 添付を取得する hub ID 群。\n * @returns Attachments grouped by hub id. hub ID ごとにまとめた添付一覧。\n */\n const getAttachmentsByHubIds = async (hubIds: number[]): Promise<Map<number, DailyReportAttachmentSummary[]>> => {\n const map = new Map<number, DailyReportAttachmentSummary[]>()\n const attachment = tables.attachment\n // codec は config 直参照。createDailyReportService の分割代入は 8 キーのみで、\n // 裸の識別子として書くと TS2304 になる (任意設定は config.x で読むのがこのファイルの様式)\n const codec = config.attachmentIdCodec\n if (hubIds.length === 0 || !attachment || !codec) {\n return map\n }\n\n const rows = await db\n .select<{ id: number; hubId: number; fileName: string; fileType: string | null; fileSize: number | null; createdAt: Date | string | null; objectCheckedAt: Date | string | null; objectMissingAt: Date | string | null }>({\n id: attachment.id,\n hubId: attachment.hubId,\n fileName: attachment.fileName,\n fileType: attachment.fileType,\n fileSize: attachment.fileSize,\n createdAt: attachment.createdAt,\n objectCheckedAt: attachment.objectCheckedAt,\n objectMissingAt: attachment.objectMissingAt,\n })\n .from(attachment)\n .where(inArray(attachment.hubId, hubIds))\n .orderBy(asc(attachment.createdAt), asc(attachment.id))\n\n for (const row of rows) {\n // 内部数値 ID はここで難読化トークンへ変換する。以降クライアントへ生 ID は出ない。\n // `uploaded_by` / `created_by` / `updated_by` / `file_path` は選択もしない\n const item: DailyReportAttachmentSummary = {\n id: codec.encode(Number(row.id)),\n fileName: String(row.fileName),\n fileType: row.fileType ? String(row.fileType) : null,\n fileSize: row.fileSize != null ? Number(row.fileSize) : null,\n createdAt: formatDateValue(row.createdAt, \"YYYY-MM-DD HH:mm:ss\"),\n state: deriveAttachmentState(row.objectCheckedAt, row.objectMissingAt),\n }\n const list = map.get(Number(row.hubId))\n if (list) {\n list.push(item)\n } else {\n map.set(Number(row.hubId), [item])\n }\n }\n\n return map\n }\n\n /**\n * Fetches one attachment together with its visibility check for a given user.\n * 指定ユーザーの可視性判定込みで添付ファイル 1 件を取得する処理。\n *\n * 認可は SQL 側で完結させる。アプリ層のフィルタリングに依存しない。\n * 認可されない / 存在しない / 親日報が論理削除済みのいずれでも `null` を返すため、\n * 呼び出し側は 403 と 404 を区別せず 404 とすること (存在オラクル回避)。\n *\n * 本関数は意図的に `SqlResultCache.getOrFetch` を通さない。これは読み取りキャッシュではなく\n * リクエストごとの**認可判定**であり、結果をキャッシュするとユーザーを跨いで再利用されうる。\n *\n * @param attachmentId Internal numeric attachment id. 内部数値の添付 ID。\n * @param userId Internal numeric user id of the viewer. 閲覧者の内部数値ユーザー ID。\n * @returns Attachment row when visible, otherwise null. 可視なら添付行、それ以外は null。\n */\n const getAttachmentForUser = async (attachmentId: number, userId: number): Promise<{ fileName: string; filePath: string; fileType: string | null; fileSize: number | null } | null> => {\n const attachment = tables.attachment\n if (!attachment) {\n return null\n }\n const draftLabelJoinIds = await getDraftLabelJoinIds()\n const DraftLabelRelation = aliasedTable(DailyReportHub_Label as unknown as Parameters<typeof aliasedTable>[0], \"draft_label\") as unknown as typeof DailyReportHub_Label\n\n // `.top(1)` は `.select()` の直後でなければならない。drizzle-mssql は top() を\n // db.select() が返す MsSqlSelectBuilder にしか定義しておらず、.from() 以降のクラスには\n // 存在しない。誤った順序は型 (構造型 SelectChain が全段階に top を宣言) でも\n // 単体テスト (モックが全段階で top を返す) でも検出されず、本番で TypeError になる。\n const rows = await db\n .select<{ fileName: string; filePath: string; fileType: string | null; fileSize: number | null }>({\n fileName: attachment.fileName,\n filePath: attachment.filePath,\n fileType: attachment.fileType,\n fileSize: attachment.fileSize,\n })\n .top(1)\n .from(attachment)\n .innerJoin(DailyReportHub, eq(DailyReportHub.id, attachment.hubId))\n .leftJoin(DraftLabelRelation, and(eq(DraftLabelRelation.hubId, DailyReportHub.id), inArray(DraftLabelRelation.labelId, draftLabelJoinIds)))\n .where(and(eq(attachment.id, attachmentId), isNull(DailyReportHub.deletedAt), or(eq(DailyReportHub.userId, userId), isNull(DraftLabelRelation.hubId))))\n\n return rows[0] ?? null\n }\n\n /**\n * Records that an attachment's backing object was confirmed missing.\n * 添付ファイルの実体が消失していたことを記録する処理。\n *\n * 実配信で `not_found` を得たときに呼ぶ。次回描画で三値が `absent` になる。\n * 記録の失敗は配信結果に影響させない (呼び出し側は await しない)。\n *\n * @param attachmentId Internal numeric attachment id. 内部数値の添付 ID。\n */\n const markAttachmentMissing = async (attachmentId: number): Promise<void> => {\n const attachment = tables.attachment\n if (!attachment) return\n const now = new Date()\n try {\n await db.update(attachment).set({ objectCheckedAt: now, objectMissingAt: now, updatedAt: now }).where(eq(attachment.id, attachmentId))\n } catch (error) {\n logger.error(\"markAttachmentMissing failed\", error)\n }\n }\n\n /**\n * Records that an attachment's backing object was confirmed present.\n * 添付ファイルの実体が存在していたことを記録する処理。\n *\n * `markAttachmentMissing` と対にすること。消失記録が一方向だと、一時的な障害で\n * 一度 `absent` が立った添付は UI がリンクを描画しなくなり (押しても 404 なので当然)、\n * **利用者が再取得を発火させる手段そのものが消える**。実体が戻っても手動の\n * 一括検証を回すまで復帰しない。配信が 200 で成功した時点で解除するのが唯一の\n * 自動回復経路である。\n *\n * 記録の失敗は配信結果に影響させない (呼び出し側は await しない)。\n *\n * @param attachmentId Internal numeric attachment id. 内部数値の添付 ID。\n */\n const markAttachmentPresent = async (attachmentId: number): Promise<void> => {\n const attachment = tables.attachment\n if (!attachment) return\n const now = new Date()\n try {\n await db.update(attachment).set({ objectCheckedAt: now, objectMissingAt: null, updatedAt: now }).where(eq(attachment.id, attachmentId))\n } catch (error) {\n logger.error(\"markAttachmentPresent failed\", error)\n }\n }\n\n /**\n * Retrieves daily report details for a business date with user-specific status and relations.\n * 指定した営業日の日報詳細を、ユーザー状態と関連データを含めて取得する。\n */\n const getDailyReportsByBusinessDateByExternalId = async (businessDate: string, externalId: string, { forceRefresh = false, snapshot = false, ttlMsOverride }: SqlResultCacheQueryOptions = {}): Promise<DailyReportDetail[]> => {\n const userId = await getUserIdByExternalId(externalId)\n if (!userId) {\n return []\n }\n\n const normalizedBusinessDate = normalizeBusinessDateKey(businessDate)\n if (!normalizedBusinessDate) {\n return []\n }\n\n const cacheKey = `daily-report:date:${normalizedBusinessDate}:user:${userId}`\n\n return sqlResultCache.getOrFetch<DailyReportDetail>({\n cacheKey,\n fetcher: async () => {\n const draftLabelJoinIds = await getDraftLabelJoinIds()\n const DraftLabelRelation = aliasedTable(DailyReportHub_Label as unknown as Parameters<typeof aliasedTable>[0], \"draft_label\") as unknown as typeof DailyReportHub_Label\n\n const rows = await applyExternalJoins(\n db\n .select<HubQueryRow>({\n hub: cols(DailyReportHub),\n internal: cols(DailyReportInternal),\n isRead: DailyReportUserStatus.isRead,\n isStarred: DailyReportUserStatus.isStarred,\n creatorName: users.displayName,\n ...externalSelections(),\n })\n .from(DailyReportHub),\n )\n .leftJoin(DailyReportInternal, eq(DailyReportHub.id, DailyReportInternal.hubId))\n .leftJoin(DailyReportUserStatus, and(eq(DailyReportHub.id, DailyReportUserStatus.hubId), eq(DailyReportUserStatus.userId, userId)))\n .leftJoin(DraftLabelRelation, and(eq(DraftLabelRelation.hubId, DailyReportHub.id), inArray(DraftLabelRelation.labelId, draftLabelJoinIds)))\n .leftJoin(users, eq(DailyReportHub.userId, users.id))\n .where(and(eq(DailyReportHub.businessDate, sql`${normalizedBusinessDate}`), isNull(DailyReportHub.deletedAt), or(eq(DailyReportHub.userId, userId), isNull(DraftLabelRelation.hubId))))\n .orderBy(desc(DailyReportHub.id))\n\n if (rows.length === 0) {\n return []\n }\n\n const hubIds = rows.map((r) => r.hub.id)\n\n // Drizzle ORM batch queries to fetch labels and comments\n const allLabels = await db\n .select<{ hubId: number; id: number; name: string; color: string | null }>({\n hubId: DailyReportHub_Label.hubId,\n id: DailyReportLabel.id,\n name: DailyReportLabel.name,\n color: DailyReportLabel.color,\n })\n .from(DailyReportHub_Label)\n .innerJoin(DailyReportLabel, eq(DailyReportHub_Label.labelId, DailyReportLabel.id))\n .where(inArray(DailyReportHub_Label.hubId, hubIds))\n\n const allComments = await db\n .select<{ hubId: number; id: number; body: string; createdAt: Date | string; userId: number; userName: string | null }>({\n hubId: DailyReportCommentModel.hubId,\n id: DailyReportCommentModel.id,\n body: DailyReportCommentModel.body,\n createdAt: DailyReportCommentModel.createdAt,\n userId: DailyReportCommentModel.userId,\n userName: users.displayName,\n })\n .from(DailyReportCommentModel)\n .leftJoin(users, eq(DailyReportCommentModel.userId, users.id))\n .where(inArray(DailyReportCommentModel.hubId, hubIds))\n .orderBy(asc(DailyReportCommentModel.createdAt))\n\n // 添付も同じ流儀で一括取得する (日報ごとに引くと N+1 になる)\n const attachmentsMap = await getAttachmentsByHubIds(hubIds)\n\n const labelsMap = new Map<number, RawJsonLabel[]>()\n const commentsMap = new Map<number, RawJsonComment[]>()\n\n for (const label of allLabels) {\n let list = labelsMap.get(label.hubId)\n if (!list) {\n list = []\n labelsMap.set(label.hubId, list)\n }\n list.push({\n id: label.id,\n name: label.name,\n color: label.color,\n })\n }\n\n for (const comment of allComments) {\n let list = commentsMap.get(comment.hubId)\n if (!list) {\n list = []\n commentsMap.set(comment.hubId, list)\n }\n list.push({\n id: comment.id,\n content: comment.body,\n createdAt: formatDateValue(comment.createdAt, \"YYYY-MM-DD HH:mm:ss\") || \"\",\n userId: comment.userId,\n userName: comment.userName || \"\",\n })\n }\n\n return rows.map((row) => {\n const hubId = row.hub.id\n return mapHubRecord(\n {\n ...row,\n attachments: attachmentsMap.get(hubId) ?? [],\n labels: labelsMap.get(hubId) || [],\n comments: commentsMap.get(hubId) || [],\n },\n userId,\n )\n })\n },\n forceRefresh,\n snapshot,\n ttlMsOverride: ttlMsOverride ?? DAILY_REPORT_BUSINESS_DATE_TTL_MS,\n epochKey: `${DAILY_REPORT_DATE_EPOCH_PREFIX}${normalizedBusinessDate}`,\n }) as Promise<DailyReportDetail[]>\n }\n\n /**\n * Retrieves a single daily report detail by ID with user-specific status and relations.\n * 指定した日報詳細を、ユーザー状態と関連データを含めて取得する。\n *\n * 可視性の述語は営業日一覧 (`getDailyReportsByBusinessDateByExternalId`) および\n * `getAttachmentForUser` と同一に保つこと。3 経路のどれか 1 つだけ緩いと、\n * 緩い経路がメタデータ (件名・本文・添付のファイル名やトークン) を渡してしまい、\n * 「SQL 述語が唯一の認可境界」という不変条件が崩れる。\n * 4 番目の経路である `fetchDailyReportIdsByUserId` だけは意図的に 1 つ緩い\n * (返すのが ID・営業日・ソース種別のみのため)。詳細は同関数の docstring を参照。\n */\n const getDailyReportDetailById = async (reportHubId: number, userId: number, { forceRefresh = false, snapshot = false, ttlMsOverride }: SqlResultCacheQueryOptions = {}): Promise<DailyReportDetail | null> => {\n const cacheKey = `daily-report:detail:${reportHubId}:user:${userId}`\n\n const results = (await sqlResultCache.getOrFetch<DailyReportDetail>({\n cacheKey,\n fetcher: async () => {\n const draftLabelJoinIds = await getDraftLabelJoinIds()\n const DraftLabelRelation = aliasedTable(DailyReportHub_Label as unknown as Parameters<typeof aliasedTable>[0], \"draft_label\") as unknown as typeof DailyReportHub_Label\n\n const rows = await applyExternalJoins(\n db\n .select<HubQueryRow>({\n hub: cols(DailyReportHub),\n internal: cols(DailyReportInternal),\n isRead: DailyReportUserStatus.isRead,\n isStarred: DailyReportUserStatus.isStarred,\n creatorName: users.displayName,\n ...externalSelections(),\n })\n .from(DailyReportHub),\n )\n .leftJoin(DailyReportInternal, eq(DailyReportHub.id, DailyReportInternal.hubId))\n .leftJoin(DailyReportUserStatus, and(eq(DailyReportHub.id, DailyReportUserStatus.hubId), eq(DailyReportUserStatus.userId, userId)))\n .leftJoin(DraftLabelRelation, and(eq(DraftLabelRelation.hubId, DailyReportHub.id), inArray(DraftLabelRelation.labelId, draftLabelJoinIds)))\n .leftJoin(users, eq(DailyReportHub.userId, users.id))\n .where(and(eq(DailyReportHub.id, reportHubId), isNull(DailyReportHub.deletedAt), or(eq(DailyReportHub.userId, userId), isNull(DraftLabelRelation.hubId))))\n\n if (rows.length === 0) {\n return []\n }\n\n const hubId = rows[0].hub.id\n\n // Fetch labels and comments via Drizzle ORM\n const labels = await db\n .select<{ id: number; name: string; color: string | null }>({\n id: DailyReportLabel.id,\n name: DailyReportLabel.name,\n color: DailyReportLabel.color,\n })\n .from(DailyReportHub_Label)\n .innerJoin(DailyReportLabel, eq(DailyReportHub_Label.labelId, DailyReportLabel.id))\n .where(eq(DailyReportHub_Label.hubId, hubId))\n\n const comments = await db\n .select<{ id: number; body: string; createdAt: Date | string; userId: number; userName: string | null }>({\n id: DailyReportCommentModel.id,\n body: DailyReportCommentModel.body,\n createdAt: DailyReportCommentModel.createdAt,\n userId: DailyReportCommentModel.userId,\n userName: users.displayName,\n })\n .from(DailyReportCommentModel)\n .leftJoin(users, eq(DailyReportCommentModel.userId, users.id))\n .where(eq(DailyReportCommentModel.hubId, hubId))\n .orderBy(asc(DailyReportCommentModel.createdAt))\n\n const attachmentsMap = await getAttachmentsByHubIds([hubId])\n\n const labelsMapped = labels.map((l) => ({\n id: l.id,\n name: l.name,\n color: l.color,\n }))\n\n const commentsMapped = comments.map((c) => ({\n id: c.id,\n content: c.body,\n createdAt: formatDateValue(c.createdAt, \"YYYY-MM-DD HH:mm:ss\") || \"\",\n userId: c.userId,\n userName: c.userName || \"\",\n }))\n\n return [\n mapHubRecord(\n {\n ...rows[0],\n attachments: attachmentsMap.get(hubId) ?? [],\n labels: labelsMapped,\n comments: commentsMapped,\n },\n userId,\n ),\n ]\n },\n forceRefresh,\n snapshot,\n ttlMsOverride: ttlMsOverride ?? DAILY_REPORT_BUSINESS_DATE_TTL_MS,\n epochKey: `${DAILY_REPORT_DETAIL_EPOCH_PREFIX}${reportHubId}`,\n })) as DailyReportDetail[]\n\n return results[0] ?? null\n }\n\n /**\n * Retrieves a single daily report detail by ID with user-specific status and relations.\n * 指定した日報詳細を、ユーザー状態と関連データを含めて取得する。\n */\n const getDailyReportDetailByIdByExternalId = async (reportHubId: number, externalId: string, options: SqlResultCacheQueryOptions = {}): Promise<DailyReportDetail | null> => {\n const userId = await getUserIdByExternalId(externalId)\n if (!userId) {\n return null\n }\n return getDailyReportDetailById(reportHubId, userId, options)\n }\n\n /**\n * Sets the starred status of a daily report for a user.\n * ユーザーの日報スター状態を設定する。\n */\n const setStarStatus = async (userId: number, reportHubId: number, businessDate: string | null, isStarred: boolean, clientTempId: string): Promise<DailyReportUserStatusRow> => {\n const existing = await db\n .select<DailyReportUserStatusRow>()\n .top(1)\n .from(DailyReportUserStatus)\n .where(and(eq(DailyReportUserStatus.hubId, reportHubId), eq(DailyReportUserStatus.userId, userId)))\n\n let updatedStatus: DailyReportUserStatusRow\n\n if (existing.length > 0) {\n if (existing[0].isStarred !== isStarred) {\n const rows = await db\n .update<DailyReportUserStatusRow>(DailyReportUserStatus)\n .set({\n isStarred: isStarred,\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n .output()\n .where(and(eq(DailyReportUserStatus.hubId, reportHubId), eq(DailyReportUserStatus.userId, userId)))\n updatedStatus = rows[0]\n } else {\n updatedStatus = existing[0]\n }\n } else {\n const rows = await db\n .insert<DailyReportUserStatusRow>(DailyReportUserStatus)\n .output()\n .values({\n hubId: reportHubId,\n userId,\n isStarred: isStarred,\n isRead: false,\n createdAt: new Date(),\n createdBy: String(userId),\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n updatedStatus = rows[0]\n }\n\n if (businessDate) {\n const normalizedDate = normalizeBusinessDateKey(businessDate)\n if (normalizedDate) {\n sqlResultCache.invalidate(`daily-report:date:${normalizedDate}:user:${userId}`)\n await incrementRedisEpoch(`${DAILY_REPORT_DATE_EPOCH_PREFIX}${normalizedDate}`)\n }\n }\n sqlResultCache.invalidate(`daily-report:detail:${reportHubId}`)\n sqlResultCache.invalidate(`daily-report:detail:${reportHubId}:user:${userId}`)\n await incrementRedisEpoch(`${DAILY_REPORT_DETAIL_EPOCH_PREFIX}${reportHubId}`)\n\n await publishToSseStream(\n statusUpdateMessageSchema.parse({\n type: \"status-update\",\n reportHubId: reportHubId,\n recipientRawUserId: userId,\n statusType: \"star\",\n value: isStarred,\n clientTempId,\n }),\n \"setStarStatus\",\n )\n\n return updatedStatus\n }\n\n /**\n * Sets the read status of a daily report for a user.\n * ユーザーの日報既読状態を設定する。\n */\n const setReadStatus = async (userId: number, reportHubId: number, businessDate: string | null, isRead: boolean, clientTempId: string): Promise<DailyReportUserStatusRow> => {\n const existing = await db\n .select<DailyReportUserStatusRow>()\n .top(1)\n .from(DailyReportUserStatus)\n .where(and(eq(DailyReportUserStatus.hubId, reportHubId), eq(DailyReportUserStatus.userId, userId)))\n\n let updatedStatus: DailyReportUserStatusRow\n\n if (existing.length > 0) {\n if (existing[0].isRead !== isRead) {\n const rows = await db\n .update<DailyReportUserStatusRow>(DailyReportUserStatus)\n .set({\n isRead: isRead,\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n .output()\n .where(and(eq(DailyReportUserStatus.hubId, reportHubId), eq(DailyReportUserStatus.userId, userId)))\n updatedStatus = rows[0]\n } else {\n updatedStatus = existing[0]\n }\n } else {\n const rows = await db\n .insert<DailyReportUserStatusRow>(DailyReportUserStatus)\n .output()\n .values({\n hubId: reportHubId,\n userId,\n isRead: isRead,\n isStarred: false,\n createdAt: new Date(),\n createdBy: String(userId),\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n updatedStatus = rows[0]\n }\n\n if (businessDate) {\n const normalizedDate = normalizeBusinessDateKey(businessDate)\n if (normalizedDate) {\n sqlResultCache.invalidate(`daily-report:date:${normalizedDate}:user:${userId}`)\n await incrementRedisEpoch(`${DAILY_REPORT_DATE_EPOCH_PREFIX}${normalizedDate}`)\n }\n }\n sqlResultCache.invalidate(`daily-report:detail:${reportHubId}`)\n sqlResultCache.invalidate(`daily-report:detail:${reportHubId}:user:${userId}`)\n await incrementRedisEpoch(`${DAILY_REPORT_DETAIL_EPOCH_PREFIX}${reportHubId}`)\n\n await publishToSseStream(\n statusUpdateMessageSchema.parse({\n type: \"status-update\",\n reportHubId: reportHubId,\n recipientRawUserId: userId,\n statusType: \"read\",\n value: isRead,\n clientTempId,\n }),\n \"setReadStatus\",\n )\n\n return updatedStatus\n }\n\n /**\n * Adds a comment to a daily report.\n * 日報にコメントを追加する。\n */\n const addComment = async (userId: number, reportHubId: number, content: string, businessDate: string | null, clientTempId: string): Promise<DailyReportCommentItem> => {\n const [hub] = await db.select<{ sourceType: string }>({ sourceType: DailyReportHub.sourceType }).top(1).from(DailyReportHub).where(eq(DailyReportHub.id, reportHubId))\n if (hub && hub.sourceType.toLowerCase() !== \"internal\") {\n throw new Error(\"Comments are restricted for external daily report sources\")\n }\n\n const [inserted] = await db\n .insert<DailyReportCommentRow>(DailyReportCommentModel)\n .output()\n .values({\n hubId: reportHubId,\n userId,\n body: content,\n createdAt: new Date(),\n createdBy: String(userId),\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n\n const [user] = await db.select<{ displayName: string | null }>({ displayName: users.displayName }).top(1).from(users).where(eq(users.id, userId))\n const userName = user?.displayName ?? \"Unknown\"\n\n // コメントは全ユーザーに表示されるため、date キャッシュも全ユーザー分を無効化\n if (businessDate) {\n const normalizedDate = normalizeBusinessDateKey(businessDate)\n if (normalizedDate) {\n sqlResultCache.invalidatePrefix(`daily-report:date:${normalizedDate}:user:`)\n await incrementRedisEpoch(`${DAILY_REPORT_DATE_EPOCH_PREFIX}${normalizedDate}`)\n }\n }\n sqlResultCache.invalidate(`daily-report:detail:${reportHubId}`)\n sqlResultCache.invalidatePrefix(`daily-report:detail:${reportHubId}:user:`)\n await incrementRedisEpoch(`${DAILY_REPORT_DETAIL_EPOCH_PREFIX}${reportHubId}`)\n\n const commentItem: DailyReportCommentItem = {\n id: inserted.id,\n userId: encodeUserId(inserted.userId),\n userName: userName,\n content: inserted.body,\n createdAt: formatDateValue(inserted.createdAt, \"YYYY-MM-DD HH:mm:ss\") ?? \"\",\n isMine: true,\n }\n\n await publishToSseStream(\n commentAddMessageSchema.parse({\n type: \"comment-add\",\n reportHubId: reportHubId,\n comment: commentItem,\n clientTempId,\n }),\n \"addComment\",\n )\n\n return commentItem\n }\n\n /**\n * Finds a daily report comment by ID.\n * 日報コメントをIDで検索する。\n */\n const findDailyReportCommentById = async (tx: DailyReportDb, commentId: number) => {\n return await tx.select<DailyReportCommentRow>().top(1).from(DailyReportCommentModel).where(eq(DailyReportCommentModel.id, commentId))\n }\n\n /**\n * Deletes a comment from a daily report.\n * 日報のコメントを削除する。\n */\n const deleteComment = async (userId: number, reportHubId: number, commentId: number, businessDate: string | null, clientTempId: string): Promise<void> => {\n const comments = await findDailyReportCommentById(db, commentId)\n if (comments.length === 0) {\n throw new Error(\"Not Found\")\n }\n if (comments[0].userId !== userId) {\n throw new Error(\"Unauthorized\")\n }\n\n await db.delete(DailyReportCommentModel).where(eq(DailyReportCommentModel.id, commentId))\n\n // コメントは全ユーザーに表示されるため、date キャッシュも全ユーザー分を無効化\n if (businessDate) {\n const normalizedDate = normalizeBusinessDateKey(businessDate)\n if (normalizedDate) {\n sqlResultCache.invalidatePrefix(`daily-report:date:${normalizedDate}:user:`)\n await incrementRedisEpoch(`${DAILY_REPORT_DATE_EPOCH_PREFIX}${normalizedDate}`)\n }\n }\n sqlResultCache.invalidate(`daily-report:detail:${reportHubId}`)\n sqlResultCache.invalidatePrefix(`daily-report:detail:${reportHubId}:user:`)\n await incrementRedisEpoch(`${DAILY_REPORT_DETAIL_EPOCH_PREFIX}${reportHubId}`)\n\n await publishToSseStream(\n commentDeleteMessageSchema.parse({\n type: \"comment-delete\",\n reportHubId: reportHubId,\n commentId,\n clientTempId,\n }),\n \"deleteComment\",\n )\n }\n\n /**\n * Creates a new draft daily report.\n * 新しい日報(下書き)を作成する。\n */\n const createDailyReport = async (userId: number, businessDate: string, clientTempId: string): Promise<DailyReportDetail> => {\n logger.info(\"createDailyReport called\", { userId, businessDate })\n\n // ユーザー名取得\n const [user] = await db.select<{ displayName: string | null }>({ displayName: users.displayName }).from(users).where(eq(users.id, userId))\n const userName = user?.displayName ?? encodeUserId(userId)\n\n try {\n const result = await db.transaction(async (tx) => {\n logger.info(\"Starting transaction\")\n // 1. Hub作成\n const sourceId = `internal-temp-${Date.now()}-${Math.random()}` // 一時的なID\n const [hub] = await tx\n .insert<DailyReportHubRow>(DailyReportHub)\n .output()\n .values({\n sourceType: \"Internal\",\n sourceId: sourceId,\n businessDate: new Date(businessDate),\n userId: userId,\n title: \"(無題)\",\n createdAt: new Date(),\n createdBy: String(userId),\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n\n // sourceId を id と同じ値に更新 (Internal の場合の正規化)\n await tx\n .update(DailyReportHub)\n .set({ sourceId: String(hub.id) })\n .where(eq(DailyReportHub.id, hub.id))\n\n logger.info(\"Hub created\", hub)\n\n // 2. Internal作成\n await tx.insert(DailyReportInternal).values({\n hubId: hub.id,\n body: \"\",\n createdAt: new Date(),\n createdBy: String(userId),\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n logger.info(\"Internal created\")\n\n // 3. ラベル付与\n logger.info(\"Calling getDraftLabelId\")\n const draftLabelId = await getDraftLabelId()\n logger.info(\"draftLabelId\", draftLabelId)\n const labels: DailyReportLabelDef[] = []\n if (draftLabelId) {\n await tx.insert(DailyReportHub_Label).values({\n hubId: hub.id,\n labelId: draftLabelId,\n createdAt: new Date(),\n createdBy: String(userId),\n })\n labels.push({ id: draftLabelId, name: draftLabelName, color: null })\n }\n logger.info(\"Label assigned\")\n\n // 4. 詳細オブジェクト構築\n return {\n reportHubId: hub.id,\n date: businessDate,\n createdAt: formatDateValue(hub.createdAt, \"YYYY-MM-DD HH:mm:ss\"),\n author: userName,\n userId: encodeUserId(userId),\n sourceType: \"Internal\",\n employeeName: userName,\n // 監査列と同様、クライアント公開時は生内部 ID を難読化する (mapHubRecord と整合)\n updatedBy: encodeUserId(userId),\n updatedAt: formatDateValue(hub.updatedAt, \"YYYY-MM-DD HH:mm:ss\"),\n category: null,\n creationCategory: null,\n visitTimeFrom: null,\n visitTimeTo: null,\n customerName: null,\n interviewers: [],\n subject: hub.title,\n content: \"\",\n comments: [],\n isRead: true, // 自分で作ったので既読\n isStarred: false,\n labels: labels,\n commentItems: [],\n // 新規作成直後の日報に添付は存在しない\n attachments: [],\n }\n })\n\n // キャッシュ無効化\n const normalizedBusinessDate = normalizeBusinessDateKey(businessDate)\n if (normalizedBusinessDate) {\n sqlResultCache.invalidate(`daily-report:date:${normalizedBusinessDate}:user:${userId}`)\n }\n sqlResultCache.invalidatePrefix(DAILY_REPORT_BUSINESS_DATE_CACHE_PREFIX)\n sqlResultCache.invalidate(`${DAILY_REPORT_IDS_CACHE_KEY}:user:${userId}`)\n // ❗ 順序重要: invalidate → incrementRedisEpoch → publishToSseStream\n await incrementRedisEpoch(DAILY_REPORT_IDS_EPOCH_KEY)\n if (normalizedBusinessDate) {\n await incrementRedisEpoch(`${DAILY_REPORT_DATE_EPOCH_PREFIX}${normalizedBusinessDate}`)\n }\n\n const fullDetail = await getDailyReportDetailById(result.reportHubId, userId, { forceRefresh: true })\n\n if (fullDetail) {\n // 下書き判定\n // 判定は代表 ID ではなく下書きラベル**全体**で行う (2 つ目の下書き名を見落とさない)\n const draftLabelIds = await getDraftLabelIds()\n const isDraft = fullDetail.labels.some((l) => draftLabelIds.includes(l.id))\n await publishToSseStream(\n reportCreateMessageSchema.parse({\n type: \"report-create\",\n reportHubId: fullDetail.reportHubId,\n report: fullDetail,\n clientTempId,\n recipientRawUserId: isDraft ? userId : undefined,\n }),\n \"createDailyReport\",\n )\n }\n\n return fullDetail ?? result\n } catch (e) {\n logger.error(\"Error in createDailyReport\", e)\n throw e\n }\n }\n\n // --- Internal Repository Layer ---\n\n /**\n * Finds a DailyReportHub by ID.\n * 日報HubをIDで検索する。\n */\n const findDailyReportHubById = async (tx: DailyReportDb, reportHubId: number) => {\n return await tx.select<DailyReportHubRow>().top(1).from(DailyReportHub).where(eq(DailyReportHub.id, reportHubId))\n }\n\n /**\n * Updates a DailyReportHub.\n * 日報Hubを更新する。\n */\n const updateDailyReportHub = async (tx: DailyReportDb, reportHubId: number, data: Partial<DailyReportHubRow>) => {\n await tx.update(DailyReportHub).set(data).where(eq(DailyReportHub.id, reportHubId))\n }\n\n /**\n * Updates a DailyReportInternal.\n * 日報Internalを更新する。\n */\n const updateDailyReportInternal = async (tx: DailyReportDb, hubId: number, data: Partial<DailyReportInternalRow>) => {\n await tx.update(DailyReportInternal).set(data).where(eq(DailyReportInternal.hubId, hubId))\n }\n\n /**\n * Deletes a label from a DailyReportHub.\n * 日報Hubからラベルを削除する。\n */\n const deleteDailyReportLabel = async (tx: DailyReportDb, hubId: number, labelId: number) => {\n await tx.delete(DailyReportHub_Label).where(and(eq(DailyReportHub_Label.hubId, hubId), eq(DailyReportHub_Label.labelId, labelId)))\n }\n\n // --- Service Layer ---\n\n /**\n * Logically deletes a daily report.\n * 日報を論理削除する。\n */\n const deleteDailyReport = async (reportHubId: number, userId: number, clientTempId: string): Promise<void> => {\n const report = await findDailyReportHubById(db, reportHubId)\n if (!report.length || report[0].deletedAt) {\n throw new Error(\"Not Found\")\n }\n if (report[0].userId !== userId) {\n throw new Error(\"Unauthorized\")\n }\n\n await updateDailyReportHub(db, reportHubId, {\n deletedAt: new Date(),\n deletedBy: String(userId),\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n\n // キャッシュ無効化\n sqlResultCache.invalidatePrefix(`daily-report:detail:${reportHubId}:user:`)\n sqlResultCache.invalidatePrefix(DAILY_REPORT_BUSINESS_DATE_CACHE_PREFIX)\n // 削除されたレポートは全ユーザーの一覧から消えるため、全ユーザーの IDs キャッシュを invalidate\n sqlResultCache.invalidatePrefix(DAILY_REPORT_IDS_CACHE_KEY)\n // ❗ 順序重要: invalidatePrefix → incrementRedisEpoch → publishToSseStream\n await incrementRedisEpoch(DAILY_REPORT_IDS_EPOCH_KEY)\n if (report[0].businessDate) {\n const normalizedDate = normalizeBusinessDateKey(formatDateValue(report[0].businessDate, \"YYYY-MM-DD\"))\n if (normalizedDate) {\n await incrementRedisEpoch(`${DAILY_REPORT_DATE_EPOCH_PREFIX}${normalizedDate}`)\n }\n }\n\n await publishToSseStream(\n reportDeleteMessageSchema.parse({\n type: \"report-delete\",\n reportHubId: reportHubId,\n clientTempId,\n }),\n \"deleteDailyReport\",\n )\n }\n\n /**\n * Updates an existing daily report.\n * 日報を更新する。\n */\n const updateDailyReport = async (reportHubId: number, userId: number, data: { title?: string; content?: string }, clientTempId: string): Promise<void> => {\n const report = await findDailyReportHubById(db, reportHubId)\n if (!report.length || report[0].deletedAt) {\n throw new Error(\"Not Found\")\n }\n if (report[0].userId !== userId) {\n throw new Error(\"Unauthorized\")\n }\n\n await db.transaction(async (tx) => {\n if (data.title !== undefined) {\n await updateDailyReportHub(tx, reportHubId, {\n title: data.title,\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n }\n if (data.content !== undefined) {\n await updateDailyReportInternal(tx, reportHubId, {\n body: data.content,\n updatedAt: new Date(),\n updatedBy: String(userId),\n })\n }\n })\n\n // キャッシュ無効化\n sqlResultCache.invalidatePrefix(`daily-report:detail:${reportHubId}:user:`)\n sqlResultCache.invalidatePrefix(DAILY_REPORT_BUSINESS_DATE_CACHE_PREFIX)\n\n const fullDetail = await getDailyReportDetailById(reportHubId, userId, { forceRefresh: true })\n if (fullDetail) {\n // 下書き判定\n // 判定は代表 ID ではなく下書きラベル**全体**で行う (2 つ目の下書き名を見落とさない)\n const draftLabelIds = await getDraftLabelIds()\n const isDraft = fullDetail.labels.some((l) => draftLabelIds.includes(l.id))\n await publishToSseStream(\n reportUpdateMessageSchema.parse({\n type: \"report-update\",\n reportHubId: fullDetail.reportHubId,\n report: fullDetail,\n clientTempId,\n recipientRawUserId: isDraft ? userId : undefined,\n }),\n \"updateDailyReport\",\n )\n }\n }\n\n /**\n * Publishes a draft daily report.\n * 日報を公開する(下書きラベルを削除)。\n */\n const publishDailyReport = async (reportHubId: number, userId: number, clientTempId: string): Promise<DailyReportDetail | null> => {\n const report = await findDailyReportHubById(db, reportHubId)\n if (!report.length) {\n throw new Error(\"Not Found\")\n }\n if (report[0].userId !== userId) {\n throw new Error(\"Unauthorized\")\n }\n\n const draftLabelId = await getDraftLabelId()\n if (!draftLabelId) return null\n\n await deleteDailyReportLabel(db, reportHubId, draftLabelId)\n\n // キャッシュ無効化\n sqlResultCache.invalidatePrefix(`daily-report:detail:${reportHubId}:user:`)\n sqlResultCache.invalidatePrefix(DAILY_REPORT_BUSINESS_DATE_CACHE_PREFIX)\n // 公開によりドラフトが全ユーザーに可視化されるため、全ユーザーの IDs キャッシュを invalidate\n sqlResultCache.invalidatePrefix(DAILY_REPORT_IDS_CACHE_KEY)\n // ❗ 順序重要: invalidatePrefix → incrementRedisEpoch → publishToSseStream\n await incrementRedisEpoch(DAILY_REPORT_IDS_EPOCH_KEY)\n if (report[0].businessDate) {\n const normalizedDate = normalizeBusinessDateKey(formatDateValue(report[0].businessDate, \"YYYY-MM-DD\"))\n if (normalizedDate) {\n await incrementRedisEpoch(`${DAILY_REPORT_DATE_EPOCH_PREFIX}${normalizedDate}`)\n }\n }\n\n // ユーザーごとの営業日別一覧キャッシュも無効化\n const normalizedBusinessDate = normalizeBusinessDateKey(formatDateValue(report[0].businessDate, \"YYYY-MM-DD\"))\n if (normalizedBusinessDate) {\n sqlResultCache.invalidate(`daily-report:date:${normalizedBusinessDate}:user:${userId}`)\n }\n\n const fullDetail = await getDailyReportDetailById(reportHubId, userId, { forceRefresh: true })\n if (fullDetail) {\n await publishToSseStream(\n reportPublishMessageSchema.parse({\n type: \"report-publish\",\n reportHubId: fullDetail.reportHubId,\n report: fullDetail,\n clientTempId,\n }),\n \"publishDailyReport\",\n )\n }\n return fullDetail ?? null\n }\n\n /**\n * Invalidates the read caches that carry hub-scoped data across every process.\n * hub 単位のデータを載せている読み取りキャッシュを、全プロセス横断で無効化する処理。\n *\n * 用途: 通常の更新経路 (create / update / publish) を通らずに DB を書き換えた場合、\n * 例えば外部システムからの一括取込。プロセス内 Map を消すだけでは稼働中の\n * 別プロセス (別ワーカー・別コンテナ) のキャッシュが TTL まで stale のままになるため、\n * Redis の epoch を進めて全プロセスのキャッシュ行を一斉に無効化する。\n *\n * `clearCache()` との違い: `clearCache()` が進める epoch は一覧 ID のものだけで、\n * 詳細キャッシュ (`detail-epoch:<hubId>`) と営業日キャッシュ (`date-epoch:<date>`) は\n * 進まない。日報本体に紐づくデータ (添付など) を書き換えたときはこちらを使うこと。\n *\n * @param hubIds Hub ids whose detail cache must be dropped. 詳細キャッシュを捨てる hub ID 群。\n * @param businessDates Business dates (YYYY-MM-DD) whose list cache must be dropped. 一覧キャッシュを捨てる営業日群。\n * @returns Nothing. なし。\n */\n const invalidateHubReadCaches = async (hubIds: readonly number[], businessDates: readonly string[]): Promise<void> => {\n sqlResultCache.invalidatePrefix(DAILY_REPORT_BUSINESS_DATE_CACHE_PREFIX)\n for (const hubId of new Set(hubIds)) {\n sqlResultCache.invalidatePrefix(`daily-report:detail:${hubId}:user:`)\n await incrementRedisEpoch(`${DAILY_REPORT_DETAIL_EPOCH_PREFIX}${hubId}`)\n }\n for (const businessDate of new Set(businessDates)) {\n const normalized = normalizeBusinessDateKey(businessDate)\n // 正規化できない値で epoch キーを作ると、誰も参照しないキーが増えるだけになる\n if (!normalized) continue\n await incrementRedisEpoch(`${DAILY_REPORT_DATE_EPOCH_PREFIX}${normalized}`)\n }\n }\n\n /**\n * Clears all server-side SQL result and user ID caches.\n * サーバー側のすべての SQL 結果キャッシュおよびユーザー ID キャッシュを全消去する。\n *\n * Redis で進めるのは一覧 ID の epoch のみ。詳細/営業日キャッシュも捨てたい場合は\n * `invalidateHubReadCaches` を使うこと (このメソッドでは進まない)。\n */\n const clearCache = async (): Promise<void> => {\n sqlResultCache.clearAll()\n userIdCache.clear()\n cachedDraftLabelIds = null\n if (epochs) {\n await incrementRedisEpoch(DAILY_REPORT_IDS_EPOCH_KEY)\n }\n logger.info(\"[DailyReportService] Server-side DB/SQL caches cleared successfully.\")\n }\n\n return {\n // 定数 (アプリ側の互換 export 用)\n streamKey: DAILY_REPORT_SSE_STREAM_KEY,\n streamMaxLen: DAILY_REPORT_SSE_STREAM_MAXLEN,\n // キャッシュクリア\n clearCache,\n invalidateHubReadCaches,\n // ユーザー解決\n getUserIdByExternalId,\n // 参照系\n getDailyReportIdsByExternalId,\n getDailyReportsByBusinessDate,\n getDailyReportsByBusinessDateByExternalId,\n getDailyReportDetailById,\n getDailyReportDetailByIdByExternalId,\n getDraftLabelId,\n // 添付 (ハンドラーが認可判定と実体消失記録に使う)\n getAttachmentsByHubIds,\n getAttachmentForUser,\n markAttachmentMissing,\n markAttachmentPresent,\n // 更新系\n setStarStatus,\n setReadStatus,\n addComment,\n deleteComment,\n createDailyReport,\n updateDailyReport,\n publishDailyReport,\n deleteDailyReport,\n // リポジトリヘルパー (統合テスト等からの直接利用向け)\n findDailyReportHubById,\n updateDailyReportHub,\n updateDailyReportInternal,\n deleteDailyReportLabel,\n }\n}\n","/**\n * Authz helper utilities for @aiquants/daily-report: standard resource definitions & auto-seeding.\n * @aiquants/daily-report 用の認可ヘルパー。標準リソース定義および自動シード機能を提供。\n */\nimport { and, eq } from \"drizzle-orm\"\n\nexport type AuthzResourceItem = {\n resourceKey: string\n name: string\n description?: string | null\n}\n\nexport type DailyReportSourceTypeInput = {\n key: string\n name: string\n description?: string | null\n includeCommentResource?: boolean\n}\n\n/**\n * Dynamically builds neutral Authz resource definitions from provided source type inputs.\n * 指定されたソース種別定義から中立な認可リソース定義リストを生成するファクトリ。\n */\nexport function defineDailyReportAuthzResources(sources?: DailyReportSourceTypeInput[]): AuthzResourceItem[] {\n const list: AuthzResourceItem[] = []\n const targetSources =\n sources && sources.length > 0\n ? sources\n : [\n { key: \"internal\", name: \"Internal\", description: \"Internal daily reports\" },\n { key: \"external\", name: \"External\", description: \"External daily reports\" },\n ]\n\n for (const src of targetSources) {\n const keyLower = src.key.toLowerCase().replace(/[^a-z0-9_]/g, \"_\")\n const resourceKey = `daily_report_${keyLower}`\n list.push({\n resourceKey,\n name: src.name,\n description: src.description ?? `${src.name} daily report access`,\n })\n\n if (src.includeCommentResource !== false) {\n list.push({\n resourceKey: `${resourceKey}_comment`,\n name: `${src.name} Comment`,\n description: `${src.name} daily report comment access`,\n })\n }\n }\n\n return list\n}\n\ntype DbSelectChain = {\n from(t: unknown): DbSelectChain\n where(cond: unknown): PromiseLike<unknown[]>\n}\ntype DbInsertChain = {\n values(v: unknown): PromiseLike<unknown>\n}\ntype LooseDb = {\n select(fields?: unknown): DbSelectChain\n insert(t: unknown): DbInsertChain\n}\n\n/**\n * Ensures all daily-report standard resources exist in the authz TMResource table idempotently.\n * authz データベース内に日報機能の標準認可リソースが存在することを自動保証(冪等シード)する処理。\n */\nexport async function seedDailyReportAuthzResources(db: unknown, authzTables: { TMResource: unknown }, opts: { appKey: string; actor?: string; sources?: DailyReportSourceTypeInput[]; resources?: AuthzResourceItem[] }): Promise<void> {\n const d = db as LooseDb\n const TMResource = authzTables.TMResource as Record<string, unknown>\n const appKey = opts.appKey\n const actor = opts.actor ?? \"system:daily-report\"\n const now = new Date()\n\n const resourceList = opts.resources ?? defineDailyReportAuthzResources(opts.sources)\n\n for (const res of resourceList) {\n const existing = await d\n .select()\n .from(TMResource)\n .where(and(eq(TMResource.appKey as never, appKey as never), eq(TMResource.resourceKey as never, res.resourceKey as never)))\n\n if (!existing || existing.length === 0) {\n await d.insert(TMResource).values({\n appKey,\n resourceKey: res.resourceKey,\n name: res.name,\n description: res.description ?? null,\n createdAt: now,\n createdBy: actor,\n updatedAt: now,\n updatedBy: actor,\n })\n }\n }\n}\n","/**\n * External source adapter contract for legacy/foreign daily-report tables.\n * レガシー・外部由来の日報テーブルを取り込む外部ソースアダプタ契約。\n *\n * DailyReportHub.source_type がアダプタの `sourceType` に一致する行は、\n * `table` を `Hub.source_id_num = idColumn` で LEFT JOIN し、その行を\n * `mapRow` で表示フィールドへ変換する (例: 別システムのレガシー日報テーブルの取り込み)。\n */\nimport type { AnyMsSqlColumn } from \"drizzle-orm/mssql-core\"\nimport { createLogger, type DailyReportLogger, LogLevel } from \"../shared/logger\"\nimport type { DailyReportComment, DailyReportInterviewer } from \"../shared/types\"\n\n/** 外部ソース行から詳細表示へ供給するフィールド群。 */\nexport type ExternalReportFields = {\n content?: string | null\n employeeName?: string | null\n category?: string | null\n creationCategory?: string | null\n visitTimeFrom?: string | null\n visitTimeTo?: string | null\n customerName?: string | null\n interviewers?: DailyReportInterviewer[]\n comments?: DailyReportComment[]\n}\n\n/** 外部ソースアダプタ。 */\nexport type DailyReportExternalSource = {\n /** DailyReportHub.source_type の一致値 (例 \"legacy\")。 */\n sourceType: string\n /** LEFT JOIN する drizzle テーブル。 */\n table: unknown\n /** Hub.source_id_num と突き合わせる ID 列。 */\n idColumn: AnyMsSqlColumn\n /** 結合行を表示フィールドへ変換する処理。 */\n mapRow: (row: Record<string, unknown>) => ExternalReportFields\n}\n\nconst defaultLogger = createLogger(LogLevel.INFO, \"[DailyReportExternalSource]\")\n\n/**\n * Parses JSON array payloads and maps each element (fail-soft: returns [] on error).\n * JSON 配列のペイロードを解析し各要素を変換する処理 (エラー時は空配列)。\n */\nexport const transformJsonArray = <TRaw, TResult>(payload: string | null, label: string, mapper: (raw: TRaw) => TResult | null, logger: DailyReportLogger = defaultLogger): TResult[] => {\n if (!payload) {\n return []\n }\n\n try {\n const parsed = JSON.parse(payload)\n if (!Array.isArray(parsed)) {\n logger.warn(`Unexpected ${label} format: not an array`)\n return []\n }\n return (parsed as TRaw[]).map(mapper).filter((entry): entry is TResult => entry !== null)\n } catch (error) {\n logger.warn(`Failed to parse ${label}:`, error)\n return []\n }\n}\n","/**\n * Structural table types + drizzle (mssql) schema factory for the daily-report tables.\n * 日報テーブル群の構造的テーブル型と drizzle (mssql) スキーマファクトリ。\n *\n * 既存アプリは自前のモデル定義をそのまま `DailyReportTables` として注入できる (構造互換)。\n * 新規プロジェクトは `defineDailyReportSchema()` で同一構造のテーブル定義を生成できる。\n */\nimport { desc, sql } from \"drizzle-orm\"\nimport { type AnyMsSqlColumn, bigint, bit, date, datetime2, foreignKey, index, int, mssqlSchema, nvarchar, primaryKey, uniqueIndex } from \"drizzle-orm/mssql-core\"\n\n/** 注入する外部ユーザーテーブルの最小形 (id / display_name)。 */\nexport type DailyReportUserTable = { id: AnyMsSqlColumn; displayName: AnyMsSqlColumn }\n\n/** DailyReportHub テーブルの構造形。 */\nexport type DailyReportHubTable = {\n id: AnyMsSqlColumn\n sourceType: AnyMsSqlColumn\n sourceId: AnyMsSqlColumn\n sourceIdNum: AnyMsSqlColumn\n businessDate: AnyMsSqlColumn\n userId: AnyMsSqlColumn\n title: AnyMsSqlColumn\n summary: AnyMsSqlColumn\n createdAt: AnyMsSqlColumn\n createdBy: AnyMsSqlColumn\n updatedAt: AnyMsSqlColumn\n updatedBy: AnyMsSqlColumn\n deletedAt: AnyMsSqlColumn\n deletedBy: AnyMsSqlColumn\n}\n\n/** DailyReportInternal テーブルの構造形。 */\nexport type DailyReportInternalTable = {\n hubId: AnyMsSqlColumn\n body: AnyMsSqlColumn\n metadata: AnyMsSqlColumn\n createdAt: AnyMsSqlColumn\n createdBy: AnyMsSqlColumn\n updatedAt: AnyMsSqlColumn\n updatedBy: AnyMsSqlColumn\n}\n\n/** DailyReportComment テーブルの構造形。 */\nexport type DailyReportCommentTable = {\n id: AnyMsSqlColumn\n hubId: AnyMsSqlColumn\n userId: AnyMsSqlColumn\n body: AnyMsSqlColumn\n createdAt: AnyMsSqlColumn\n createdBy: AnyMsSqlColumn\n updatedAt: AnyMsSqlColumn\n updatedBy: AnyMsSqlColumn\n}\n\n/** DailyReportLabel テーブルの構造形。 */\nexport type DailyReportLabelTable = {\n id: AnyMsSqlColumn\n userId: AnyMsSqlColumn\n name: AnyMsSqlColumn\n color: AnyMsSqlColumn\n sortOrder: AnyMsSqlColumn\n createdAt: AnyMsSqlColumn\n createdBy: AnyMsSqlColumn\n updatedAt: AnyMsSqlColumn\n updatedBy: AnyMsSqlColumn\n}\n\n/** DailyReportHub_Label 中間テーブルの構造形。 */\nexport type DailyReportHubLabelTable = {\n hubId: AnyMsSqlColumn\n labelId: AnyMsSqlColumn\n createdAt: AnyMsSqlColumn\n createdBy: AnyMsSqlColumn\n}\n\n/** DailyReportUserStatus テーブルの構造形。 */\nexport type DailyReportUserStatusTable = {\n hubId: AnyMsSqlColumn\n userId: AnyMsSqlColumn\n isRead: AnyMsSqlColumn\n isStarred: AnyMsSqlColumn\n createdAt: AnyMsSqlColumn\n createdBy: AnyMsSqlColumn\n updatedAt: AnyMsSqlColumn\n updatedBy: AnyMsSqlColumn\n}\n\n/**\n * DailyReportAttachment テーブルの構造形。\n *\n * `uploadedBy` は外部取込ソース由来の行に投稿者が存在しないため NULL 許容。\n * `sourceKey` は取込の冪等 upsert の自然キー。\n * `objectCheckedAt` / `objectMissingAt` から実体存在の三値 (unknown / present / absent) を導出する。\n */\nexport type DailyReportAttachmentTable = {\n id: AnyMsSqlColumn\n hubId: AnyMsSqlColumn\n uploadedBy: AnyMsSqlColumn\n sourceKey: AnyMsSqlColumn\n fileName: AnyMsSqlColumn\n filePath: AnyMsSqlColumn\n fileType: AnyMsSqlColumn\n fileSize: AnyMsSqlColumn\n objectCheckedAt: AnyMsSqlColumn\n objectMissingAt: AnyMsSqlColumn\n createdAt: AnyMsSqlColumn\n createdBy: AnyMsSqlColumn\n updatedAt: AnyMsSqlColumn\n updatedBy: AnyMsSqlColumn\n}\n\n/**\n * サービスへ注入するテーブル一式 (アプリ既存モデルまたは本ファクトリ生成物)。\n *\n * `attachment` のみ任意。添付テーブルを持たない消費アプリでも本パッケージを使えるようにするため。\n * 未注入時は詳細の `attachments` が常に空配列になり、添付エンドポイントは全トークンで 404 を返す\n * (意図した縮退動作)。\n */\nexport type DailyReportTables = {\n hub: DailyReportHubTable\n internal: DailyReportInternalTable\n comment: DailyReportCommentTable\n attachment?: DailyReportAttachmentTable\n label: DailyReportLabelTable\n hubLabel: DailyReportHubLabelTable\n userStatus: DailyReportUserStatusTable\n}\n\n/**\n * Generic factory: build the seven daily-report tables under an arbitrary schema name.\n * 任意のスキーマ名の下に日報 7 テーブルを生成する汎用ファクトリ。\n *\n * 制約・索引名は `${schemaName}_<Table>_...` 規約で生成する。\n * ユーザーテーブルへの FK は注入された `userTable` を参照する。\n */\nexport function defineDailyReportSchema<S extends string>(schemaName: S, opts: { userTable: DailyReportUserTable }) {\n const s = mssqlSchema(schemaName)\n const users = opts.userTable\n\n const hub = s.table(\n \"DailyReportHub\",\n {\n id: bigint(\"id\", { mode: \"number\" }).identity().notNull(),\n sourceType: nvarchar(\"source_type\", { length: 20 }).notNull(),\n sourceId: nvarchar(\"source_id\", { length: 100 }).notNull(),\n sourceIdNum: bigint(\"source_id_num\", { mode: \"number\" }).generatedAlwaysAs(sql`TRY_CAST(source_id AS BIGINT)`),\n businessDate: date(\"business_date\"),\n userId: bigint(\"user_id\", { mode: \"number\" }),\n title: nvarchar(\"title\", { length: 200 }),\n summary: nvarchar(\"summary\", { length: \"max\" }),\n createdAt: datetime2(\"created_at\").notNull(),\n createdBy: nvarchar(\"created_by\", { length: 50 }).notNull(),\n updatedAt: datetime2(\"updated_at\").notNull(),\n updatedBy: nvarchar(\"updated_by\", { length: 50 }).notNull(),\n deletedAt: datetime2(\"deleted_at\"),\n deletedBy: nvarchar(\"deleted_by\", { length: 50 }),\n },\n (table) => [\n primaryKey({ name: `${schemaName}_DailyReportHub_pk`, columns: [table.id] }),\n foreignKey({\n name: `${schemaName}_DailyReportHub_user_id_fk`,\n columns: [table.userId],\n foreignColumns: [users.id],\n }),\n index(`${schemaName}_DailyReportHub_business_date_index`).on(table.businessDate),\n index(`${schemaName}_DailyReportHub_updated_at_index`).on(table.updatedAt),\n index(`${schemaName}_DailyReportHub_source_index`).on(table.sourceType, table.sourceId),\n index(`${schemaName}_DailyReportHub_user_id_index`).on(table.userId),\n index(`${schemaName}_DailyReportHub_business_date_id_index`).on(desc(table.businessDate), desc(table.id)),\n index(`${schemaName}_DailyReportHub_deleted_at_index`).on(table.deletedAt),\n ],\n )\n\n const internal = s.table(\n \"DailyReportInternal\",\n {\n hubId: bigint(\"hub_id\", { mode: \"number\" }).notNull(),\n body: nvarchar(\"body\", { length: \"max\" }),\n metadata: nvarchar(\"metadata\", { length: \"max\" }),\n createdAt: datetime2(\"created_at\").notNull(),\n createdBy: nvarchar(\"created_by\", { length: 50 }).notNull(),\n updatedAt: datetime2(\"updated_at\").notNull(),\n updatedBy: nvarchar(\"updated_by\", { length: 50 }).notNull(),\n },\n (table) => [\n primaryKey({ name: `${schemaName}_DailyReportInternal_pk`, columns: [table.hubId] }),\n foreignKey({\n name: `${schemaName}_DailyReportInternal_hub_id_fk`,\n columns: [table.hubId],\n foreignColumns: [hub.id],\n }),\n ],\n )\n\n const comment = s.table(\n \"DailyReportComment\",\n {\n id: bigint(\"id\", { mode: \"number\" }).identity().notNull(),\n hubId: bigint(\"hub_id\", { mode: \"number\" }).notNull(),\n userId: bigint(\"user_id\", { mode: \"number\" }).notNull(),\n body: nvarchar(\"body\", { length: \"max\" }).notNull(),\n createdAt: datetime2(\"created_at\").notNull(),\n createdBy: nvarchar(\"created_by\", { length: 50 }).notNull(),\n updatedAt: datetime2(\"updated_at\").notNull(),\n updatedBy: nvarchar(\"updated_by\", { length: 50 }).notNull(),\n },\n (table) => [\n primaryKey({ name: `${schemaName}_DailyReportComment_pk`, columns: [table.id] }),\n foreignKey({\n name: `${schemaName}_DailyReportComment_hub_id_fk`,\n columns: [table.hubId],\n foreignColumns: [hub.id],\n }),\n foreignKey({\n name: `${schemaName}_DailyReportComment_user_id_fk`,\n columns: [table.userId],\n foreignColumns: [users.id],\n }),\n index(`${schemaName}_DailyReportComment_hub_id_index`).on(table.hubId),\n ],\n )\n\n const label = s.table(\n \"DailyReportLabel\",\n {\n id: bigint(\"id\", { mode: \"number\" }).identity().notNull(),\n userId: bigint(\"user_id\", { mode: \"number\" }),\n name: nvarchar(\"name\", { length: 50 }).notNull(),\n color: nvarchar(\"color\", { length: 20 }),\n sortOrder: int(\"sort_order\"),\n createdAt: datetime2(\"created_at\").notNull(),\n createdBy: nvarchar(\"created_by\", { length: 50 }).notNull(),\n updatedAt: datetime2(\"updated_at\").notNull(),\n updatedBy: nvarchar(\"updated_by\", { length: 50 }).notNull(),\n },\n (table) => [\n primaryKey({ name: `${schemaName}_DailyReportLabel_pk`, columns: [table.id] }),\n foreignKey({\n name: `${schemaName}_DailyReportLabel_user_id_fk`,\n columns: [table.userId],\n foreignColumns: [users.id],\n }),\n ],\n )\n\n const hubLabel = s.table(\n \"DailyReportHub_Label\",\n {\n hubId: bigint(\"hub_id\", { mode: \"number\" }).notNull(),\n labelId: bigint(\"label_id\", { mode: \"number\" }).notNull(),\n createdAt: datetime2(\"created_at\").notNull(),\n createdBy: nvarchar(\"created_by\", { length: 50 }).notNull(),\n },\n (table) => [\n primaryKey({ name: `${schemaName}_DailyReportHub_Label_pk`, columns: [table.hubId, table.labelId] }),\n foreignKey({\n name: `${schemaName}_DailyReportHub_Label_hub_id_fk`,\n columns: [table.hubId],\n foreignColumns: [hub.id],\n }),\n foreignKey({\n name: `${schemaName}_DailyReportHub_Label_label_id_fk`,\n columns: [table.labelId],\n foreignColumns: [label.id],\n }),\n ],\n )\n\n const userStatus = s.table(\n \"DailyReportUserStatus\",\n {\n hubId: bigint(\"hub_id\", { mode: \"number\" }).notNull(),\n userId: bigint(\"user_id\", { mode: \"number\" }).notNull(),\n isRead: bit(\"is_read\").default(false).notNull(),\n isStarred: bit(\"is_starred\").default(false).notNull(),\n createdAt: datetime2(\"created_at\").notNull(),\n createdBy: nvarchar(\"created_by\", { length: 50 }).notNull(),\n updatedAt: datetime2(\"updated_at\").notNull(),\n updatedBy: nvarchar(\"updated_by\", { length: 50 }).notNull(),\n },\n (table) => [\n primaryKey({ name: `${schemaName}_DailyReportUserStatus_pk`, columns: [table.hubId, table.userId] }),\n foreignKey({\n name: `${schemaName}_DailyReportUserStatus_hub_id_fk`,\n columns: [table.hubId],\n foreignColumns: [hub.id],\n }),\n foreignKey({\n name: `${schemaName}_DailyReportUserStatus_user_id_fk`,\n columns: [table.userId],\n foreignColumns: [users.id],\n }),\n ],\n )\n\n const attachment = s.table(\n \"DailyReportAttachment\",\n {\n id: bigint(\"id\", { mode: \"number\" }).identity().notNull(),\n hubId: bigint(\"hub_id\", { mode: \"number\" }).notNull(),\n // 外部取込ソース由来の行には投稿者が存在しないため NULL 許容\n uploadedBy: bigint(\"uploaded_by\", { mode: \"number\" }),\n // 取込の冪等 upsert の自然キー。`<source>:<external-id>` 形式を推奨 (値の規約は消費アプリが決める)\n sourceKey: nvarchar(\"source_key\", { length: 200 }).notNull(),\n fileName: nvarchar(\"file_name\", { length: 255 }).notNull(),\n // ストレージ URI。受け入れるのは `gcs://` のみ (取込側で表明・読み取り側で再検証)\n filePath: nvarchar(\"file_path\", { length: \"max\" }).notNull(),\n fileType: nvarchar(\"file_type\", { length: 100 }),\n fileSize: bigint(\"file_size\", { mode: \"number\" }),\n // 実体存在の三値をこの 2 列から導出する:\n // objectCheckedAt が null -> unknown (未検証)\n // objectMissingAt が非 null -> absent (消失を確認)\n // 上記以外 -> present (実在を確認)\n objectCheckedAt: datetime2(\"object_checked_at\"),\n objectMissingAt: datetime2(\"object_missing_at\"),\n createdAt: datetime2(\"created_at\").notNull(),\n createdBy: nvarchar(\"created_by\", { length: 50 }).notNull(),\n updatedAt: datetime2(\"updated_at\").notNull(),\n updatedBy: nvarchar(\"updated_by\", { length: 50 }).notNull(),\n },\n (table) => [\n primaryKey({ name: `${schemaName}_DailyReportAttachment_pk`, columns: [table.id] }),\n foreignKey({\n name: `${schemaName}_DailyReportAttachment_hub_id_fk`,\n columns: [table.hubId],\n foreignColumns: [hub.id],\n }),\n foreignKey({\n name: `${schemaName}_DailyReportAttachment_uploaded_by_fk`,\n columns: [table.uploadedBy],\n foreignColumns: [users.id],\n }),\n // 取込の冪等性を DB 側で担保する\n uniqueIndex(`${schemaName}_DailyReportAttachment_source_unique`).on(table.hubId, table.sourceKey),\n // 一括取得 (hub_id IN (...) ORDER BY created_at) を被覆する\n index(`${schemaName}_DailyReportAttachment_hub_id_index`).on(table.hubId, table.createdAt),\n ],\n )\n\n return { hub, internal, comment, attachment, label, hubLabel, userStatus } satisfies DailyReportTables\n}\n","/**\n * Server entry of @aiquants/daily-report: schema factory, service, SSE reader, and handler factories.\n * @aiquants/daily-report の server エントリ。スキーマファクトリ・サービス・SSE リーダー・ハンドラ工場を公開。\n */\nimport { createEpochStore, SqlResultCache } from \"./server/cache\"\nimport type { DailyReportExternalSource } from \"./server/external-source\"\nimport { createDailyReportHandlers } from \"./server/handlers\"\nimport type { DailyReportAuthenticate, DailyReportReadAttachment } from \"./server/ports\"\nimport { createDailyReportService, type DailyReportServiceConfig } from \"./server/service\"\nimport { DailyReportSseReader } from \"./server/sse-reader\"\n\nexport * from \"./server/authz\"\nexport * from \"./server/cache\"\nexport { generateETag } from \"./server/etag\"\nexport * from \"./server/external-source\"\nexport * from \"./server/handlers\"\nexport * from \"./server/ports\"\nexport * from \"./server/response\"\nexport * from \"./server/schema\"\nexport * from \"./server/service\"\nexport * from \"./server/sse-reader\"\n\n/** createDailyReportServer の設定 (サービス設定 + 認証ポート + ハンドラ設定)。 */\nexport type DailyReportServerConfig = Omit<DailyReportServiceConfig, \"cache\" | \"epochs\"> & {\n /** リクエスト認証ポート。 */\n authenticate: DailyReportAuthenticate\n /** 添付ファイル読み取りポート。未注入なら添付エンドポイントは常に 404。 */\n readAttachment?: DailyReportReadAttachment\n /** 添付ファイルの最大バイト数 (既定 32 MiB)。サービス側のワイヤ上限より低く保つこと。 */\n attachmentMaxBytes?: number\n /** 添付エンドポイントの 1 分あたり呼び出し上限 (ユーザー単位・既定 60)。 */\n attachmentRateLimitPerMinute?: number\n /** 添付読み取りの同時実行上限 (プロセス単位・既定 4)。 */\n attachmentConcurrency?: number\n /** 未ログイン時のリダイレクト先 (index.loader 用、既定 \"/auth/login\")。 */\n loginRedirectPath?: string\n /** SQL 結果キャッシュの既定 TTL (既定 60,000ms)。 */\n cacheDefaultTtlMs?: number\n /** 外部ソースアダプタ群 (レガシー日報テーブル等)。 */\n externalSources?: DailyReportExternalSource[]\n}\n\n/**\n * One-stop factory wiring cache, service, SSE fan-out reader, and route handlers.\n * キャッシュ・サービス・SSE Fan-Out リーダー・ルートハンドラを一括結線するファクトリ。\n *\n * @example\n * const server = createDailyReportServer({ db, tables, userTable, resolveUserId, encodeUserId, authenticate, redis })\n * // routes:\n * // daily_report._index → server.index.loader (+ data() ラップ)\n * // daily_report.api.$endpoint → server.api.loader / server.api.action\n * // sse.daily_report.$endpoint → server.sse.loader\n */\nexport function createDailyReportServer(config: DailyReportServerConfig) {\n // epoch ストアと SQL 結果キャッシュ (サービスと SSE リーダーで同一インスタンスを共有する)\n const epochs = createEpochStore(config.redis)\n const cache = new SqlResultCache({ defaultTtlMs: config.cacheDefaultTtlMs ?? 60_000 }, epochs)\n\n const service = createDailyReportService({ ...config, cache, epochs })\n\n const sseReader = new DailyReportSseReader({\n redis: config.redis,\n streamKey: service.streamKey,\n cache,\n logger: config.logger,\n })\n\n // ❗ この呼び出しはスプレッドではなく明示列挙である。新しいポートをここへ足し忘れると\n // アプリが注入しても黙って捨てられ、添付エンドポイントは永久に 404 を返す。\n const handlers = createDailyReportHandlers({\n authenticate: config.authenticate,\n service,\n encodeUserId: config.encodeUserId,\n attachmentIdCodec: config.attachmentIdCodec,\n readAttachment: config.readAttachment,\n attachmentMaxBytes: config.attachmentMaxBytes,\n attachmentRateLimitPerMinute: config.attachmentRateLimitPerMinute,\n attachmentConcurrency: config.attachmentConcurrency,\n redis: config.redis,\n sseReader,\n streamKey: service.streamKey,\n loginRedirectPath: config.loginRedirectPath,\n logger: config.logger,\n })\n\n return {\n /** データアクセスサービス (CRUD + キャッシュ + SSE publish)。 */\n service,\n /** SQL 結果キャッシュ (サービス・SSE リーダー共有インスタンス)。 */\n cache,\n /** クロスワーカー epoch ストア。 */\n epochs,\n /** SSE Fan-Out 共有リーダー。 */\n sseReader,\n /** SSE Redis Stream キー。 */\n streamKey: service.streamKey,\n ...handlers,\n }\n}\n"],"mappings":"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","names":["createEpochStore","redis","epochKey","client","val","SqlResultCache","config","epochStore","cacheKey","opts","forceRefresh","snapshot","bucket","currentEpoch","r","shared","promise","records","expireAt","epoch","result","prefix","key","now","ISO_DATE_PATTERN","SLASH_DATE_PATTERN","zeroPad","value","normalizeFromDate","year","month","day","normalizeBusinessDateKey","trimmed","parsed","createLogger","level","prefix","impl","format","message","rest","z","dailyReportInterviewerSchema","dailyReportCommentSchema","dailyReportLabelDefSchema","dailyReportCommentItemSchema","dailyReportAttachmentItemSchema","dailyReportDetailSchema","connectedMessageSchema","statusUpdateMessageSchema","commentAddMessageSchema","commentDeleteMessageSchema","reportCreateMessageSchema","reportUpdateMessageSchema","reportPublishMessageSchema","reportDeleteMessageSchema","dailyReportSseMessageSchema","createHash","generateETag","data","json","defaultLogger","createLogger","jsonResponseWithETag","request","cookie","payload","status","logger","etag","generateETag","ifNoneMatch","headers","EventEmitter","isStreamIdLte","a","b","aMs","aSeq","bMs","bSeq","DailyReportSseReader","config","EventEmitter","createLogger","onEntry","onError","resolve","err","results","stream","msg","parsed","msgType","reportHubId","jsonData","payload","init","DEFAULT_ATTACHMENT_MAX_BYTES","DEFAULT_ATTACHMENT_RATE_LIMIT_PER_MINUTE","DEFAULT_ATTACHMENT_CONCURRENCY","ATTACHMENT_BODY_FLUSH_TIMEOUT_MS","RATE_LIMITER_MAX_ENTRIES","RATE_LIMIT_RETRY_AFTER_SECONDS","CONCURRENCY_RETRY_AFTER_SECONDS","INLINE_SAFE_MEDIA_TYPES","HEADER_SAFE_PATTERN","MEDIA_TYPE_PATTERN","sanitizeMediaType","value","encodeRfc8187","name","c","asciiFallbackFileName","attachmentFailureToStatus","reason","ATTACHMENT_FAILURE_MESSAGES","createRateLimiter","limitPerMinute","buckets","userId","nowMs","existing","bucket","elapsedMs","allowed","oldest","createConcurrencyGate","limit","active","streamAndRelease","bytes","release","timeoutMs","timer","finish","sent","controller","createDailyReportHandlers","config","authenticate","service","encodeUserId","redis","sseReader","streamKey","loginRedirectPath","apiLogger","createLogger","sseLogger","attachmentLogger","attachmentMaxBytes","consumeAttachmentRateToken","attachmentGate","indexLoader","request","user","cookie","headers","hashedUserId","endpointHandlers","url","normalizedBusinessDate","normalizeBusinessDateKey","forceRefresh","jsonResponseWithETag","reports","dailyReportIds","param","parsedId","detail","params","sanitizedCookie","endpoint","handler","startTime","error","elapsed","err","formData","intent","reportHubIdRaw","reportHubId","businessDate","operationTimestamp","clientTempId","newReport","title","content","e","publishedReport","isStarredRaw","isStarred","updatedStatus","isReadRaw","isRead","newComment","safeComment","commentId","internalUserId","lastEventId","encoder","controllerRef","keepAliveInterval","unsubscribe","isCleaningUp","cleanup","processEntry","entryId","fields","raw","result","dailyReportSseMessageSchema","parsed","recipientRawUserId","sanitizedMessage","_","rest","stream","lastProcessedId","entry","isStreamIdLte","client","latest","connectedPayload","sseMessage","catchUpEntries","fail","status","message","retryAfterSeconds","codec","readAttachment","token","attachmentId","attachment","gateReleased","releaseGate","releaseDeferredToStream","isHead","declared","inlineSafe","wantsDownload","contentType","disposition","contentLength","aliasedTable","and","asc","desc","eq","getColumns","inArray","isNull","or","sql","cols","t","getColumns","createDailyReportService","config","db","tables","users","resolveUserId","encodeUserId","redis","sqlResultCache","epochs","DailyReportHub","DailyReportInternal","DailyReportCommentModel","DailyReportLabel","DailyReportHub_Label","DailyReportUserStatus","externalSources","draftLabelNames","draftLabelName","logger","createLogger","DAILY_REPORT_IDS_CACHE_KEY","DAILY_REPORT_IDS_TTL_MS","DAILY_REPORT_IDS_EPOCH_KEY","DAILY_REPORT_BUSINESS_DATE_CACHE_PREFIX","DAILY_REPORT_BUSINESS_DATE_TTL_MS","DAILY_REPORT_DATE_EPOCH_PREFIX","DAILY_REPORT_DETAIL_EPOCH_PREFIX","DAILY_REPORT_SSE_STREAM_KEY","DAILY_REPORT_SSE_STREAM_MAXLEN","incrementRedisEpoch","key","publishToSseStream","message","callerName","client","publishStartMs","publishDurationMs","e","formatDateValue","value","pattern","dateValue","opts","maskAuditActor","externalSelections","out","adapter","applyExternalJoins","chain","c","and","eq","mapHubRecord","row","currentUserId","hub","internal","content","interviewers","category","creationCategory","visitTimeFrom","visitTimeTo","customerName","employeeName","comments","a","externalRow","fields","labelsRaw","l","commentItemsRaw","isCreationCategoryDraft","cachedPrimaryDraftLabelId","cachedDraftLabelIds","fetchDailyReportIdsByUserId","userId","DraftLabelRelation","aliasedTable","draftLabelJoinIds","getDraftLabelJoinIds","inArray","isNull","or","desc","item","fetchDailyReportsByBusinessDate","normalizedBusinessDate","rows","sql","attachmentsMap","getAttachmentsByHubIds","r","buildBusinessDateCacheKey","userIdCache","getUserIdByExternalId","externalId","getDailyReportIdsByExternalId","forceRefresh","snapshot","ttlMsOverride","cacheKey","getDailyReportsByBusinessDate","businessDate","normalizeBusinessDateKey","getDraftLabelIds","ids","getDraftLabelId","primary","deriveAttachmentState","checkedAt","missingAt","hubIds","map","attachment","codec","asc","list","getAttachmentForUser","attachmentId","markAttachmentMissing","now","error","markAttachmentPresent","getDailyReportsByBusinessDateByExternalId","allLabels","allComments","labelsMap","commentsMap","label","comment","hubId","getDailyReportDetailById","reportHubId","labels","labelsMapped","commentsMapped","getDailyReportDetailByIdByExternalId","options","setStarStatus","isStarred","clientTempId","existing","updatedStatus","normalizedDate","statusUpdateMessageSchema","setReadStatus","isRead","addComment","inserted","user","userName","commentItem","commentAddMessageSchema","findDailyReportCommentById","tx","commentId","deleteComment","commentDeleteMessageSchema","createDailyReport","result","sourceId","draftLabelId","fullDetail","draftLabelIds","isDraft","reportCreateMessageSchema","findDailyReportHubById","updateDailyReportHub","data","updateDailyReportInternal","deleteDailyReportLabel","labelId","businessDates","normalized","report","reportUpdateMessageSchema","reportPublishMessageSchema","reportDeleteMessageSchema","and","eq","defineDailyReportAuthzResources","sources","list","targetSources","src","resourceKey","seedDailyReportAuthzResources","db","authzTables","opts","d","TMResource","appKey","actor","now","resourceList","res","existing","defaultLogger","createLogger","transformJsonArray","payload","label","mapper","logger","parsed","entry","error","desc","sql","bigint","bit","date","datetime2","foreignKey","index","int","mssqlSchema","nvarchar","primaryKey","uniqueIndex","defineDailyReportSchema","schemaName","opts","s","users","hub","table","internal","comment","label","hubLabel","userStatus","attachment","createDailyReportServer","config","epochs","createEpochStore","cache","SqlResultCache","service","createDailyReportService","sseReader","DailyReportSseReader","handlers","createDailyReportHandlers"]}