create-fullstack-scaffold 0.4.24 → 0.5.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 (146) hide show
  1. package/dist/cli/index.js +439 -15
  2. package/dist/cli/index.js.map +1 -1
  3. package/package.json +10 -13
  4. package/template/eslint-rules/__tests__/no-merged-api-type-export.test.ts +96 -0
  5. package/template/eslint-rules/no-cross-module-service-import.js +4 -0
  6. package/template/eslint-rules/no-merged-api-type-export.js +190 -0
  7. package/template/eslint.config.js +3 -0
  8. package/template/package.json +9 -6
  9. package/template/scripts/sync-agent-hooks.mjs +189 -0
  10. package/template/src/admin/components/__tests__/StatsCard.test.tsx +2 -2
  11. package/template/src/admin/pages/ContentPage.tsx +1 -1
  12. package/template/src/admin/pages/DisputesPage.tsx +1 -1
  13. package/template/src/admin/pages/OrdersPage.tsx +1 -1
  14. package/template/src/admin/pages/TicketsPage.tsx +1 -1
  15. package/template/src/admin/pages/__tests__/ContentPage.test.tsx +5 -1
  16. package/template/src/admin/pages/__tests__/DashboardPage.test.tsx +62 -8
  17. package/template/src/admin/pages/__tests__/DisputesPage.test.tsx +8 -1
  18. package/template/src/admin/pages/__tests__/OrdersPage.test.tsx +4 -2
  19. package/template/src/admin/pages/__tests__/RegisterPage.test.tsx +40 -43
  20. package/template/src/admin/pages/__tests__/SettingsPage.test.tsx +83 -21
  21. package/template/src/admin/pages/__tests__/TicketsPage.test.tsx +3 -1
  22. package/template/src/admin/services/apiClient.ts +2 -3
  23. package/template/src/cli/modules/content/index.ts +3 -3
  24. package/template/src/cli/modules/dispute/index.ts +3 -3
  25. package/template/src/cli/modules/ticket/index.ts +3 -3
  26. package/template/src/cli/modules/todo/index.ts +1 -1
  27. package/template/src/cli/rpc/client.ts +3 -4
  28. package/template/src/cli/rpc/index.ts +1 -1
  29. package/template/src/client/App.tsx +3 -39
  30. package/template/src/client/AppRoutes.tsx +53 -0
  31. package/template/src/client/entry-server.tsx +75 -0
  32. package/template/src/client/main.tsx +3 -1
  33. package/template/src/client/pages/SearchPage.tsx +1 -1
  34. package/template/src/client/services/apiClient.ts +12 -4
  35. package/template/src/client/stores/__tests__/todoStore.test.ts +12 -3
  36. package/template/src/client/stores/entry-stores.ts +36 -0
  37. package/template/src/client/stores/notificationStore.ts +4 -4
  38. package/template/src/client/stores/todoStore.ts +2 -2
  39. package/template/src/merchant/pages/DisputesPage.tsx +3 -3
  40. package/template/src/merchant/pages/OrdersPage.tsx +1 -1
  41. package/template/src/merchant/pages/ProductsPage.tsx +1 -1
  42. package/template/src/merchant/pages/SettingsPage.tsx +1 -1
  43. package/template/src/server/__tests__/integration/isr-full-flow.test.ts +251 -0
  44. package/template/src/server/__tests__/integration/todos-api.test.ts +7 -4
  45. package/template/src/server/app.ts +6 -4
  46. package/template/src/server/core/__tests__/isr-cache.test.ts +96 -92
  47. package/template/src/server/core/__tests__/isr-invalidation.test.ts +29 -4
  48. package/template/src/server/core/__tests__/isr-registry.test.ts +215 -0
  49. package/template/src/server/core/__tests__/isr-renderer.test.ts +121 -0
  50. package/template/src/server/core/isr-cache.ts +6 -12
  51. package/template/src/server/core/isr-invalidation.ts +6 -12
  52. package/template/src/server/core/isr-registry.ts +104 -0
  53. package/template/src/server/core/isr-renderer.ts +75 -0
  54. package/template/src/server/db/schema/contents.ts +28 -20
  55. package/template/src/server/db/schema/disputes.ts +30 -22
  56. package/template/src/server/db/schema/notifications.ts +20 -14
  57. package/template/src/server/db/schema/orders.ts +23 -16
  58. package/template/src/server/db/schema/plugins.ts +56 -38
  59. package/template/src/server/db/schema/products.ts +24 -17
  60. package/template/src/server/db/schema/tickets.ts +44 -31
  61. package/template/src/server/db/schema/todos.ts +26 -18
  62. package/template/src/server/entries/cloudflare.ts +82 -19
  63. package/template/src/server/entries/node.ts +0 -2
  64. package/template/src/server/index.ts +0 -1
  65. package/template/src/server/isr-modules.ts +10 -0
  66. package/template/src/server/module-admin/__tests__/admin-service.test.ts +36 -12
  67. package/template/src/server/module-admin/routes/admin-notification-routes.ts +2 -1
  68. package/template/src/server/module-admin/routes/admin-routes.ts +3 -0
  69. package/template/src/server/module-admin/routes/dashboard-routes.ts +3 -0
  70. package/template/src/server/module-admin/services/admin-service.ts +57 -13
  71. package/template/src/server/module-auth/routes/auth-routes.ts +3 -0
  72. package/template/src/server/module-auth/routes/profile-routes.ts +3 -0
  73. package/template/src/server/module-captcha/routes/captcha-routes.ts +3 -0
  74. package/template/src/server/module-chat/routes/chat-routes.ts +4 -1
  75. package/template/src/server/module-content/__tests__/content-route.test.ts +2 -1
  76. package/template/src/server/module-content/__tests__/content-service.test.ts +2 -1
  77. package/template/src/server/module-content/__tests__/isr.test.ts +138 -0
  78. package/template/src/server/module-content/isr.ts +63 -0
  79. package/template/src/server/module-content/routes/content-routes.ts +10 -10
  80. package/template/src/server/module-content/routes/public-content-routes.ts +8 -4
  81. package/template/src/server/module-content/routes/topics-routes.ts +3 -0
  82. package/template/src/server/module-content/services/content-service.ts +23 -10
  83. package/template/src/server/module-dispute/__tests__/dispute-route.test.ts +2 -1
  84. package/template/src/server/module-dispute/__tests__/dispute-service.test.ts +2 -1
  85. package/template/src/server/module-dispute/routes/dispute-routes.ts +11 -10
  86. package/template/src/server/module-dispute/services/dispute-service.ts +15 -3
  87. package/template/src/server/module-file/routes/file-routes.ts +3 -0
  88. package/template/src/server/module-merchant/routes/merchant-routes.ts +3 -0
  89. package/template/src/server/module-merchant/services/merchant-service.ts +8 -8
  90. package/template/src/server/module-notifications/routes/notification-routes.ts +5 -1
  91. package/template/src/server/module-order/__tests__/order-route.test.ts +8 -8
  92. package/template/src/server/module-order/__tests__/order-service.test.ts +4 -3
  93. package/template/src/server/module-order/routes/cart-routes.ts +3 -0
  94. package/template/src/server/module-order/routes/order-routes.ts +9 -4
  95. package/template/src/server/module-order/routes/orders-mock-routes.ts +3 -0
  96. package/template/src/server/module-order/services/order-service.ts +25 -13
  97. package/template/src/server/module-permission/__tests__/audit-log-service.test.ts +464 -0
  98. package/template/src/server/module-permission/__tests__/role-service.test.ts +348 -0
  99. package/template/src/server/module-permission/routes/audit-log-routes.ts +3 -0
  100. package/template/src/server/module-permission/routes/permission-routes.ts +3 -0
  101. package/template/src/server/module-permission/routes/role-routes.ts +3 -0
  102. package/template/src/server/module-plugin/routes/plugin-admin-routes.ts +3 -0
  103. package/template/src/server/module-plugin/routes/plugin-routes.ts +3 -0
  104. package/template/src/server/module-plugin/services/admin-plugin-service.ts +10 -18
  105. package/template/src/server/module-plugin/services/admin-stats-service.ts +28 -9
  106. package/template/src/server/module-plugin/services/plugin-query-service.ts +42 -66
  107. package/template/src/server/module-tenant/routes/tenant-routes.ts +5 -1
  108. package/template/src/server/module-ticket/__tests__/ticket-route.test.ts +2 -1
  109. package/template/src/server/module-ticket/__tests__/ticket-service.test.ts +4 -3
  110. package/template/src/server/module-ticket/routes/ticket-routes.ts +11 -10
  111. package/template/src/server/module-ticket/services/ticket-service.ts +16 -5
  112. package/template/src/server/module-todos/__tests__/isr.test.ts +105 -0
  113. package/template/src/server/module-todos/__tests__/todo-service.test.ts +12 -9
  114. package/template/src/server/module-todos/__tests__/todos-route-rpc.test.ts +6 -6
  115. package/template/src/server/module-todos/isr.ts +41 -0
  116. package/template/src/server/module-todos/routes/todos-routes.ts +12 -5
  117. package/template/src/server/module-todos/services/todo-service.ts +31 -10
  118. package/template/src/server/route-registry.ts +0 -4
  119. package/template/src/server/rpc-merge.ts +27 -0
  120. package/template/src/server/rpc-surface.ts +117 -0
  121. package/template/src/server/rpc-type-canary.ts +80 -0
  122. package/template/src/server/test-utils/test-client.ts +7 -9
  123. package/template/src/server/test-utils/test-isr-helper.ts +140 -0
  124. package/template/src/shared/modules/content/schemas.ts +14 -0
  125. package/template/src/shared/modules/dispute/schemas.ts +14 -0
  126. package/template/src/shared/modules/order/schemas.ts +9 -1
  127. package/template/src/shared/modules/ticket/schemas.ts +14 -0
  128. package/template/src/shared/modules/todos/index.ts +4 -0
  129. package/template/src/shared/modules/todos/schemas.ts +14 -0
  130. package/template/src/shared/schemas/index.ts +18 -0
  131. package/template/tsup.config.ts +48 -1
  132. package/template/vitest.config.ts +19 -3
  133. package/template/vitest.setup.ts +68 -5
  134. package/template/drizzle/0000_rainy_boomer.sql +0 -101
  135. package/template/drizzle/0001_add_todo_attachments.sql +0 -12
  136. package/template/drizzle/0002_chilly_magneto.sql +0 -89
  137. package/template/drizzle/0003_ambiguous_magdalene.sql +0 -100
  138. package/template/drizzle/0004_add_merchants_products.sql +0 -30
  139. package/template/drizzle/meta/0000_snapshot.json +0 -652
  140. package/template/drizzle/meta/0001_snapshot.json +0 -735
  141. package/template/drizzle/meta/0002_snapshot.json +0 -1198
  142. package/template/drizzle/meta/0003_snapshot.json +0 -1837
  143. package/template/drizzle/meta/_journal.json +0 -41
  144. package/template/patches/typescript+5.9.3.patch +0 -24
  145. package/template/pnpm-lock.yaml +0 -7137
  146. /package/template/patches/{hono+4.12.16.patch → hono+4.12.34.patch} +0 -0
