@aiquants/daily-report 0.1.1

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 (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +150 -0
  3. package/dist/client.d.mts +449 -0
  4. package/dist/client.d.ts +449 -0
  5. package/dist/client.js +7 -0
  6. package/dist/client.js.map +1 -0
  7. package/dist/client.mjs +7 -0
  8. package/dist/client.mjs.map +1 -0
  9. package/dist/index.d.mts +42 -0
  10. package/dist/index.d.ts +42 -0
  11. package/dist/index.js +2 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/index.mjs +2 -0
  14. package/dist/index.mjs.map +1 -0
  15. package/dist/logger-D3krZrNK.d.mts +29 -0
  16. package/dist/logger-D3krZrNK.d.ts +29 -0
  17. package/dist/server.d.mts +1515 -0
  18. package/dist/server.d.ts +1515 -0
  19. package/dist/server.js +10 -0
  20. package/dist/server.js.map +1 -0
  21. package/dist/server.mjs +10 -0
  22. package/dist/server.mjs.map +1 -0
  23. package/dist/sse-schema-CK7cUnEo.d.ts +1986 -0
  24. package/dist/sse-schema-yl5AaSsj.d.mts +1986 -0
  25. package/dist/types-CVhwLhSN.d.mts +76 -0
  26. package/dist/types-CVhwLhSN.d.ts +76 -0
  27. package/package.json +108 -0
  28. package/src/client/components/business-day-thumb-overlay.tsx +19 -0
  29. package/src/client/components/daily-report-comment-item.tsx +81 -0
  30. package/src/client/components/daily-report-comment-section.tsx +166 -0
  31. package/src/client/components/daily-report-detail-list.tsx +676 -0
  32. package/src/client/components/daily-report-edit-form.tsx +81 -0
  33. package/src/client/components/daily-report-list.tsx +1024 -0
  34. package/src/client/components/daily-report-page.tsx +147 -0
  35. package/src/client/components/daily-report-resolved-content.tsx +139 -0
  36. package/src/client/components/unread-indicator.tsx +13 -0
  37. package/src/client/config-context.tsx +129 -0
  38. package/src/client/contexts/daily-report-action-context.tsx +910 -0
  39. package/src/client/hooks/use-daily-report-comments.ts +73 -0
  40. package/src/client/hooks/use-daily-report-sse-connection.ts +86 -0
  41. package/src/client/hooks/use-daily-report.spec.ts +155 -0
  42. package/src/client/hooks/use-daily-report.ts +426 -0
  43. package/src/client/hooks/use-dynamic-viewport-height.ts +127 -0
  44. package/src/client/route-helpers.ts +76 -0
  45. package/src/client/ui/button.tsx +42 -0
  46. package/src/client/ui/cn.ts +14 -0
  47. package/src/client/ui/input.tsx +21 -0
  48. package/src/client/ui/label.tsx +16 -0
  49. package/src/client/ui/switch.tsx +19 -0
  50. package/src/client/ui/tabs.tsx +40 -0
  51. package/src/client/ui/textarea.tsx +19 -0
  52. package/src/client/utils/constants.ts +29 -0
  53. package/src/client.ts +21 -0
  54. package/src/index.ts +10 -0
  55. package/src/server/cache.ts +165 -0
  56. package/src/server/etag.ts +18 -0
  57. package/src/server/external-source.ts +60 -0
  58. package/src/server/handlers.ts +543 -0
  59. package/src/server/ports.ts +68 -0
  60. package/src/server/response.ts +37 -0
  61. package/src/server/schema.ts +266 -0
  62. package/src/server/service.spec.ts +97 -0
  63. package/src/server/service.ts +1308 -0
  64. package/src/server/sse-reader.spec.ts +55 -0
  65. package/src/server/sse-reader.ts +223 -0
  66. package/src/server.ts +83 -0
  67. package/src/shared/business-date.spec.ts +61 -0
  68. package/src/shared/business-date.ts +84 -0
  69. package/src/shared/comment-adapter.ts +78 -0
  70. package/src/shared/logger.ts +47 -0
  71. package/src/shared/sse-schema.ts +147 -0
  72. package/src/shared/text-utils.ts +57 -0
  73. package/src/shared/types.ts +76 -0
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Structural table types + drizzle (mssql) schema factory for the daily-report tables.
3
+ * 日報テーブル群の構造的テーブル型と drizzle (mssql) スキーマファクトリ。
4
+ *
5
+ * 既存アプリは自前のモデル定義をそのまま `DailyReportTables` として注入できる (構造互換)。
6
+ * 新規プロジェクトは `defineDailyReportSchema()` で同一構造のテーブル定義を生成できる。
7
+ */
8
+ import { desc, sql } from "drizzle-orm"
9
+ import { type AnyMsSqlColumn, bigint, bit, date, datetime2, foreignKey, index, int, mssqlSchema, nvarchar, primaryKey } from "drizzle-orm/mssql-core"
10
+
11
+ /** 注入する外部ユーザーテーブルの最小形 (id / display_name)。 */
12
+ export type DailyReportUserTable = { id: AnyMsSqlColumn; displayName: AnyMsSqlColumn }
13
+
14
+ /** DailyReportHub テーブルの構造形。 */
15
+ export type DailyReportHubTable = {
16
+ id: AnyMsSqlColumn
17
+ sourceType: AnyMsSqlColumn
18
+ sourceId: AnyMsSqlColumn
19
+ sourceIdNum: AnyMsSqlColumn
20
+ businessDate: AnyMsSqlColumn
21
+ userId: AnyMsSqlColumn
22
+ title: AnyMsSqlColumn
23
+ summary: AnyMsSqlColumn
24
+ createdAt: AnyMsSqlColumn
25
+ createdBy: AnyMsSqlColumn
26
+ updatedAt: AnyMsSqlColumn
27
+ updatedBy: AnyMsSqlColumn
28
+ deletedAt: AnyMsSqlColumn
29
+ deletedBy: AnyMsSqlColumn
30
+ }
31
+
32
+ /** DailyReportInternal テーブルの構造形。 */
33
+ export type DailyReportInternalTable = {
34
+ hubId: AnyMsSqlColumn
35
+ body: AnyMsSqlColumn
36
+ metadata: AnyMsSqlColumn
37
+ createdAt: AnyMsSqlColumn
38
+ createdBy: AnyMsSqlColumn
39
+ updatedAt: AnyMsSqlColumn
40
+ updatedBy: AnyMsSqlColumn
41
+ }
42
+
43
+ /** DailyReportComment テーブルの構造形。 */
44
+ export type DailyReportCommentTable = {
45
+ id: AnyMsSqlColumn
46
+ hubId: AnyMsSqlColumn
47
+ userId: AnyMsSqlColumn
48
+ body: AnyMsSqlColumn
49
+ createdAt: AnyMsSqlColumn
50
+ createdBy: AnyMsSqlColumn
51
+ updatedAt: AnyMsSqlColumn
52
+ updatedBy: AnyMsSqlColumn
53
+ }
54
+
55
+ /** DailyReportLabel テーブルの構造形。 */
56
+ export type DailyReportLabelTable = {
57
+ id: AnyMsSqlColumn
58
+ userId: AnyMsSqlColumn
59
+ name: AnyMsSqlColumn
60
+ color: AnyMsSqlColumn
61
+ sortOrder: AnyMsSqlColumn
62
+ createdAt: AnyMsSqlColumn
63
+ createdBy: AnyMsSqlColumn
64
+ updatedAt: AnyMsSqlColumn
65
+ updatedBy: AnyMsSqlColumn
66
+ }
67
+
68
+ /** DailyReportHub_Label 中間テーブルの構造形。 */
69
+ export type DailyReportHubLabelTable = {
70
+ hubId: AnyMsSqlColumn
71
+ labelId: AnyMsSqlColumn
72
+ createdAt: AnyMsSqlColumn
73
+ createdBy: AnyMsSqlColumn
74
+ }
75
+
76
+ /** DailyReportUserStatus テーブルの構造形。 */
77
+ export type DailyReportUserStatusTable = {
78
+ hubId: AnyMsSqlColumn
79
+ userId: AnyMsSqlColumn
80
+ isRead: AnyMsSqlColumn
81
+ isStarred: AnyMsSqlColumn
82
+ createdAt: AnyMsSqlColumn
83
+ createdBy: AnyMsSqlColumn
84
+ updatedAt: AnyMsSqlColumn
85
+ updatedBy: AnyMsSqlColumn
86
+ }
87
+
88
+ /** サービスへ注入するテーブル一式 (アプリ既存モデルまたは本ファクトリ生成物)。 */
89
+ export type DailyReportTables = {
90
+ hub: DailyReportHubTable
91
+ internal: DailyReportInternalTable
92
+ comment: DailyReportCommentTable
93
+ label: DailyReportLabelTable
94
+ hubLabel: DailyReportHubLabelTable
95
+ userStatus: DailyReportUserStatusTable
96
+ }
97
+
98
+ /**
99
+ * Generic factory: build the six daily-report tables under an arbitrary schema name.
100
+ * 任意のスキーマ名の下に日報 6 テーブルを生成する汎用ファクトリ。
101
+ *
102
+ * 制約・索引名は `${schemaName}_<Table>_...` 規約で生成する。
103
+ * ユーザーテーブルへの FK は注入された `userTable` を参照する。
104
+ */
105
+ export function defineDailyReportSchema<S extends string>(schemaName: S, opts: { userTable: DailyReportUserTable }) {
106
+ const s = mssqlSchema(schemaName)
107
+ const users = opts.userTable
108
+
109
+ const hub = s.table(
110
+ "DailyReportHub",
111
+ {
112
+ id: bigint("id", { mode: "number" }).identity().notNull(),
113
+ sourceType: nvarchar("source_type", { length: 20 }).notNull(),
114
+ sourceId: nvarchar("source_id", { length: 100 }).notNull(),
115
+ sourceIdNum: bigint("source_id_num", { mode: "number" }).generatedAlwaysAs(sql`TRY_CAST(source_id AS BIGINT)`),
116
+ businessDate: date("business_date"),
117
+ userId: bigint("user_id", { mode: "number" }),
118
+ title: nvarchar("title", { length: 200 }),
119
+ summary: nvarchar("summary", { length: "max" }),
120
+ createdAt: datetime2("created_at").notNull(),
121
+ createdBy: nvarchar("created_by", { length: 50 }).notNull(),
122
+ updatedAt: datetime2("updated_at").notNull(),
123
+ updatedBy: nvarchar("updated_by", { length: 50 }).notNull(),
124
+ deletedAt: datetime2("deleted_at"),
125
+ deletedBy: nvarchar("deleted_by", { length: 50 }),
126
+ },
127
+ (table) => [
128
+ primaryKey({ name: `${schemaName}_DailyReportHub_pk`, columns: [table.id] }),
129
+ foreignKey({
130
+ name: `${schemaName}_DailyReportHub_user_id_fk`,
131
+ columns: [table.userId],
132
+ foreignColumns: [users.id],
133
+ }),
134
+ index(`${schemaName}_DailyReportHub_business_date_index`).on(table.businessDate),
135
+ index(`${schemaName}_DailyReportHub_updated_at_index`).on(table.updatedAt),
136
+ index(`${schemaName}_DailyReportHub_source_index`).on(table.sourceType, table.sourceId),
137
+ index(`${schemaName}_DailyReportHub_user_id_index`).on(table.userId),
138
+ index(`${schemaName}_DailyReportHub_business_date_id_index`).on(desc(table.businessDate), desc(table.id)),
139
+ index(`${schemaName}_DailyReportHub_deleted_at_index`).on(table.deletedAt),
140
+ ],
141
+ )
142
+
143
+ const internal = s.table(
144
+ "DailyReportInternal",
145
+ {
146
+ hubId: bigint("hub_id", { mode: "number" }).notNull(),
147
+ body: nvarchar("body", { length: "max" }),
148
+ metadata: nvarchar("metadata", { length: "max" }),
149
+ createdAt: datetime2("created_at").notNull(),
150
+ createdBy: nvarchar("created_by", { length: 50 }).notNull(),
151
+ updatedAt: datetime2("updated_at").notNull(),
152
+ updatedBy: nvarchar("updated_by", { length: 50 }).notNull(),
153
+ },
154
+ (table) => [
155
+ primaryKey({ name: `${schemaName}_DailyReportInternal_pk`, columns: [table.hubId] }),
156
+ foreignKey({
157
+ name: `${schemaName}_DailyReportInternal_hub_id_fk`,
158
+ columns: [table.hubId],
159
+ foreignColumns: [hub.id],
160
+ }),
161
+ ],
162
+ )
163
+
164
+ const comment = s.table(
165
+ "DailyReportComment",
166
+ {
167
+ id: bigint("id", { mode: "number" }).identity().notNull(),
168
+ hubId: bigint("hub_id", { mode: "number" }).notNull(),
169
+ userId: bigint("user_id", { mode: "number" }).notNull(),
170
+ body: nvarchar("body", { length: "max" }).notNull(),
171
+ createdAt: datetime2("created_at").notNull(),
172
+ createdBy: nvarchar("created_by", { length: 50 }).notNull(),
173
+ updatedAt: datetime2("updated_at").notNull(),
174
+ updatedBy: nvarchar("updated_by", { length: 50 }).notNull(),
175
+ },
176
+ (table) => [
177
+ primaryKey({ name: `${schemaName}_DailyReportComment_pk`, columns: [table.id] }),
178
+ foreignKey({
179
+ name: `${schemaName}_DailyReportComment_hub_id_fk`,
180
+ columns: [table.hubId],
181
+ foreignColumns: [hub.id],
182
+ }),
183
+ foreignKey({
184
+ name: `${schemaName}_DailyReportComment_user_id_fk`,
185
+ columns: [table.userId],
186
+ foreignColumns: [users.id],
187
+ }),
188
+ index(`${schemaName}_DailyReportComment_hub_id_index`).on(table.hubId),
189
+ ],
190
+ )
191
+
192
+ const label = s.table(
193
+ "DailyReportLabel",
194
+ {
195
+ id: bigint("id", { mode: "number" }).identity().notNull(),
196
+ userId: bigint("user_id", { mode: "number" }),
197
+ name: nvarchar("name", { length: 50 }).notNull(),
198
+ color: nvarchar("color", { length: 20 }),
199
+ sortOrder: int("sort_order"),
200
+ createdAt: datetime2("created_at").notNull(),
201
+ createdBy: nvarchar("created_by", { length: 50 }).notNull(),
202
+ updatedAt: datetime2("updated_at").notNull(),
203
+ updatedBy: nvarchar("updated_by", { length: 50 }).notNull(),
204
+ },
205
+ (table) => [
206
+ primaryKey({ name: `${schemaName}_DailyReportLabel_pk`, columns: [table.id] }),
207
+ foreignKey({
208
+ name: `${schemaName}_DailyReportLabel_user_id_fk`,
209
+ columns: [table.userId],
210
+ foreignColumns: [users.id],
211
+ }),
212
+ ],
213
+ )
214
+
215
+ const hubLabel = s.table(
216
+ "DailyReportHub_Label",
217
+ {
218
+ hubId: bigint("hub_id", { mode: "number" }).notNull(),
219
+ labelId: bigint("label_id", { mode: "number" }).notNull(),
220
+ createdAt: datetime2("created_at").notNull(),
221
+ createdBy: nvarchar("created_by", { length: 50 }).notNull(),
222
+ },
223
+ (table) => [
224
+ primaryKey({ name: `${schemaName}_DailyReportHub_Label_pk`, columns: [table.hubId, table.labelId] }),
225
+ foreignKey({
226
+ name: `${schemaName}_DailyReportHub_Label_hub_id_fk`,
227
+ columns: [table.hubId],
228
+ foreignColumns: [hub.id],
229
+ }),
230
+ foreignKey({
231
+ name: `${schemaName}_DailyReportHub_Label_label_id_fk`,
232
+ columns: [table.labelId],
233
+ foreignColumns: [label.id],
234
+ }),
235
+ ],
236
+ )
237
+
238
+ const userStatus = s.table(
239
+ "DailyReportUserStatus",
240
+ {
241
+ hubId: bigint("hub_id", { mode: "number" }).notNull(),
242
+ userId: bigint("user_id", { mode: "number" }).notNull(),
243
+ isRead: bit("is_read").default(false).notNull(),
244
+ isStarred: bit("is_starred").default(false).notNull(),
245
+ createdAt: datetime2("created_at").notNull(),
246
+ createdBy: nvarchar("created_by", { length: 50 }).notNull(),
247
+ updatedAt: datetime2("updated_at").notNull(),
248
+ updatedBy: nvarchar("updated_by", { length: 50 }).notNull(),
249
+ },
250
+ (table) => [
251
+ primaryKey({ name: `${schemaName}_DailyReportUserStatus_pk`, columns: [table.hubId, table.userId] }),
252
+ foreignKey({
253
+ name: `${schemaName}_DailyReportUserStatus_hub_id_fk`,
254
+ columns: [table.hubId],
255
+ foreignColumns: [hub.id],
256
+ }),
257
+ foreignKey({
258
+ name: `${schemaName}_DailyReportUserStatus_user_id_fk`,
259
+ columns: [table.userId],
260
+ foreignColumns: [users.id],
261
+ }),
262
+ ],
263
+ )
264
+
265
+ return { hub, internal, comment, label, hubLabel, userStatus } satisfies DailyReportTables
266
+ }
@@ -0,0 +1,97 @@
1
+ import { bigint, mssqlSchema, nvarchar, primaryKey } from "drizzle-orm/mssql-core"
2
+ import { describe, expect, it } from "vitest"
3
+ import { createEpochStore, SqlResultCache } from "./cache"
4
+ import { defineDailyReportSchema } from "./schema"
5
+ import { createDailyReportService, type DailyReportDb, type DailyReportHubRow } from "./service"
6
+
7
+ /**
8
+ * Security regression tests for audit-actor masking in mapHubRecord.
9
+ * mapHubRecord の監査アクターマスク (生内部 ID 漏洩防止) のセキュリティ回帰テスト。
10
+ */
11
+
12
+ // 実テーブル (getColumns / eq / desc が実カラムを要求するため mock 不可)
13
+ const testUsers = mssqlSchema("test").table("Users", { id: bigint("id", { mode: "number" }).notNull(), displayName: nvarchar("display_name", { length: 100 }) }, (t) => [primaryKey({ columns: [t.id] })])
14
+ const tables = defineDailyReportSchema("test", { userTable: testUsers })
15
+
16
+ /** getDailyReportsByBusinessDate 経路 (select→from→leftJoin→leftJoin→where→orderBy) だけを満たす最小モック。 */
17
+ const makeDb = (rows: unknown[]): DailyReportDb => {
18
+ const chain = {
19
+ from: () => chain,
20
+ innerJoin: () => chain,
21
+ leftJoin: () => chain,
22
+ where: () => chain,
23
+ top: () => chain,
24
+ orderBy: () => Promise.resolve(rows),
25
+ }
26
+ return { select: () => chain } as unknown as DailyReportDb
27
+ }
28
+
29
+ const baseHub = (over: Partial<DailyReportHubRow>): DailyReportHubRow => ({
30
+ id: 1,
31
+ sourceType: "Internal",
32
+ sourceId: "1",
33
+ sourceIdNum: 1,
34
+ businessDate: "2099-01-05",
35
+ userId: 8,
36
+ title: "t",
37
+ summary: "s",
38
+ createdAt: "2099-01-05 00:00:00",
39
+ createdBy: "8",
40
+ updatedAt: "2099-01-05 00:00:00",
41
+ updatedBy: "8",
42
+ deletedAt: null,
43
+ deletedBy: null,
44
+ ...over,
45
+ })
46
+
47
+ const makeService = (rows: unknown[]) => {
48
+ const epochs = createEpochStore(undefined)
49
+ const cache = new SqlResultCache({ defaultTtlMs: 60_000 }, epochs)
50
+ return createDailyReportService({
51
+ db: makeDb(rows),
52
+ tables,
53
+ userTable: testUsers,
54
+ resolveUserId: async () => 8,
55
+ encodeUserId: (id) => `ENC${id}`, // テスト用の識別可能な難読化
56
+ cache,
57
+ epochs,
58
+ draftLabelName: "draft", // 必須注入 (ドラフト可視性フィルタ用のラベル名。テストは中立値で十分)
59
+ })
60
+ }
61
+
62
+ describe("mapHubRecord audit-actor masking (security)", () => {
63
+ it("数値の updatedBy / createdBy (= 生内部 ID) は encodeUserId で難読化される", async () => {
64
+ const svc = makeService([{ hub: baseHub({ createdBy: "8", updatedBy: "8" }), internal: null, creatorName: null }])
65
+ const [r] = await svc.getDailyReportsByBusinessDate("2099-01-05", { forceRefresh: true })
66
+ // 生 ID "8" がそのまま出ないこと (漏洩防止の核心)
67
+ expect(r.updatedBy).toBe("ENC8")
68
+ expect(r.updatedBy).not.toBe("8")
69
+ expect(r.author).toBe("ENC8") // creatorName(null) → createdBy をマスク
70
+ expect(r.employeeName).toBe("ENC8")
71
+ // userId フィールドは従来どおり難読化
72
+ expect(r.userId).toBe("ENC8")
73
+ })
74
+
75
+ it("表示名 (creatorName) がある場合はマスクせず表示名を優先する", async () => {
76
+ const svc = makeService([{ hub: baseHub({ createdBy: "8", updatedBy: "8" }), internal: null, creatorName: "山田太郎" }])
77
+ const [r] = await svc.getDailyReportsByBusinessDate("2099-01-05", { forceRefresh: true })
78
+ expect(r.author).toBe("山田太郎")
79
+ expect(r.employeeName).toBe("山田太郎")
80
+ // updatedBy は表示名解決を持たないため常にマスク
81
+ expect(r.updatedBy).toBe("ENC8")
82
+ })
83
+
84
+ it("非数値の監査値 (レガシー氏名/システム文字列) はそのまま通す", async () => {
85
+ const svc = makeService([{ hub: baseHub({ createdBy: "system-import", updatedBy: "batch" }), internal: null, creatorName: null }])
86
+ const [r] = await svc.getDailyReportsByBusinessDate("2099-01-05", { forceRefresh: true })
87
+ expect(r.author).toBe("system-import")
88
+ expect(r.updatedBy).toBe("batch")
89
+ })
90
+
91
+ it("空 / null の監査値は null になる (生値の漏れなし)", async () => {
92
+ const svc = makeService([{ hub: baseHub({ createdBy: "", updatedBy: "" }), internal: null, creatorName: null }])
93
+ const [r] = await svc.getDailyReportsByBusinessDate("2099-01-05", { forceRefresh: true })
94
+ expect(r.updatedBy).toBeNull()
95
+ expect(r.author).toBe("") // author は非 null 契約のため空文字
96
+ })
97
+ })