create-fullstack-scaffold 0.4.25 → 0.5.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.
- package/dist/cli/index.js +432 -15
- package/dist/cli/index.js.map +1 -1
- package/package.json +10 -13
- package/template/drizzle.config.ts +10 -4
- package/template/eslint-rules/__tests__/no-merged-api-type-export.test.ts +96 -0
- package/template/eslint-rules/no-cross-module-service-import.js +4 -0
- package/template/eslint-rules/no-merged-api-type-export.js +190 -0
- package/template/eslint.config.js +3 -0
- package/template/package.json +9 -6
- package/template/scripts/sync-agent-hooks.mjs +189 -0
- package/template/src/admin/components/__tests__/StatsCard.test.tsx +2 -2
- package/template/src/admin/pages/ContentPage.tsx +1 -1
- package/template/src/admin/pages/DisputesPage.tsx +1 -1
- package/template/src/admin/pages/OrdersPage.tsx +1 -1
- package/template/src/admin/pages/TicketsPage.tsx +1 -1
- package/template/src/admin/pages/__tests__/ContentPage.test.tsx +5 -1
- package/template/src/admin/pages/__tests__/DashboardPage.test.tsx +62 -8
- package/template/src/admin/pages/__tests__/DisputesPage.test.tsx +8 -1
- package/template/src/admin/pages/__tests__/OrdersPage.test.tsx +4 -2
- package/template/src/admin/pages/__tests__/RegisterPage.test.tsx +40 -43
- package/template/src/admin/pages/__tests__/SettingsPage.test.tsx +83 -21
- package/template/src/admin/pages/__tests__/TicketsPage.test.tsx +3 -1
- package/template/src/admin/services/apiClient.ts +2 -3
- package/template/src/cli/modules/content/index.ts +3 -3
- package/template/src/cli/modules/dispute/index.ts +3 -3
- package/template/src/cli/modules/ticket/index.ts +3 -3
- package/template/src/cli/modules/todo/index.ts +1 -1
- package/template/src/cli/rpc/client.ts +3 -4
- package/template/src/cli/rpc/index.ts +1 -1
- package/template/src/client/App.tsx +3 -39
- package/template/src/client/AppRoutes.tsx +53 -0
- package/template/src/client/entry-server.tsx +75 -0
- package/template/src/client/main.tsx +3 -1
- package/template/src/client/pages/SearchPage.tsx +1 -1
- package/template/src/client/services/apiClient.ts +12 -4
- package/template/src/client/stores/__tests__/todoStore.test.ts +12 -3
- package/template/src/client/stores/entry-stores.ts +36 -0
- package/template/src/client/stores/notificationStore.ts +4 -4
- package/template/src/client/stores/todoStore.ts +2 -2
- package/template/src/merchant/pages/DisputesPage.tsx +3 -3
- package/template/src/merchant/pages/OrdersPage.tsx +1 -1
- package/template/src/merchant/pages/ProductsPage.tsx +1 -1
- package/template/src/merchant/pages/SettingsPage.tsx +1 -1
- package/template/src/server/__tests__/integration/isr-full-flow.test.ts +251 -0
- package/template/src/server/__tests__/integration/todos-api.test.ts +7 -4
- package/template/src/server/app.ts +6 -4
- package/template/src/server/core/__tests__/isr-cache-cf.test.ts +96 -0
- package/template/src/server/core/__tests__/isr-cache.test.ts +96 -92
- package/template/src/server/core/__tests__/isr-invalidation.test.ts +29 -4
- package/template/src/server/core/__tests__/isr-registry.test.ts +215 -0
- package/template/src/server/core/__tests__/isr-renderer.test.ts +121 -0
- package/template/src/server/core/isr-cache.ts +61 -18
- package/template/src/server/core/isr-invalidation.ts +6 -12
- package/template/src/server/core/isr-registry.ts +104 -0
- package/template/src/server/core/isr-renderer.ts +75 -0
- package/template/src/server/db/schema/contents.ts +28 -20
- package/template/src/server/db/schema/disputes.ts +30 -22
- package/template/src/server/db/schema/notifications.ts +20 -14
- package/template/src/server/db/schema/orders.ts +23 -16
- package/template/src/server/db/schema/plugins.ts +56 -38
- package/template/src/server/db/schema/products.ts +24 -17
- package/template/src/server/db/schema/tickets.ts +44 -31
- package/template/src/server/db/schema/todos.ts +26 -18
- package/template/src/server/entries/cloudflare.ts +97 -21
- package/template/src/server/entries/node.ts +0 -2
- package/template/src/server/index.ts +0 -1
- package/template/src/server/isr-modules.ts +10 -0
- package/template/src/server/module-admin/__tests__/admin-service.test.ts +36 -12
- package/template/src/server/module-admin/routes/admin-notification-routes.ts +2 -1
- package/template/src/server/module-admin/routes/admin-routes.ts +3 -0
- package/template/src/server/module-admin/routes/dashboard-routes.ts +3 -0
- package/template/src/server/module-admin/services/admin-service.ts +57 -13
- package/template/src/server/module-auth/routes/auth-routes.ts +3 -0
- package/template/src/server/module-auth/routes/profile-routes.ts +3 -0
- package/template/src/server/module-captcha/routes/captcha-routes.ts +3 -0
- package/template/src/server/module-chat/routes/chat-routes.ts +4 -1
- package/template/src/server/module-content/__tests__/content-route.test.ts +2 -1
- package/template/src/server/module-content/__tests__/content-service.test.ts +2 -1
- package/template/src/server/module-content/__tests__/isr.test.ts +138 -0
- package/template/src/server/module-content/isr.ts +63 -0
- package/template/src/server/module-content/routes/content-routes.ts +10 -10
- package/template/src/server/module-content/routes/public-content-routes.ts +8 -4
- package/template/src/server/module-content/routes/topics-routes.ts +3 -0
- package/template/src/server/module-content/services/content-service.ts +23 -10
- package/template/src/server/module-dispute/__tests__/dispute-route.test.ts +2 -1
- package/template/src/server/module-dispute/__tests__/dispute-service.test.ts +2 -1
- package/template/src/server/module-dispute/routes/dispute-routes.ts +11 -10
- package/template/src/server/module-dispute/services/dispute-service.ts +15 -3
- package/template/src/server/module-file/routes/file-routes.ts +3 -0
- package/template/src/server/module-merchant/routes/merchant-routes.ts +3 -0
- package/template/src/server/module-merchant/services/merchant-service.ts +8 -8
- package/template/src/server/module-notifications/routes/notification-routes.ts +5 -1
- package/template/src/server/module-order/__tests__/order-route.test.ts +8 -8
- package/template/src/server/module-order/__tests__/order-service.test.ts +4 -3
- package/template/src/server/module-order/routes/cart-routes.ts +3 -0
- package/template/src/server/module-order/routes/order-routes.ts +9 -4
- package/template/src/server/module-order/routes/orders-mock-routes.ts +3 -0
- package/template/src/server/module-order/services/order-service.ts +25 -13
- package/template/src/server/module-permission/__tests__/audit-log-service.test.ts +464 -0
- package/template/src/server/module-permission/__tests__/role-service.test.ts +348 -0
- package/template/src/server/module-permission/routes/audit-log-routes.ts +3 -0
- package/template/src/server/module-permission/routes/permission-routes.ts +3 -0
- package/template/src/server/module-permission/routes/role-routes.ts +3 -0
- package/template/src/server/module-plugin/routes/plugin-admin-routes.ts +3 -0
- package/template/src/server/module-plugin/routes/plugin-routes.ts +3 -0
- package/template/src/server/module-plugin/services/admin-plugin-service.ts +10 -18
- package/template/src/server/module-plugin/services/admin-stats-service.ts +28 -9
- package/template/src/server/module-plugin/services/plugin-query-service.ts +42 -66
- package/template/src/server/module-tenant/routes/tenant-routes.ts +5 -1
- package/template/src/server/module-ticket/__tests__/ticket-route.test.ts +2 -1
- package/template/src/server/module-ticket/__tests__/ticket-service.test.ts +4 -3
- package/template/src/server/module-ticket/routes/ticket-routes.ts +11 -10
- package/template/src/server/module-ticket/services/ticket-service.ts +16 -5
- package/template/src/server/module-todos/__tests__/isr.test.ts +105 -0
- package/template/src/server/module-todos/__tests__/todo-service.test.ts +12 -9
- package/template/src/server/module-todos/__tests__/todos-route-rpc.test.ts +6 -6
- package/template/src/server/module-todos/isr.ts +41 -0
- package/template/src/server/module-todos/routes/todos-routes.ts +12 -5
- package/template/src/server/module-todos/services/todo-service.ts +31 -10
- package/template/src/server/route-registry.ts +0 -4
- package/template/src/server/rpc-merge.ts +27 -0
- package/template/src/server/rpc-surface.ts +117 -0
- package/template/src/server/rpc-type-canary.ts +80 -0
- package/template/src/server/test-utils/test-client.ts +7 -9
- package/template/src/server/test-utils/test-isr-helper.ts +140 -0
- package/template/src/shared/modules/content/schemas.ts +14 -0
- package/template/src/shared/modules/dispute/schemas.ts +14 -0
- package/template/src/shared/modules/order/schemas.ts +9 -1
- package/template/src/shared/modules/ticket/schemas.ts +14 -0
- package/template/src/shared/modules/todos/index.ts +4 -0
- package/template/src/shared/modules/todos/schemas.ts +14 -0
- package/template/src/shared/schemas/index.ts +18 -0
- package/template/tsup.config.ts +48 -1
- package/template/vitest.config.ts +19 -3
- package/template/vitest.setup.ts +68 -5
- package/template/patches/typescript+5.9.3.patch +0 -24
- /package/template/patches/{hono+4.12.16.patch → hono+4.12.34.patch} +0 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { RoleService } from '../services/role-service'
|
|
3
|
+
import { getRawClient, getDb } from '@server/db'
|
|
4
|
+
import { setupTestDatabase, cleanupTestDatabase } from '@server/db/test-setup'
|
|
5
|
+
|
|
6
|
+
describe('Role Service', () => {
|
|
7
|
+
let service: RoleService
|
|
8
|
+
|
|
9
|
+
beforeAll(async () => {
|
|
10
|
+
await setupTestDatabase()
|
|
11
|
+
const db = await getDb()
|
|
12
|
+
expect(db).toBeDefined()
|
|
13
|
+
service = new RoleService()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
afterAll(async () => {
|
|
17
|
+
await cleanupTestDatabase()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
const client = await getRawClient()
|
|
22
|
+
if (client && 'execute' in client) {
|
|
23
|
+
await client.execute('DELETE FROM user_roles')
|
|
24
|
+
await client.execute('DELETE FROM role_permissions')
|
|
25
|
+
await client.execute('DELETE FROM roles')
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
afterEach(async () => {
|
|
30
|
+
const client = await getRawClient()
|
|
31
|
+
if (client && 'execute' in client) {
|
|
32
|
+
await client.execute('DELETE FROM user_roles')
|
|
33
|
+
await client.execute('DELETE FROM role_permissions')
|
|
34
|
+
await client.execute('DELETE FROM roles')
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
async function insertTestRole(overrides: {
|
|
39
|
+
id: string
|
|
40
|
+
code: string
|
|
41
|
+
name: string
|
|
42
|
+
label: string
|
|
43
|
+
isSystem?: boolean
|
|
44
|
+
isActive?: boolean
|
|
45
|
+
}) {
|
|
46
|
+
const client = await getRawClient()
|
|
47
|
+
if (client && 'execute' in client) {
|
|
48
|
+
await client.execute({
|
|
49
|
+
sql: `INSERT INTO roles (id, code, name, label, is_system, is_active, sort_order, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
50
|
+
args: [
|
|
51
|
+
overrides.id,
|
|
52
|
+
overrides.code,
|
|
53
|
+
overrides.name,
|
|
54
|
+
overrides.label,
|
|
55
|
+
overrides.isSystem ? 1 : 0,
|
|
56
|
+
overrides.isActive !== false ? 1 : 0,
|
|
57
|
+
0,
|
|
58
|
+
Date.now(),
|
|
59
|
+
Date.now(),
|
|
60
|
+
],
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe('getAll', () => {
|
|
66
|
+
it('should return empty array when no roles exist', async () => {
|
|
67
|
+
const result = await service.getAll()
|
|
68
|
+
|
|
69
|
+
expect(result).toEqual([])
|
|
70
|
+
expect(Array.isArray(result)).toBe(true)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('should return all roles including inactive ones', async () => {
|
|
74
|
+
await insertTestRole({ id: 'r1', code: 'admin', name: 'Admin', label: 'Admin' })
|
|
75
|
+
await insertTestRole({
|
|
76
|
+
id: 'r2',
|
|
77
|
+
code: 'guest',
|
|
78
|
+
name: 'Guest',
|
|
79
|
+
label: 'Guest',
|
|
80
|
+
isActive: false,
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const result = await service.getAll()
|
|
84
|
+
|
|
85
|
+
expect(result).toHaveLength(2)
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe('getActive', () => {
|
|
90
|
+
it('should return only active roles', async () => {
|
|
91
|
+
await insertTestRole({ id: 'r1', code: 'admin', name: 'Admin', label: 'Admin' })
|
|
92
|
+
await insertTestRole({
|
|
93
|
+
id: 'r2',
|
|
94
|
+
code: 'guest',
|
|
95
|
+
name: 'Guest',
|
|
96
|
+
label: 'Guest',
|
|
97
|
+
isActive: false,
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const result = await service.getActive()
|
|
101
|
+
|
|
102
|
+
expect(result).toHaveLength(1)
|
|
103
|
+
expect(result[0].id).toBe('r1')
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe('getById', () => {
|
|
108
|
+
it('should return undefined for non-existent role', async () => {
|
|
109
|
+
const result = await service.getById('non-existent')
|
|
110
|
+
|
|
111
|
+
expect(result).toBeUndefined()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('should return role by id with transformed dates', async () => {
|
|
115
|
+
await insertTestRole({ id: 'r1', code: 'admin', name: 'Admin', label: 'Admin' })
|
|
116
|
+
|
|
117
|
+
const result = await service.getById('r1')
|
|
118
|
+
|
|
119
|
+
expect(result).toBeDefined()
|
|
120
|
+
expect(result?.id).toBe('r1')
|
|
121
|
+
expect(result?.code).toBe('admin')
|
|
122
|
+
expect(result?.name).toBe('Admin')
|
|
123
|
+
expect(typeof result?.createdAt).toBe('string')
|
|
124
|
+
expect(typeof result?.updatedAt).toBe('string')
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
describe('getByCode', () => {
|
|
129
|
+
it('should return undefined for non-existent code', async () => {
|
|
130
|
+
const result = await service.getByCode('non_existent')
|
|
131
|
+
|
|
132
|
+
expect(result).toBeUndefined()
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('should return role by code', async () => {
|
|
136
|
+
await insertTestRole({ id: 'r1', code: 'admin', name: 'Admin', label: 'Admin' })
|
|
137
|
+
|
|
138
|
+
const result = await service.getByCode('admin')
|
|
139
|
+
|
|
140
|
+
expect(result).toBeDefined()
|
|
141
|
+
expect(result?.code).toBe('admin')
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
describe('create', () => {
|
|
146
|
+
it('should create a new role with all fields', async () => {
|
|
147
|
+
const data = {
|
|
148
|
+
id: 'role_test_1',
|
|
149
|
+
code: 'test_role',
|
|
150
|
+
name: 'Test Role',
|
|
151
|
+
label: 'Test Role Label',
|
|
152
|
+
description: 'A test role',
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const result = await service.create(data)
|
|
156
|
+
|
|
157
|
+
expect(result.id).toBe('role_test_1')
|
|
158
|
+
expect(result.code).toBe('test_role')
|
|
159
|
+
expect(result.name).toBe('Test Role')
|
|
160
|
+
expect(result.label).toBe('Test Role Label')
|
|
161
|
+
expect(result.description).toBe('A test role')
|
|
162
|
+
expect(typeof result.createdAt).toBe('string')
|
|
163
|
+
expect(typeof result.updatedAt).toBe('string')
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('should create role without optional fields', async () => {
|
|
167
|
+
const data = {
|
|
168
|
+
id: 'role_minimal',
|
|
169
|
+
code: 'minimal',
|
|
170
|
+
name: 'Minimal',
|
|
171
|
+
label: 'Minimal',
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const result = await service.create(data)
|
|
175
|
+
|
|
176
|
+
expect(result.id).toBe('role_minimal')
|
|
177
|
+
expect(result.code).toBe('minimal')
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
describe('update', () => {
|
|
182
|
+
it('should update role name and label', async () => {
|
|
183
|
+
await insertTestRole({ id: 'r1', code: 'admin', name: 'Admin', label: 'Admin' })
|
|
184
|
+
|
|
185
|
+
const result = await service.update('r1', { name: 'Super Admin', label: 'Super Admin' })
|
|
186
|
+
|
|
187
|
+
expect(result).toBeDefined()
|
|
188
|
+
expect(result?.name).toBe('Super Admin')
|
|
189
|
+
expect(result?.label).toBe('Super Admin')
|
|
190
|
+
expect(result?.code).toBe('admin')
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('should return undefined for non-existent role', async () => {
|
|
194
|
+
const result = await service.update('non-existent', { name: 'Updated' })
|
|
195
|
+
|
|
196
|
+
expect(result).toBeUndefined()
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('should update updatedAt timestamp', async () => {
|
|
200
|
+
const client = await getRawClient()
|
|
201
|
+
if (client && 'execute' in client) {
|
|
202
|
+
await insertTestRole({ id: 'r1', code: 'admin', name: 'Admin', label: 'Admin' })
|
|
203
|
+
await service.getById('r1')
|
|
204
|
+
|
|
205
|
+
const result = await service.update('r1', { name: 'Updated Admin' })
|
|
206
|
+
|
|
207
|
+
expect(result).toBeDefined()
|
|
208
|
+
expect(result!.updatedAt).toBeDefined()
|
|
209
|
+
expect(typeof result!.updatedAt).toBe('string')
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
describe('delete', () => {
|
|
215
|
+
it('should soft-delete a non-system role', async () => {
|
|
216
|
+
await insertTestRole({ id: 'r1', code: 'custom', name: 'Custom', label: 'Custom' })
|
|
217
|
+
|
|
218
|
+
const result = await service.delete('r1')
|
|
219
|
+
|
|
220
|
+
expect(result).toBe(true)
|
|
221
|
+
|
|
222
|
+
const role = await service.getById('r1')
|
|
223
|
+
expect(role?.isActive).toBe(false)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('should return false for non-existent role', async () => {
|
|
227
|
+
const result = await service.delete('non-existent')
|
|
228
|
+
|
|
229
|
+
expect(result).toBe(false)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('should return false for system role', async () => {
|
|
233
|
+
await insertTestRole({
|
|
234
|
+
id: 'r1',
|
|
235
|
+
code: 'admin',
|
|
236
|
+
name: 'Admin',
|
|
237
|
+
label: 'Admin',
|
|
238
|
+
isSystem: true,
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
const result = await service.delete('r1')
|
|
242
|
+
|
|
243
|
+
expect(result).toBe(false)
|
|
244
|
+
|
|
245
|
+
const role = await service.getById('r1')
|
|
246
|
+
expect(role?.isActive).toBe(true)
|
|
247
|
+
})
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
describe('assignRoleToUser', () => {
|
|
251
|
+
it('should assign a role to a user', async () => {
|
|
252
|
+
await insertTestRole({ id: 'r1', code: 'admin', name: 'Admin', label: 'Admin' })
|
|
253
|
+
|
|
254
|
+
await service.assignRoleToUser('user-1', 'r1')
|
|
255
|
+
|
|
256
|
+
const roles = await service.getUserRoles('user-1')
|
|
257
|
+
expect(roles).toHaveLength(1)
|
|
258
|
+
expect(roles[0].id).toBe('r1')
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('should not duplicate assignment if already assigned', async () => {
|
|
262
|
+
await insertTestRole({ id: 'r1', code: 'admin', name: 'Admin', label: 'Admin' })
|
|
263
|
+
|
|
264
|
+
await service.assignRoleToUser('user-1', 'r1')
|
|
265
|
+
await service.assignRoleToUser('user-1', 'r1')
|
|
266
|
+
|
|
267
|
+
const roles = await service.getUserRoles('user-1')
|
|
268
|
+
expect(roles).toHaveLength(1)
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it('should record assignedBy', async () => {
|
|
272
|
+
await insertTestRole({ id: 'r1', code: 'admin', name: 'Admin', label: 'Admin' })
|
|
273
|
+
|
|
274
|
+
await service.assignRoleToUser('user-1', 'r1', 'admin-user')
|
|
275
|
+
|
|
276
|
+
const client = await getRawClient()
|
|
277
|
+
if (client && 'execute' in client) {
|
|
278
|
+
const result = await client.execute(
|
|
279
|
+
'SELECT assigned_by FROM user_roles WHERE user_id = ? AND role_id = ?',
|
|
280
|
+
['user-1', 'r1']
|
|
281
|
+
)
|
|
282
|
+
const row = result.rows[0] as unknown as { assigned_by: string | null }
|
|
283
|
+
expect(row.assigned_by).toBe('admin-user')
|
|
284
|
+
}
|
|
285
|
+
})
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
describe('revokeRoleFromUser', () => {
|
|
289
|
+
it('should revoke a role from a user', async () => {
|
|
290
|
+
await insertTestRole({ id: 'r1', code: 'admin', name: 'Admin', label: 'Admin' })
|
|
291
|
+
await service.assignRoleToUser('user-1', 'r1')
|
|
292
|
+
|
|
293
|
+
await service.revokeRoleFromUser('user-1', 'r1')
|
|
294
|
+
|
|
295
|
+
const roles = await service.getUserRoles('user-1')
|
|
296
|
+
expect(roles).toHaveLength(0)
|
|
297
|
+
})
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
describe('getUserRoles', () => {
|
|
301
|
+
it('should return empty array for user with no roles', async () => {
|
|
302
|
+
const result = await service.getUserRoles('unknown-user')
|
|
303
|
+
|
|
304
|
+
expect(result).toEqual([])
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
it('should return all active roles for a user', async () => {
|
|
308
|
+
await insertTestRole({ id: 'r1', code: 'admin', name: 'Admin', label: 'Admin' })
|
|
309
|
+
await insertTestRole({ id: 'r2', code: 'editor', name: 'Editor', label: 'Editor' })
|
|
310
|
+
|
|
311
|
+
await service.assignRoleToUser('user-1', 'r1')
|
|
312
|
+
await new Promise(resolve => setTimeout(resolve, 10))
|
|
313
|
+
await service.assignRoleToUser('user-1', 'r2')
|
|
314
|
+
|
|
315
|
+
const result = await service.getUserRoles('user-1')
|
|
316
|
+
|
|
317
|
+
expect(result).toHaveLength(2)
|
|
318
|
+
const ids = result.map(r => r.id)
|
|
319
|
+
expect(ids).toContain('r1')
|
|
320
|
+
expect(ids).toContain('r2')
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
it('should not return inactive roles', async () => {
|
|
324
|
+
await insertTestRole({ id: 'r1', code: 'admin', name: 'Admin', label: 'Admin' })
|
|
325
|
+
await insertTestRole({
|
|
326
|
+
id: 'r2',
|
|
327
|
+
code: 'inactive_role',
|
|
328
|
+
name: 'Inactive',
|
|
329
|
+
label: 'Inactive',
|
|
330
|
+
isActive: false,
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
await service.assignRoleToUser('user-1', 'r1')
|
|
334
|
+
const client = await getRawClient()
|
|
335
|
+
if (client && 'execute' in client) {
|
|
336
|
+
await client.execute({
|
|
337
|
+
sql: `INSERT INTO user_roles (id, user_id, role_id, is_active, assigned_at) VALUES (?, ?, ?, ?, ?)`,
|
|
338
|
+
args: ['ur_inactive', 'user-1', 'r2', 1, Date.now()],
|
|
339
|
+
})
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const result = await service.getUserRoles('user-1')
|
|
343
|
+
|
|
344
|
+
expect(result).toHaveLength(1)
|
|
345
|
+
expect(result[0].id).toBe('r1')
|
|
346
|
+
})
|
|
347
|
+
})
|
|
348
|
+
})
|
|
@@ -259,3 +259,6 @@ export const pluginAdminRoutes = new OpenAPIHono<{ Variables: { authUser: AuthUs
|
|
|
259
259
|
await adminCategoryService.deleteCategory(id)
|
|
260
260
|
return c.json(success({ id }), 200)
|
|
261
261
|
})
|
|
262
|
+
|
|
263
|
+
/** 模块级窄类型(深度 = 1 个模块)— 供 rpc-surface 门面使用,禁止再向上合并 */
|
|
264
|
+
export type PluginAdminApiType = typeof pluginAdminRoutes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { eq, desc } from 'drizzle-orm'
|
|
2
2
|
import type { Plugin } from '@shared/schemas'
|
|
3
|
-
import { getDb
|
|
3
|
+
import { getDb } from '@server/db'
|
|
4
4
|
import { plugins } from '@server/db/schema'
|
|
5
5
|
import { NotFoundError } from '@server/utils/app-error'
|
|
6
6
|
import { mapRow } from './plugin-service'
|
|
@@ -27,14 +27,8 @@ export async function listPending(
|
|
|
27
27
|
.limit(limit)
|
|
28
28
|
.offset(offset)
|
|
29
29
|
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
if (client && 'execute' in client) {
|
|
33
|
-
const result = await client.execute(
|
|
34
|
-
"SELECT COUNT(*) as count FROM plugins WHERE status = 'pending'"
|
|
35
|
-
)
|
|
36
|
-
total = (result.rows[0] as unknown as { count: number })?.count ?? 0
|
|
37
|
-
}
|
|
30
|
+
const allPending = await db.select().from(plugins).where(eq(plugins.status, 'pending'))
|
|
31
|
+
const total = allPending.length
|
|
38
32
|
|
|
39
33
|
return { plugins: rows.map(mapRow), total, page, limit }
|
|
40
34
|
}
|
|
@@ -120,15 +114,13 @@ export async function listAllPlugins(
|
|
|
120
114
|
.offset(offset)
|
|
121
115
|
: await db.select().from(plugins).orderBy(desc(plugins.createdAt)).limit(limit).offset(offset)
|
|
122
116
|
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
total = (result.rows[0] as unknown as { count: number })?.count ?? 0
|
|
131
|
-
}
|
|
117
|
+
const allRows = options.status
|
|
118
|
+
? await db
|
|
119
|
+
.select()
|
|
120
|
+
.from(plugins)
|
|
121
|
+
.where(eq(plugins.status, options.status as 'pending' | 'approved' | 'rejected'))
|
|
122
|
+
: await db.select().from(plugins)
|
|
123
|
+
const total = allRows.length
|
|
132
124
|
|
|
133
125
|
return { plugins: rows.map(mapRow), total, page, limit }
|
|
134
126
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { getRawClient } from '@server/db'
|
|
2
|
-
import {
|
|
3
|
-
import { plugins } from '@server/db/schema'
|
|
1
|
+
import { getRawClient, getDb } from '@server/db'
|
|
2
|
+
import { plugins, pluginCategories } from '@server/db/schema'
|
|
4
3
|
import type { AdminDashboardStats } from '@shared/schemas'
|
|
5
4
|
import { mapRow } from './plugin-service'
|
|
6
5
|
|
|
@@ -8,12 +7,32 @@ export async function getDashboardStats(): Promise<AdminDashboardStats> {
|
|
|
8
7
|
const client = await getRawClient()
|
|
9
8
|
|
|
10
9
|
if (!client || !('execute' in client)) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
const db = await getDb()
|
|
11
|
+
try {
|
|
12
|
+
const allPlugins = await db.select().from(plugins)
|
|
13
|
+
const totalPlugins = allPlugins.length
|
|
14
|
+
const pendingPlugins = allPlugins.filter(p => p.status === 'pending').length
|
|
15
|
+
const totalDownloads = allPlugins.reduce((sum, p) => sum + (p.downloadCount ?? 0), 0)
|
|
16
|
+
const uniqueDevelopers = new Set(allPlugins.map(p => p.authorId)).size
|
|
17
|
+
const allCategories = await db.select().from(pluginCategories)
|
|
18
|
+
const recentRows = await db.select().from(plugins).orderBy(plugins.createdAt).limit(5)
|
|
19
|
+
const recentSubmissions = recentRows.map(mapRow)
|
|
20
|
+
return {
|
|
21
|
+
totalPlugins,
|
|
22
|
+
pendingPlugins,
|
|
23
|
+
totalDownloads,
|
|
24
|
+
totalDevelopers: uniqueDevelopers,
|
|
25
|
+
totalCategories: allCategories.length,
|
|
26
|
+
recentSubmissions,
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
return {
|
|
30
|
+
totalPlugins: 42,
|
|
31
|
+
pendingPlugins: 5,
|
|
32
|
+
totalDownloads: 15820,
|
|
33
|
+
totalDevelopers: 18,
|
|
34
|
+
totalCategories: 8,
|
|
35
|
+
}
|
|
17
36
|
}
|
|
18
37
|
}
|
|
19
38
|
|
|
@@ -6,7 +6,7 @@ import type {
|
|
|
6
6
|
MarketplaceStats,
|
|
7
7
|
PluginListResponse,
|
|
8
8
|
} from '@shared/schemas'
|
|
9
|
-
import { getDb
|
|
9
|
+
import { getDb } from '@server/db'
|
|
10
10
|
import {
|
|
11
11
|
plugins,
|
|
12
12
|
pluginVersions,
|
|
@@ -41,17 +41,16 @@ function mapCategoryRow(row: typeof pluginCategories.$inferSelect): Category {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
async function
|
|
44
|
+
async function getPluginsCount(status?: string | null): Promise<number> {
|
|
45
45
|
try {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
} catch {
|
|
46
|
+
const db = await getDb()
|
|
47
|
+
const rows = await db.select().from(plugins)
|
|
48
|
+
if (status) {
|
|
49
|
+
return rows.filter(r => r.status === status).length
|
|
50
|
+
}
|
|
51
|
+
return rows.length
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error('[PluginQueryService] getPluginCount failed:', error)
|
|
55
54
|
return 0
|
|
56
55
|
}
|
|
57
56
|
}
|
|
@@ -105,10 +104,7 @@ export async function listPlugins(options: ListOptions = {}): Promise<PluginList
|
|
|
105
104
|
.orderBy(orderExpr)
|
|
106
105
|
.limit(limit)
|
|
107
106
|
.offset(offset)
|
|
108
|
-
const total = await
|
|
109
|
-
'plugins',
|
|
110
|
-
options.status ? `status = '${options.status}'` : "status = 'approved'"
|
|
111
|
-
)
|
|
107
|
+
const total = await getPluginsCount(options.status ?? 'approved')
|
|
112
108
|
|
|
113
109
|
return {
|
|
114
110
|
plugins: rows.map(mapRow),
|
|
@@ -116,7 +112,8 @@ export async function listPlugins(options: ListOptions = {}): Promise<PluginList
|
|
|
116
112
|
page,
|
|
117
113
|
limit,
|
|
118
114
|
}
|
|
119
|
-
} catch {
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error('[PluginQueryService] listPlugins failed:', error)
|
|
120
117
|
return { plugins: [], total: 0, page: options.page ?? 1, limit: options.limit ?? 20 }
|
|
121
118
|
}
|
|
122
119
|
}
|
|
@@ -174,18 +171,10 @@ export async function searchPlugins(
|
|
|
174
171
|
.limit(limit)
|
|
175
172
|
.offset(offset)
|
|
176
173
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const escapedQuery = query.replace(/'/g, "''")
|
|
182
|
-
const result = await client.execute(
|
|
183
|
-
`SELECT COUNT(*) as count FROM plugins WHERE status = 'approved' AND (name LIKE '%${escapedQuery}%' OR description LIKE '%${escapedQuery}%' OR tags LIKE '%${escapedQuery}%' OR author_name LIKE '%${escapedQuery}%')`
|
|
184
|
-
)
|
|
185
|
-
total = (result.rows[0] as unknown as { count: number })?.count ?? 0
|
|
186
|
-
} catch {
|
|
187
|
-
total = 0
|
|
188
|
-
}
|
|
174
|
+
let total = rows.length
|
|
175
|
+
if (rows.length === limit) {
|
|
176
|
+
const allMatching = await db.select().from(plugins).where(where)
|
|
177
|
+
total = allMatching.length
|
|
189
178
|
}
|
|
190
179
|
|
|
191
180
|
return {
|
|
@@ -194,7 +183,8 @@ export async function searchPlugins(
|
|
|
194
183
|
page,
|
|
195
184
|
limit,
|
|
196
185
|
}
|
|
197
|
-
} catch {
|
|
186
|
+
} catch (error) {
|
|
187
|
+
console.error('[PluginQueryService] searchPlugins failed:', error)
|
|
198
188
|
return { plugins: [], total: 0, page: options.page ?? 1, limit: options.limit ?? 20 }
|
|
199
189
|
}
|
|
200
190
|
}
|
|
@@ -213,8 +203,8 @@ export async function getPluginBySlug(slug: string): Promise<Plugin> {
|
|
|
213
203
|
.update(plugins)
|
|
214
204
|
.set({ viewCount: rows[0].viewCount + 1 })
|
|
215
205
|
.where(eq(plugins.slug, slug))
|
|
216
|
-
} catch {
|
|
217
|
-
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.error('[PluginQueryService] viewCount update failed:', error)
|
|
218
208
|
}
|
|
219
209
|
|
|
220
210
|
return mapRow(rows[0])
|
|
@@ -233,7 +223,8 @@ export async function getVersions(pluginId: string): Promise<Version[]> {
|
|
|
233
223
|
.where(eq(pluginVersions.pluginId, pluginId))
|
|
234
224
|
.orderBy(desc(pluginVersions.publishedAt))
|
|
235
225
|
return rows.map(mapVersionRow)
|
|
236
|
-
} catch {
|
|
226
|
+
} catch (error) {
|
|
227
|
+
console.error('[PluginQueryService] getPluginVersions failed:', error)
|
|
237
228
|
return []
|
|
238
229
|
}
|
|
239
230
|
}
|
|
@@ -243,7 +234,8 @@ export async function listCategories(): Promise<Category[]> {
|
|
|
243
234
|
const db = await getDb()
|
|
244
235
|
const rows = await db.select().from(pluginCategories).orderBy(asc(pluginCategories.sortOrder))
|
|
245
236
|
return rows.map(mapCategoryRow)
|
|
246
|
-
} catch {
|
|
237
|
+
} catch (error) {
|
|
238
|
+
console.error('[PluginQueryService] listCategories failed:', error)
|
|
247
239
|
return []
|
|
248
240
|
}
|
|
249
241
|
}
|
|
@@ -293,19 +285,8 @@ export async function getPluginsByCategory(
|
|
|
293
285
|
.limit(limit)
|
|
294
286
|
.offset(offset)
|
|
295
287
|
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
if (client && 'execute' in client) {
|
|
299
|
-
try {
|
|
300
|
-
const idsList = ids.map(id => `'${id}'`).join(',')
|
|
301
|
-
const result = await client.execute(
|
|
302
|
-
`SELECT COUNT(*) as count FROM plugins WHERE status = 'approved' AND id IN (${idsList})`
|
|
303
|
-
)
|
|
304
|
-
total = (result.rows[0] as unknown as { count: number })?.count ?? 0
|
|
305
|
-
} catch {
|
|
306
|
-
total = 0
|
|
307
|
-
}
|
|
308
|
-
}
|
|
288
|
+
const allMatchingRows = await db.select().from(plugins).where(where)
|
|
289
|
+
const total = allMatchingRows.length
|
|
309
290
|
|
|
310
291
|
return {
|
|
311
292
|
plugins: rows.map(mapRow),
|
|
@@ -328,33 +309,28 @@ export async function listMyPlugins(userId: string): Promise<Plugin[]> {
|
|
|
328
309
|
.where(eq(plugins.authorId, userId))
|
|
329
310
|
.orderBy(desc(plugins.createdAt))
|
|
330
311
|
return rows.map(mapRow)
|
|
331
|
-
} catch {
|
|
312
|
+
} catch (error) {
|
|
313
|
+
console.error('[PluginQueryService] getRecentPlugins failed:', error)
|
|
332
314
|
return []
|
|
333
315
|
}
|
|
334
316
|
}
|
|
335
317
|
|
|
336
318
|
export async function getStats(): Promise<MarketplaceStats> {
|
|
337
319
|
try {
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
const [totalResult, downloadResult, developerResult, categoryResult] = await Promise.all([
|
|
345
|
-
client.execute("SELECT COUNT(*) as count FROM plugins WHERE status = 'approved'"),
|
|
346
|
-
client.execute('SELECT COALESCE(SUM(download_count), 0) as total FROM plugins'),
|
|
347
|
-
client.execute('SELECT COUNT(DISTINCT author_id) as count FROM plugins'),
|
|
348
|
-
client.execute('SELECT COUNT(*) as count FROM plugin_categories'),
|
|
349
|
-
])
|
|
350
|
-
|
|
320
|
+
const db = await getDb()
|
|
321
|
+
const allPlugins = await db.select().from(plugins)
|
|
322
|
+
const approved = allPlugins.filter(p => p.status === 'approved')
|
|
323
|
+
const totalDownloads = allPlugins.reduce((sum, p) => sum + (p.downloadCount ?? 0), 0)
|
|
324
|
+
const uniqueDevelopers = new Set(allPlugins.map(p => p.authorId)).size
|
|
325
|
+
const allCategories = await db.select().from(pluginCategories)
|
|
351
326
|
return {
|
|
352
|
-
totalPlugins:
|
|
353
|
-
totalDownloads
|
|
354
|
-
totalDevelopers:
|
|
355
|
-
totalCategories:
|
|
327
|
+
totalPlugins: approved.length,
|
|
328
|
+
totalDownloads,
|
|
329
|
+
totalDevelopers: uniqueDevelopers,
|
|
330
|
+
totalCategories: allCategories.length,
|
|
356
331
|
}
|
|
357
|
-
} catch {
|
|
358
|
-
|
|
332
|
+
} catch (error) {
|
|
333
|
+
console.error('[PluginQueryService] getStats failed:', error)
|
|
334
|
+
return { totalPlugins: 42, totalDownloads: 15820, totalDevelopers: 18, totalCategories: 8 }
|
|
359
335
|
}
|
|
360
336
|
}
|