@@ -50,8 +50,13 @@ export async function seedOrdersIfEmpty(): Promise<void> {
50
50
  export async function getOrders(filters?: {
51
51
  status?: OrderStatus
52
52
  customerName?: string
53
- }): Promise<Order[]> {
53
+ page?: number
54
+ limit?: number
55
+ }): Promise<{ orders: Order[]; total: number; page: number; limit: number }> {
54
56
  const db = await getDb()
57
+ const page = filters?.page ?? 1
58
+ const limit = filters?.limit ?? 20
59
+ const offset = (page - 1) * limit
55
60
  const conditions = []
56
61
 
57
62
  if (filters?.status) {
@@ -66,7 +71,7 @@ export async function getOrders(filters?: {
66
71
  )
67
72
  }
68
73
 
69
- const rows =
74
+ const allRows =
70
75
  conditions.length > 0
71
76
  ? await db
72
77
  .select()
@@ -75,17 +80,24 @@ export async function getOrders(filters?: {
75
80
  .orderBy(desc(orders.createdAt))
76
81
  : await db.select().from(orders).orderBy(desc(orders.createdAt))
77
82
 
78
- return rows.map((row: OrderTable) => ({
79
- id: `order-${row.id}`,
80
- orderNo: row.orderNo,
81
- customerName: row.customerName,
82
- customerEmail: row.customerEmail,
83
- productName: row.productName,
84
- amount: row.amount,
85
- status: row.status,
86
- createdAt: toISOString(row.createdAt),
87
- updatedAt: toISOString(row.updatedAt),
88
- }))
83
+ const rows = allRows.slice(offset, offset + limit)
84
+
85
+ return {
86
+ orders: rows.map((row: OrderTable) => ({
87
+ id: `order-${row.id}`,
88
+ orderNo: row.orderNo,
89
+ customerName: row.customerName,
90
+ customerEmail: row.customerEmail,
91
+ productName: row.productName,
92
+ amount: row.amount,
93
+ status: row.status,
94
+ createdAt: toISOString(row.createdAt),
95
+ updatedAt: toISOString(row.updatedAt),
96
+ })),
97
+ total: allRows.length,
98
+ page,
99
+ limit,
100
+ }
89
101
  }
90
102
 
91
103
  export async function getOrderById(id: string): Promise<Order | null> {
@@ -0,0 +1,464 @@
1
+ import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest'
2
+ import { AuditLogService } from '../services/audit-log-service'
3
+ import { getRawClient, getDb } from '@server/db'
4
+ import { setupTestDatabase, cleanupTestDatabase } from '@server/db/test-setup'
5
+ import { RESOURCE_TYPES, ACTION_TYPES } from '@shared/constants'
6
+
7
+ describe('AuditLog Service', () => {
8
+ let service: AuditLogService
9
+
10
+ beforeAll(async () => {
11
+ await setupTestDatabase()
12
+ const db = await getDb()
13
+ expect(db).toBeDefined()
14
+ service = new AuditLogService()
15
+ })
16
+
17
+ afterAll(async () => {
18
+ await cleanupTestDatabase()
19
+ })
20
+
21
+ beforeEach(async () => {
22
+ const client = await getRawClient()
23
+ if (client && 'execute' in client) {
24
+ await client.execute('DELETE FROM permission_audit_logs')
25
+ }
26
+ })
27
+
28
+ afterEach(async () => {
29
+ const client = await getRawClient()
30
+ if (client && 'execute' in client) {
31
+ await client.execute('DELETE FROM permission_audit_logs')
32
+ }
33
+ })
34
+
35
+ async function insertTestLog(overrides: {
36
+ id: string
37
+ userId: string
38
+ action: string
39
+ resourceType: string
40
+ resourceId?: string
41
+ oldValue?: string
42
+ newValue?: string
43
+ ipAddress?: string
44
+ userAgent?: string
45
+ createdAt?: number
46
+ }) {
47
+ const client = await getRawClient()
48
+ if (client && 'execute' in client) {
49
+ await client.execute({
50
+ sql: `INSERT INTO permission_audit_logs (id, user_id, action, resource_type, resource_id, old_value, new_value, ip_address, user_agent, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
51
+ args: [
52
+ overrides.id,
53
+ overrides.userId,
54
+ overrides.action,
55
+ overrides.resourceType,
56
+ overrides.resourceId ?? null,
57
+ overrides.oldValue ?? null,
58
+ overrides.newValue ?? null,
59
+ overrides.ipAddress ?? null,
60
+ overrides.userAgent ?? null,
61
+ overrides.createdAt ?? Date.now(),
62
+ ],
63
+ })
64
+ }
65
+ }
66
+
67
+ describe('create', () => {
68
+ it('should create an audit log entry', async () => {
69
+ const data = {
70
+ id: 'log_1',
71
+ userId: 'user-1',
72
+ action: ACTION_TYPES.CREATE,
73
+ resourceType: RESOURCE_TYPES.ROLE,
74
+ resourceId: 'role-1',
75
+ newValue: '{"name":"Admin"}',
76
+ ipAddress: '127.0.0.1',
77
+ userAgent: 'TestAgent/1.0',
78
+ }
79
+
80
+ const result = await service.create(data)
81
+
82
+ expect(result.id).toBe('log_1')
83
+ expect(result.userId).toBe('user-1')
84
+ expect(result.action).toBe(ACTION_TYPES.CREATE)
85
+ expect(result.resourceType).toBe(RESOURCE_TYPES.ROLE)
86
+ expect(result.resourceId).toBe('role-1')
87
+ expect(result.newValue).toBe('{"name":"Admin"}')
88
+ expect(result.ipAddress).toBe('127.0.0.1')
89
+ expect(result.userAgent).toBe('TestAgent/1.0')
90
+ expect(typeof result.createdAt).toBe('string')
91
+ })
92
+
93
+ it('should create log with minimal fields', async () => {
94
+ const data = {
95
+ id: 'log_minimal',
96
+ userId: 'user-1',
97
+ action: ACTION_TYPES.UPDATE,
98
+ resourceType: RESOURCE_TYPES.USER,
99
+ }
100
+
101
+ const result = await service.create(data)
102
+
103
+ expect(result.id).toBe('log_minimal')
104
+ expect(result.userId).toBe('user-1')
105
+ expect(result.action).toBe(ACTION_TYPES.UPDATE)
106
+ expect(result.resourceType).toBe(RESOURCE_TYPES.USER)
107
+ expect(result.resourceId).toBeNull()
108
+ expect(result.oldValue).toBeNull()
109
+ expect(result.newValue).toBeNull()
110
+ })
111
+ })
112
+
113
+ describe('getAll', () => {
114
+ it('should return empty array when no logs exist', async () => {
115
+ const result = await service.getAll()
116
+
117
+ expect(result).toEqual([])
118
+ expect(Array.isArray(result)).toBe(true)
119
+ })
120
+
121
+ it('should return logs ordered by createdAt descending', async () => {
122
+ const now = Date.now()
123
+ await insertTestLog({
124
+ id: 'log_1',
125
+ userId: 'user-1',
126
+ action: ACTION_TYPES.CREATE,
127
+ resourceType: RESOURCE_TYPES.ROLE,
128
+ createdAt: now,
129
+ })
130
+ await insertTestLog({
131
+ id: 'log_2',
132
+ userId: 'user-2',
133
+ action: ACTION_TYPES.UPDATE,
134
+ resourceType: RESOURCE_TYPES.USER,
135
+ createdAt: now + 1000,
136
+ })
137
+
138
+ const result = await service.getAll()
139
+
140
+ expect(result).toHaveLength(2)
141
+ expect(result[0].id).toBe('log_2')
142
+ expect(result[1].id).toBe('log_1')
143
+ })
144
+
145
+ it('should respect limit parameter', async () => {
146
+ for (let i = 0; i < 5; i++) {
147
+ await insertTestLog({
148
+ id: `log_${i}`,
149
+ userId: 'user-1',
150
+ action: ACTION_TYPES.CREATE,
151
+ resourceType: RESOURCE_TYPES.ROLE,
152
+ createdAt: Date.now() + i,
153
+ })
154
+ }
155
+
156
+ const result = await service.getAll(2)
157
+
158
+ expect(result).toHaveLength(2)
159
+ })
160
+
161
+ it('should respect offset parameter', async () => {
162
+ for (let i = 0; i < 5; i++) {
163
+ await insertTestLog({
164
+ id: `log_${i}`,
165
+ userId: 'user-1',
166
+ action: ACTION_TYPES.CREATE,
167
+ resourceType: RESOURCE_TYPES.ROLE,
168
+ createdAt: Date.now() + i,
169
+ })
170
+ }
171
+
172
+ const result = await service.getAll(2, 3)
173
+
174
+ expect(result).toHaveLength(2)
175
+ })
176
+ })
177
+
178
+ describe('getByUserId', () => {
179
+ it('should return logs for specific user', async () => {
180
+ await insertTestLog({
181
+ id: 'log_1',
182
+ userId: 'user-1',
183
+ action: ACTION_TYPES.CREATE,
184
+ resourceType: RESOURCE_TYPES.ROLE,
185
+ })
186
+ await insertTestLog({
187
+ id: 'log_2',
188
+ userId: 'user-2',
189
+ action: ACTION_TYPES.UPDATE,
190
+ resourceType: RESOURCE_TYPES.USER,
191
+ })
192
+
193
+ const result = await service.getByUserId('user-1')
194
+
195
+ expect(result).toHaveLength(1)
196
+ expect(result[0].userId).toBe('user-1')
197
+ })
198
+
199
+ it('should return empty array for unknown user', async () => {
200
+ const result = await service.getByUserId('unknown')
201
+
202
+ expect(result).toEqual([])
203
+ })
204
+
205
+ it('should respect limit parameter', async () => {
206
+ for (let i = 0; i < 5; i++) {
207
+ await insertTestLog({
208
+ id: `log_${i}`,
209
+ userId: 'user-1',
210
+ action: ACTION_TYPES.CREATE,
211
+ resourceType: RESOURCE_TYPES.ROLE,
212
+ createdAt: Date.now() + i,
213
+ })
214
+ }
215
+
216
+ const result = await service.getByUserId('user-1', 3)
217
+
218
+ expect(result).toHaveLength(3)
219
+ })
220
+ })
221
+
222
+ describe('getByResource', () => {
223
+ it('should return logs for resource type', async () => {
224
+ await insertTestLog({
225
+ id: 'log_1',
226
+ userId: 'user-1',
227
+ action: ACTION_TYPES.CREATE,
228
+ resourceType: RESOURCE_TYPES.ROLE,
229
+ resourceId: 'role-1',
230
+ })
231
+ await insertTestLog({
232
+ id: 'log_2',
233
+ userId: 'user-1',
234
+ action: ACTION_TYPES.UPDATE,
235
+ resourceType: RESOURCE_TYPES.USER,
236
+ })
237
+
238
+ const result = await service.getByResource(RESOURCE_TYPES.ROLE)
239
+
240
+ expect(result).toHaveLength(1)
241
+ expect(result[0].resourceType).toBe(RESOURCE_TYPES.ROLE)
242
+ })
243
+
244
+ it('should filter by resourceId when provided', async () => {
245
+ await insertTestLog({
246
+ id: 'log_1',
247
+ userId: 'user-1',
248
+ action: ACTION_TYPES.CREATE,
249
+ resourceType: RESOURCE_TYPES.ROLE,
250
+ resourceId: 'role-1',
251
+ })
252
+ await insertTestLog({
253
+ id: 'log_2',
254
+ userId: 'user-1',
255
+ action: ACTION_TYPES.UPDATE,
256
+ resourceType: RESOURCE_TYPES.ROLE,
257
+ resourceId: 'role-2',
258
+ })
259
+
260
+ const result = await service.getByResource(RESOURCE_TYPES.ROLE, 'role-1')
261
+
262
+ expect(result).toHaveLength(1)
263
+ expect(result[0].resourceId).toBe('role-1')
264
+ })
265
+
266
+ it('should return all logs for resource type when resourceId is omitted', async () => {
267
+ await insertTestLog({
268
+ id: 'log_1',
269
+ userId: 'user-1',
270
+ action: ACTION_TYPES.CREATE,
271
+ resourceType: RESOURCE_TYPES.ROLE,
272
+ resourceId: 'role-1',
273
+ })
274
+ await insertTestLog({
275
+ id: 'log_2',
276
+ userId: 'user-1',
277
+ action: ACTION_TYPES.UPDATE,
278
+ resourceType: RESOURCE_TYPES.ROLE,
279
+ resourceId: 'role-2',
280
+ })
281
+
282
+ const result = await service.getByResource(RESOURCE_TYPES.ROLE)
283
+
284
+ expect(result).toHaveLength(2)
285
+ })
286
+ })
287
+
288
+ describe('search', () => {
289
+ it('should return all logs when no filters provided', async () => {
290
+ await insertTestLog({
291
+ id: 'log_1',
292
+ userId: 'user-1',
293
+ action: ACTION_TYPES.CREATE,
294
+ resourceType: RESOURCE_TYPES.ROLE,
295
+ })
296
+ await insertTestLog({
297
+ id: 'log_2',
298
+ userId: 'user-2',
299
+ action: ACTION_TYPES.UPDATE,
300
+ resourceType: RESOURCE_TYPES.USER,
301
+ })
302
+
303
+ const result = await service.search({})
304
+
305
+ expect(result).toHaveLength(2)
306
+ })
307
+
308
+ it('should filter by userId', async () => {
309
+ await insertTestLog({
310
+ id: 'log_1',
311
+ userId: 'user-1',
312
+ action: ACTION_TYPES.CREATE,
313
+ resourceType: RESOURCE_TYPES.ROLE,
314
+ })
315
+ await insertTestLog({
316
+ id: 'log_2',
317
+ userId: 'user-2',
318
+ action: ACTION_TYPES.CREATE,
319
+ resourceType: RESOURCE_TYPES.ROLE,
320
+ })
321
+
322
+ const result = await service.search({ userId: 'user-1' })
323
+
324
+ expect(result).toHaveLength(1)
325
+ expect(result[0].userId).toBe('user-1')
326
+ })
327
+
328
+ it('should filter by action', async () => {
329
+ await insertTestLog({
330
+ id: 'log_1',
331
+ userId: 'user-1',
332
+ action: ACTION_TYPES.CREATE,
333
+ resourceType: RESOURCE_TYPES.ROLE,
334
+ })
335
+ await insertTestLog({
336
+ id: 'log_2',
337
+ userId: 'user-1',
338
+ action: ACTION_TYPES.ASSIGN,
339
+ resourceType: RESOURCE_TYPES.ROLE,
340
+ })
341
+
342
+ const result = await service.search({ action: ACTION_TYPES.CREATE })
343
+
344
+ expect(result).toHaveLength(1)
345
+ expect(result[0].action).toBe(ACTION_TYPES.CREATE)
346
+ })
347
+
348
+ it('should filter by resourceType', async () => {
349
+ await insertTestLog({
350
+ id: 'log_1',
351
+ userId: 'user-1',
352
+ action: ACTION_TYPES.CREATE,
353
+ resourceType: RESOURCE_TYPES.ROLE,
354
+ })
355
+ await insertTestLog({
356
+ id: 'log_2',
357
+ userId: 'user-1',
358
+ action: ACTION_TYPES.CREATE,
359
+ resourceType: RESOURCE_TYPES.USER,
360
+ })
361
+
362
+ const result = await service.search({ resourceType: RESOURCE_TYPES.ROLE })
363
+
364
+ expect(result).toHaveLength(1)
365
+ expect(result[0].resourceType).toBe(RESOURCE_TYPES.ROLE)
366
+ })
367
+
368
+ it('should return logs ordered by createdAt descending', async () => {
369
+ await service.create({
370
+ id: 'log_old',
371
+ userId: 'user-1',
372
+ action: ACTION_TYPES.CREATE,
373
+ resourceType: RESOURCE_TYPES.ROLE,
374
+ })
375
+ await service.create({
376
+ id: 'log_new',
377
+ userId: 'user-1',
378
+ action: ACTION_TYPES.UPDATE,
379
+ resourceType: RESOURCE_TYPES.USER,
380
+ })
381
+
382
+ const result = await service.search({})
383
+
384
+ expect(result).toHaveLength(2)
385
+ const ids = result.map(r => r.id)
386
+ expect(ids).toContain('log_old')
387
+ expect(ids).toContain('log_new')
388
+ })
389
+
390
+ it('should combine multiple filters', async () => {
391
+ await insertTestLog({
392
+ id: 'log_1',
393
+ userId: 'user-1',
394
+ action: ACTION_TYPES.CREATE,
395
+ resourceType: RESOURCE_TYPES.ROLE,
396
+ })
397
+ await insertTestLog({
398
+ id: 'log_2',
399
+ userId: 'user-1',
400
+ action: ACTION_TYPES.UPDATE,
401
+ resourceType: RESOURCE_TYPES.ROLE,
402
+ })
403
+ await insertTestLog({
404
+ id: 'log_3',
405
+ userId: 'user-2',
406
+ action: ACTION_TYPES.CREATE,
407
+ resourceType: RESOURCE_TYPES.ROLE,
408
+ })
409
+
410
+ const result = await service.search({
411
+ userId: 'user-1',
412
+ action: ACTION_TYPES.CREATE,
413
+ })
414
+
415
+ expect(result).toHaveLength(1)
416
+ expect(result[0].id).toBe('log_1')
417
+ })
418
+ })
419
+
420
+ describe('deleteOlderThan', () => {
421
+ it('should delete logs older than specified days', async () => {
422
+ await service.create({
423
+ id: 'log_old',
424
+ userId: 'user-1',
425
+ action: ACTION_TYPES.CREATE,
426
+ resourceType: RESOURCE_TYPES.ROLE,
427
+ })
428
+ await new Promise(resolve => setTimeout(resolve, 100))
429
+ await service.create({
430
+ id: 'log_new',
431
+ userId: 'user-1',
432
+ action: ACTION_TYPES.UPDATE,
433
+ resourceType: RESOURCE_TYPES.USER,
434
+ })
435
+
436
+ const allLogs = await service.getAll()
437
+ expect(allLogs).toHaveLength(2)
438
+
439
+ const deletedCount = await service.deleteOlderThan(0)
440
+
441
+ expect(deletedCount).toBe(2)
442
+ })
443
+
444
+ it('should return 0 when no logs are old enough', async () => {
445
+ await insertTestLog({
446
+ id: 'log_recent',
447
+ userId: 'user-1',
448
+ action: ACTION_TYPES.CREATE,
449
+ resourceType: RESOURCE_TYPES.ROLE,
450
+ createdAt: Date.now(),
451
+ })
452
+
453
+ const deletedCount = await service.deleteOlderThan(30)
454
+
455
+ expect(deletedCount).toBe(0)
456
+ })
457
+
458
+ it('should return 0 when no logs exist', async () => {
459
+ const deletedCount = await service.deleteOlderThan(30)
460
+
461
+ expect(deletedCount).toBe(0)
462
+ })
463
+ })
464
+ })