create-fullstack-scaffold 0.1.1 → 0.2.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 (155) hide show
  1. package/dist/cli/index.js +1673 -696
  2. package/dist/cli/index.js.map +1 -1
  3. package/package.json +16 -10
  4. package/template/.husky/pre-commit +1 -1
  5. package/template/modules.config.ts +29 -2
  6. package/template/package.json +12 -12
  7. package/template/patches/{typescript+5.8.3.patch → typescript+5.9.3.patch} +2 -4
  8. package/template/playwright.config.ts +7 -1
  9. package/template/src/admin/App.tsx +2 -2
  10. package/template/src/admin/components/CaptchaModal.tsx +7 -9
  11. package/template/src/admin/layouts/Header.tsx +56 -22
  12. package/template/src/admin/layouts/Layout.tsx +14 -9
  13. package/template/src/admin/layouts/Sidebar.tsx +122 -105
  14. package/template/src/admin/pages/CategoryManagementPage.tsx +241 -0
  15. package/template/src/admin/pages/ContentPage.tsx +2 -0
  16. package/template/src/admin/pages/DashboardPage.tsx +2 -0
  17. package/template/src/admin/pages/DisputesPage.tsx +2 -0
  18. package/template/src/admin/pages/MediaTestPage.tsx +1 -1
  19. package/template/src/admin/pages/OrdersPage.tsx +2 -0
  20. package/template/src/admin/pages/PluginDashboardPage.tsx +297 -0
  21. package/template/src/admin/pages/PluginManagementPage.tsx +340 -0
  22. package/template/src/admin/pages/PluginReviewPage.tsx +255 -0
  23. package/template/src/admin/pages/SystemLogsPage.tsx +11 -2
  24. package/template/src/admin/pages/TicketsPage.tsx +2 -0
  25. package/template/src/admin/pages/UsersPage.tsx +3 -2
  26. package/template/src/admin/stores/adminStore.ts +5 -0
  27. package/template/src/cli/index.ts +17 -19
  28. package/template/src/cli/modules/auth/index.ts +65 -0
  29. package/template/src/cli/modules/config/index.ts +74 -77
  30. package/template/src/cli/modules/index.ts +28 -6
  31. package/template/src/cli/modules/notification/index.ts +95 -79
  32. package/template/src/cli/modules/plugin/index.ts +111 -0
  33. package/template/src/cli/modules/todo/index.ts +99 -51
  34. package/template/src/cli/utils/auto-command.ts +7 -25
  35. package/template/src/cli/utils/index.ts +3 -1
  36. package/template/src/client/App.tsx +36 -16
  37. package/template/src/client/Layout.tsx +67 -10
  38. package/template/src/client/components/AuthButton.tsx +25 -18
  39. package/template/src/client/components/BottomTabBar.tsx +107 -0
  40. package/template/src/client/components/Navigation.tsx +162 -52
  41. package/template/src/client/components/__tests__/App.test.tsx +48 -32
  42. package/template/src/client/components/__tests__/AuthButton.test.tsx +78 -77
  43. package/template/src/client/components/__tests__/Navigation.test.tsx +31 -20
  44. package/template/src/client/components/index.ts +1 -0
  45. package/template/src/client/contexts/PresetContext.tsx +10 -0
  46. package/template/src/client/main.tsx +63 -8
  47. package/template/src/client/pages/CartPage.tsx +244 -0
  48. package/template/src/client/pages/CategoriesPage.tsx +100 -0
  49. package/template/src/client/pages/ContentDetailPage.tsx +9 -14
  50. package/template/src/client/pages/ContentListPage.tsx +4 -11
  51. package/template/src/client/pages/DashboardPage.tsx +261 -0
  52. package/template/src/client/pages/DeveloperDashboardPage.tsx +211 -0
  53. package/template/src/client/pages/LoginPage.tsx +127 -0
  54. package/template/src/client/pages/OrdersPage.tsx +196 -0
  55. package/template/src/client/pages/PluginDetailPage.tsx +345 -0
  56. package/template/src/client/pages/PluginsPage.tsx +223 -0
  57. package/template/src/client/pages/ProfilePage.tsx +206 -0
  58. package/template/src/client/pages/PublishPage.tsx +336 -0
  59. package/template/src/client/pages/RegisterPage.tsx +136 -0
  60. package/template/src/client/pages/SearchPage.tsx +204 -0
  61. package/template/src/client/pages/SettingsPage.tsx +220 -0
  62. package/template/src/client/pages/TopicsPage.tsx +180 -0
  63. package/template/src/client/pages/__tests__/LoginPage.test.tsx +170 -0
  64. package/template/src/client/pages/__tests__/RegisterPage.test.tsx +168 -0
  65. package/template/src/client/preset-ui-config.ts +492 -0
  66. package/template/src/client/services/apiClient.ts +14 -4
  67. package/template/src/client/stores/__tests__/authStore.test.ts +293 -38
  68. package/template/src/client/stores/__tests__/todoStore.test.ts +7 -17
  69. package/template/src/client/stores/authStore.ts +58 -5
  70. package/template/src/client/stores/chatWSStore.ts +9 -0
  71. package/template/src/client/stores/notificationStore.ts +4 -0
  72. package/template/src/client/stores/pluginStore.ts +279 -0
  73. package/template/src/client/stores/todoStore.ts +2 -6
  74. package/template/src/server/core/__tests__/isr-cache.test.ts +117 -0
  75. package/template/src/server/core/__tests__/isr-invalidation.test.ts +72 -0
  76. package/template/src/server/core/__tests__/ssr-renderer.test.ts +89 -0
  77. package/template/src/server/core/isr-cache.ts +239 -0
  78. package/template/src/server/core/isr-invalidation.ts +45 -0
  79. package/template/src/server/core/module-loader.ts +14 -7
  80. package/template/src/server/core/ssr-renderer.ts +240 -0
  81. package/template/src/server/db/init.ts +257 -10
  82. package/template/src/server/db/schema/developers.ts +20 -0
  83. package/template/src/server/db/schema/index.ts +2 -0
  84. package/template/src/server/db/schema/plugins.ts +114 -0
  85. package/template/src/server/db/test-setup.ts +91 -0
  86. package/template/src/server/entries/cloudflare.ts +79 -7
  87. package/template/src/server/entries/node.ts +48 -5
  88. package/template/src/server/middleware/__tests__/captcha.test.ts +23 -13
  89. package/template/src/server/middleware/auth.ts +8 -1
  90. package/template/src/server/middleware/captcha.ts +14 -4
  91. package/template/src/server/middleware/rate-limit.ts +7 -2
  92. package/template/src/server/module-admin/module.ts +9 -5
  93. package/template/src/server/module-admin/routes/admin-notification-routes.ts +43 -14
  94. package/template/src/server/module-admin/routes/admin-routes.ts +0 -2
  95. package/template/src/server/module-admin/routes/client-auth-routes.ts +90 -0
  96. package/template/src/server/module-admin/routes/dashboard-routes.ts +79 -0
  97. package/template/src/server/module-admin/services/admin-service.ts +68 -11
  98. package/template/src/server/module-auth/__tests__/auth-service.test.ts +239 -0
  99. package/template/src/server/module-auth/index.ts +7 -0
  100. package/template/src/server/module-auth/module.ts +40 -0
  101. package/template/src/server/module-auth/routes/auth-routes.ts +94 -0
  102. package/template/src/server/module-auth/routes/profile-routes.ts +31 -0
  103. package/template/src/server/module-auth/services/auth-service.ts +100 -0
  104. package/template/src/server/module-content/module.ts +10 -4
  105. package/template/src/server/module-content/routes/public-content-routes.ts +2 -2
  106. package/template/src/server/module-content/routes/topics-routes.ts +205 -0
  107. package/template/src/server/module-content/services/content-service.ts +18 -2
  108. package/template/src/server/module-dispute/routes/dispute-routes.ts +2 -1
  109. package/template/src/server/module-dispute/services/dispute-service.ts +1 -1
  110. package/template/src/server/module-notifications/__tests__/sse-rpc.test.ts +14 -14
  111. package/template/src/server/module-notifications/routes/notification-routes.ts +34 -8
  112. package/template/src/server/module-order/__tests__/order-route.test.ts +22 -2
  113. package/template/src/server/module-order/module.ts +15 -0
  114. package/template/src/server/module-order/routes/cart-routes.ts +103 -0
  115. package/template/src/server/module-order/routes/orders-mock-routes.ts +67 -0
  116. package/template/src/server/module-order/services/order-service.ts +1 -1
  117. package/template/src/server/module-plugin/__tests__/plugin-query-service.test.ts +203 -0
  118. package/template/src/server/module-plugin/__tests__/plugin-service.test.ts +234 -0
  119. package/template/src/server/module-plugin/index.ts +2 -0
  120. package/template/src/server/module-plugin/module.ts +52 -0
  121. package/template/src/server/module-plugin/routes/plugin-admin-routes.ts +261 -0
  122. package/template/src/server/module-plugin/routes/plugin-routes.ts +354 -0
  123. package/template/src/server/module-plugin/services/admin-category-service.ts +99 -0
  124. package/template/src/server/module-plugin/services/admin-plugin-service.ts +170 -0
  125. package/template/src/server/module-plugin/services/admin-stats-service.ts +41 -0
  126. package/template/src/server/module-plugin/services/plugin-query-service.ts +360 -0
  127. package/template/src/server/module-plugin/services/plugin-review-service.ts +95 -0
  128. package/template/src/server/module-plugin/services/plugin-service.ts +163 -0
  129. package/template/src/server/module-ticket/services/ticket-service.ts +1 -1
  130. package/template/src/server/module-todos/routes/todos-routes.ts +0 -4
  131. package/template/src/server/route-registry.ts +16 -1
  132. package/template/src/server/test-utils/test-client.ts +1 -2
  133. package/template/src/server/utils/auth.ts +6 -0
  134. package/template/src/server/utils/json.ts +13 -0
  135. package/template/src/shared/core/module-manifest.ts +3 -6
  136. package/template/src/shared/modules/auth/index.ts +12 -0
  137. package/template/src/shared/modules/auth/schemas.ts +50 -0
  138. package/template/src/shared/modules/cart/index.ts +1 -0
  139. package/template/src/shared/modules/cart/schemas.ts +41 -0
  140. package/template/src/shared/modules/community/index.ts +1 -0
  141. package/template/src/shared/modules/community/schemas.ts +58 -0
  142. package/template/src/shared/modules/dashboard/index.ts +1 -0
  143. package/template/src/shared/modules/dashboard/schemas.ts +35 -0
  144. package/template/src/shared/modules/index.ts +41 -0
  145. package/template/src/shared/modules/order/schemas.ts +29 -0
  146. package/template/src/shared/modules/plugins/index.ts +48 -0
  147. package/template/src/shared/modules/plugins/schemas.ts +227 -0
  148. package/template/src/shared/schemas/index.ts +130 -0
  149. package/template/tests/e2e/todo.spec.ts +23 -18
  150. package/template/tests/e2e/visual-screenshots.spec.ts +1824 -0
  151. package/template/uploads/.gitkeep +0 -0
  152. package/template/vite.config.ts +2 -1
  153. package/template/vitest.setup.ts +11 -0
  154. package/template/wrangler.toml +4 -3
  155. package/template/package-lock.json +0 -14554
@@ -0,0 +1,261 @@
1
+ import { createRoute, z } from '@hono/zod-openapi'
2
+ import { OpenAPIHono } from '@hono/zod-openapi'
3
+ import type { AuthUser } from '@server/middleware/auth'
4
+ import * as adminPluginService from '../services/admin-plugin-service'
5
+ import * as adminCategoryService from '../services/admin-category-service'
6
+ import * as adminStatsService from '../services/admin-stats-service'
7
+ import {
8
+ PluginSchema,
9
+ CategorySchema,
10
+ AdminDashboardStatsSchema,
11
+ PluginListAdminSchema,
12
+ AdminListQuerySchema,
13
+ AdminListAllQuerySchema,
14
+ PluginSlugSchema,
15
+ RejectPluginBodySchema,
16
+ PluginDeleteResponseSchema,
17
+ BulkApproveBodySchema,
18
+ BulkRejectBodySchema,
19
+ BulkResponseSchema,
20
+ CreateCategoryBodySchema,
21
+ UpdateCategoryBodySchema,
22
+ CategoryIdParamsSchema,
23
+ CategoryIdResponseSchema,
24
+ } from '@shared/schemas'
25
+ import { successResponse, errorResponse, success, created } from '@server/utils/route-helpers'
26
+
27
+ const getDashboardStatsRoute = createRoute({
28
+ method: 'get',
29
+ path: '/stats/dashboard',
30
+ tags: ['admin-plugins'],
31
+ responses: {
32
+ 200: successResponse(AdminDashboardStatsSchema, 'Dashboard stats'),
33
+ },
34
+ })
35
+
36
+ const listPendingRoute = createRoute({
37
+ method: 'get',
38
+ path: '/plugins/pending',
39
+ tags: ['admin-plugins'],
40
+ request: {
41
+ query: AdminListQuerySchema,
42
+ },
43
+ responses: {
44
+ 200: successResponse(PluginListAdminSchema, 'Pending plugins'),
45
+ },
46
+ })
47
+
48
+ const listAllPluginsRoute = createRoute({
49
+ method: 'get',
50
+ path: '/plugins',
51
+ tags: ['admin-plugins'],
52
+ request: {
53
+ query: AdminListAllQuerySchema,
54
+ },
55
+ responses: {
56
+ 200: successResponse(PluginListAdminSchema, 'All plugins'),
57
+ },
58
+ })
59
+
60
+ const approvePluginRoute = createRoute({
61
+ method: 'put',
62
+ path: '/plugins/{slug}/approve',
63
+ tags: ['admin-plugins'],
64
+ request: { params: PluginSlugSchema },
65
+ responses: {
66
+ 200: successResponse(PluginSchema, 'Plugin approved'),
67
+ 404: errorResponse('Plugin not found'),
68
+ },
69
+ })
70
+
71
+ const rejectPluginRoute = createRoute({
72
+ method: 'put',
73
+ path: '/plugins/{slug}/reject',
74
+ tags: ['admin-plugins'],
75
+ request: {
76
+ params: PluginSlugSchema,
77
+ body: { content: { 'application/json': { schema: RejectPluginBodySchema } } },
78
+ },
79
+ responses: {
80
+ 200: successResponse(PluginSchema, 'Plugin rejected'),
81
+ 404: errorResponse('Plugin not found'),
82
+ },
83
+ })
84
+
85
+ const toggleFeaturedRoute = createRoute({
86
+ method: 'put',
87
+ path: '/plugins/{slug}/feature',
88
+ tags: ['admin-plugins'],
89
+ request: { params: PluginSlugSchema },
90
+ responses: {
91
+ 200: successResponse(PluginSchema, 'Featured toggled'),
92
+ 404: errorResponse('Plugin not found'),
93
+ },
94
+ })
95
+
96
+ const removePluginRoute = createRoute({
97
+ method: 'delete',
98
+ path: '/plugins/{slug}',
99
+ tags: ['admin-plugins'],
100
+ request: { params: PluginSlugSchema },
101
+ responses: {
102
+ 200: successResponse(PluginDeleteResponseSchema, 'Plugin removed'),
103
+ 404: errorResponse('Plugin not found'),
104
+ },
105
+ })
106
+
107
+ const bulkApproveRoute = createRoute({
108
+ method: 'post',
109
+ path: '/plugins/bulk-approve',
110
+ tags: ['admin-plugins'],
111
+ request: {
112
+ body: { content: { 'application/json': { schema: BulkApproveBodySchema } } },
113
+ },
114
+ responses: {
115
+ 200: successResponse(BulkResponseSchema, 'Bulk approved'),
116
+ },
117
+ })
118
+
119
+ const bulkRejectRoute = createRoute({
120
+ method: 'post',
121
+ path: '/plugins/bulk-reject',
122
+ tags: ['admin-plugins'],
123
+ request: {
124
+ body: {
125
+ content: {
126
+ 'application/json': {
127
+ schema: BulkRejectBodySchema,
128
+ },
129
+ },
130
+ },
131
+ },
132
+ responses: {
133
+ 200: successResponse(BulkResponseSchema, 'Bulk rejected'),
134
+ },
135
+ })
136
+
137
+ const listCategoriesRoute = createRoute({
138
+ method: 'get',
139
+ path: '/categories',
140
+ tags: ['admin-plugins'],
141
+ responses: {
142
+ 200: successResponse(z.array(CategorySchema), 'List categories'),
143
+ },
144
+ })
145
+
146
+ const createCategoryRoute = createRoute({
147
+ method: 'post',
148
+ path: '/categories',
149
+ tags: ['admin-plugins'],
150
+ request: {
151
+ body: {
152
+ content: {
153
+ 'application/json': {
154
+ schema: CreateCategoryBodySchema,
155
+ },
156
+ },
157
+ },
158
+ },
159
+ responses: {
160
+ 201: successResponse(CategorySchema, 'Category created'),
161
+ 409: errorResponse('Category slug already exists'),
162
+ },
163
+ })
164
+
165
+ const updateCategoryRoute = createRoute({
166
+ method: 'put',
167
+ path: '/categories/{id}',
168
+ tags: ['admin-plugins'],
169
+ request: {
170
+ params: CategoryIdParamsSchema,
171
+ body: {
172
+ content: {
173
+ 'application/json': {
174
+ schema: UpdateCategoryBodySchema,
175
+ },
176
+ },
177
+ },
178
+ },
179
+ responses: {
180
+ 200: successResponse(CategorySchema, 'Category updated'),
181
+ 404: errorResponse('Category not found'),
182
+ },
183
+ })
184
+
185
+ const deleteCategoryRoute = createRoute({
186
+ method: 'delete',
187
+ path: '/categories/{id}',
188
+ tags: ['admin-plugins'],
189
+ request: { params: CategoryIdParamsSchema },
190
+ responses: {
191
+ 200: successResponse(CategoryIdResponseSchema, 'Category deleted'),
192
+ 404: errorResponse('Category not found'),
193
+ },
194
+ })
195
+
196
+ export const pluginAdminRoutes = new OpenAPIHono<{ Variables: { authUser: AuthUser } }>()
197
+ .openapi(getDashboardStatsRoute, async c => {
198
+ const stats = await adminStatsService.getDashboardStats()
199
+ return c.json(success(stats), 200)
200
+ })
201
+ .openapi(listPendingRoute, async c => {
202
+ const { page, limit } = c.req.valid('query')
203
+ const result = await adminPluginService.listPending({ page, limit })
204
+ return c.json(success(result), 200)
205
+ })
206
+ .openapi(listAllPluginsRoute, async c => {
207
+ const { page, limit, status } = c.req.valid('query')
208
+ const result = await adminPluginService.listAllPlugins({ page, limit, status })
209
+ return c.json(success(result), 200)
210
+ })
211
+ .openapi(approvePluginRoute, async c => {
212
+ const { slug } = c.req.valid('param')
213
+ const plugin = await adminPluginService.approvePlugin(slug)
214
+ return c.json(success(plugin), 200)
215
+ })
216
+ .openapi(rejectPluginRoute, async c => {
217
+ const { slug } = c.req.valid('param')
218
+ const { reason } = c.req.valid('json')
219
+ const plugin = await adminPluginService.rejectPlugin(slug, reason)
220
+ return c.json(success(plugin), 200)
221
+ })
222
+ .openapi(toggleFeaturedRoute, async c => {
223
+ const { slug } = c.req.valid('param')
224
+ const plugin = await adminPluginService.toggleFeatured(slug)
225
+ return c.json(success(plugin), 200)
226
+ })
227
+ .openapi(removePluginRoute, async c => {
228
+ const { slug } = c.req.valid('param')
229
+ await adminPluginService.adminRemovePlugin(slug)
230
+ return c.json(success({ slug }), 200)
231
+ })
232
+ .openapi(bulkApproveRoute, async c => {
233
+ const { slugs } = c.req.valid('json')
234
+ const count = await adminPluginService.bulkApprove(slugs)
235
+ return c.json(success({ count }), 200)
236
+ })
237
+ .openapi(bulkRejectRoute, async c => {
238
+ const { slugs, reason } = c.req.valid('json')
239
+ const count = await adminPluginService.bulkReject(slugs, reason)
240
+ return c.json(success({ count }), 200)
241
+ })
242
+ .openapi(listCategoriesRoute, async c => {
243
+ const categories = await adminCategoryService.listAllCategories()
244
+ return c.json(success(categories), 200)
245
+ })
246
+ .openapi(createCategoryRoute, async c => {
247
+ const data = c.req.valid('json')
248
+ const category = await adminCategoryService.createCategory(data)
249
+ return c.json(created(category), 201)
250
+ })
251
+ .openapi(updateCategoryRoute, async c => {
252
+ const { id } = c.req.valid('param')
253
+ const data = c.req.valid('json')
254
+ const category = await adminCategoryService.updateCategory(id, data)
255
+ return c.json(success(category), 200)
256
+ })
257
+ .openapi(deleteCategoryRoute, async c => {
258
+ const { id } = c.req.valid('param')
259
+ await adminCategoryService.deleteCategory(id)
260
+ return c.json(success({ id }), 200)
261
+ })
@@ -0,0 +1,354 @@
1
+ import { createRoute, z } from '@hono/zod-openapi'
2
+ import { OpenAPIHono } from '@hono/zod-openapi'
3
+ import * as pluginService from '../services/plugin-service'
4
+ import * as queryService from '../services/plugin-query-service'
5
+ import * as reviewService from '../services/plugin-review-service'
6
+ import {
7
+ PluginSchema,
8
+ CreatePluginSchema,
9
+ UpdatePluginSchema,
10
+ PluginSlugSchema,
11
+ ReviewSchema,
12
+ CreateReviewSchema,
13
+ MarketplaceStatsSchema,
14
+ CategorySchema,
15
+ VersionSchema,
16
+ PluginListResponseSchema,
17
+ PluginListQuerySchema,
18
+ PluginSearchQuerySchema,
19
+ PluginDeleteResponseSchema,
20
+ ReviewIdParamsSchema,
21
+ ReviewDeleteResponseSchema,
22
+ CategorySlugParamsSchema,
23
+ CategoryPluginsQuerySchema,
24
+ } from '@shared/schemas'
25
+ import { successResponse, errorResponse, success, created } from '@server/utils/route-helpers'
26
+ import { getAuthUser } from '@server/utils/auth'
27
+
28
+ const listRoute = createRoute({
29
+ method: 'get',
30
+ path: '/plugins',
31
+ tags: ['plugins'],
32
+ request: { query: PluginListQuerySchema },
33
+ responses: {
34
+ 200: successResponse(PluginListResponseSchema, 'List plugins'),
35
+ },
36
+ })
37
+
38
+ const searchRoute = createRoute({
39
+ method: 'get',
40
+ path: '/plugins/search',
41
+ tags: ['plugins'],
42
+ request: {
43
+ query: PluginSearchQuerySchema,
44
+ },
45
+ responses: {
46
+ 200: successResponse(PluginListResponseSchema, 'Search plugins'),
47
+ },
48
+ })
49
+
50
+ const getBySlugRoute = createRoute({
51
+ method: 'get',
52
+ path: '/plugins/{slug}',
53
+ tags: ['plugins'],
54
+ request: { params: PluginSlugSchema },
55
+ responses: {
56
+ 200: successResponse(PluginSchema, 'Get plugin by slug'),
57
+ 404: errorResponse('Plugin not found'),
58
+ },
59
+ })
60
+
61
+ const getVersionsRoute = createRoute({
62
+ method: 'get',
63
+ path: '/plugins/{slug}/versions',
64
+ tags: ['plugins'],
65
+ request: { params: PluginSlugSchema },
66
+ responses: {
67
+ 200: successResponse(z.array(VersionSchema), 'List plugin versions'),
68
+ 404: errorResponse('Plugin not found'),
69
+ },
70
+ })
71
+
72
+ const createRouteDef = createRoute({
73
+ method: 'post',
74
+ path: '/plugins',
75
+ tags: ['plugins'],
76
+ security: [{ Bearer: [] }],
77
+ request: {
78
+ body: { content: { 'application/json': { schema: CreatePluginSchema } } },
79
+ },
80
+ responses: {
81
+ 201: successResponse(PluginSchema, 'Create plugin'),
82
+ 400: errorResponse('Invalid input'),
83
+ 409: errorResponse('Plugin slug already exists'),
84
+ },
85
+ })
86
+
87
+ const updateRoute = createRoute({
88
+ method: 'put',
89
+ path: '/plugins/{slug}',
90
+ tags: ['plugins'],
91
+ security: [{ Bearer: [] }],
92
+ request: {
93
+ params: PluginSlugSchema,
94
+ body: { content: { 'application/json': { schema: UpdatePluginSchema } } },
95
+ },
96
+ responses: {
97
+ 200: successResponse(PluginSchema, 'Update plugin'),
98
+ 404: errorResponse('Plugin not found'),
99
+ 403: errorResponse('Forbidden'),
100
+ },
101
+ })
102
+
103
+ const deleteRoute = createRoute({
104
+ method: 'delete',
105
+ path: '/plugins/{slug}',
106
+ tags: ['plugins'],
107
+ security: [{ Bearer: [] }],
108
+ request: { params: PluginSlugSchema },
109
+ responses: {
110
+ 200: successResponse(PluginDeleteResponseSchema, 'Delete plugin'),
111
+ 404: errorResponse('Plugin not found'),
112
+ 403: errorResponse('Forbidden'),
113
+ },
114
+ })
115
+
116
+ const submitReviewRoute = createRoute({
117
+ method: 'post',
118
+ path: '/plugins/{slug}/reviews',
119
+ tags: ['plugins'],
120
+ security: [{ Bearer: [] }],
121
+ request: {
122
+ params: PluginSlugSchema,
123
+ body: { content: { 'application/json': { schema: CreateReviewSchema } } },
124
+ },
125
+ responses: {
126
+ 201: successResponse(ReviewSchema, 'Submit review'),
127
+ 404: errorResponse('Plugin not found'),
128
+ 409: errorResponse('Already reviewed'),
129
+ },
130
+ })
131
+
132
+ const getReviewsRoute = createRoute({
133
+ method: 'get',
134
+ path: '/plugins/{slug}/reviews',
135
+ tags: ['plugins'],
136
+ request: { params: PluginSlugSchema },
137
+ responses: {
138
+ 200: successResponse(z.array(ReviewSchema), 'List reviews'),
139
+ },
140
+ })
141
+
142
+ const deleteReviewRoute = createRoute({
143
+ method: 'delete',
144
+ path: '/plugins/{slug}/reviews/{reviewId}',
145
+ tags: ['plugins'],
146
+ security: [{ Bearer: [] }],
147
+ request: {
148
+ params: ReviewIdParamsSchema,
149
+ },
150
+ responses: {
151
+ 200: successResponse(ReviewDeleteResponseSchema, 'Review deleted'),
152
+ 404: errorResponse('Review not found'),
153
+ 403: errorResponse('Forbidden'),
154
+ },
155
+ })
156
+
157
+ const trackInstallRoute = createRoute({
158
+ method: 'post',
159
+ path: '/plugins/{slug}/install',
160
+ tags: ['plugins'],
161
+ request: { params: PluginSlugSchema },
162
+ responses: {
163
+ 200: successResponse(PluginDeleteResponseSchema, 'Install tracked'),
164
+ 404: errorResponse('Plugin not found'),
165
+ },
166
+ })
167
+
168
+ const listCategoriesRoute = createRoute({
169
+ method: 'get',
170
+ path: '/categories',
171
+ tags: ['plugins'],
172
+ responses: {
173
+ 200: successResponse(z.array(CategorySchema), 'List categories'),
174
+ },
175
+ })
176
+
177
+ const getPluginsByCategoryRoute = createRoute({
178
+ method: 'get',
179
+ path: '/categories/{slug}/plugins',
180
+ tags: ['plugins'],
181
+ request: {
182
+ params: CategorySlugParamsSchema,
183
+ query: CategoryPluginsQuerySchema,
184
+ },
185
+ responses: {
186
+ 200: successResponse(PluginListResponseSchema, 'Plugins by category'),
187
+ 404: errorResponse('Category not found'),
188
+ },
189
+ })
190
+
191
+ const getStatsRoute = createRoute({
192
+ method: 'get',
193
+ path: '/stats',
194
+ tags: ['plugins'],
195
+ responses: {
196
+ 200: successResponse(MarketplaceStatsSchema, 'Marketplace stats'),
197
+ },
198
+ })
199
+
200
+ const listMyPluginsRoute = createRoute({
201
+ method: 'get',
202
+ path: '/plugins/mine',
203
+ tags: ['plugins'],
204
+ security: [{ Bearer: [] }],
205
+ responses: {
206
+ 200: successResponse(z.array(PluginSchema), 'My plugins'),
207
+ },
208
+ })
209
+
210
+ export const pluginRoutes = new OpenAPIHono()
211
+ .openapi(listMyPluginsRoute, async c => {
212
+ const user = getAuthUser(c)
213
+ if (!user) {
214
+ return c.json({ success: false as const, error: 'Unauthorized' }, 401)
215
+ }
216
+ try {
217
+ const plugins = await queryService.listMyPlugins(user.id)
218
+ return c.json(success(plugins), 200)
219
+ } catch {
220
+ return c.json(success([]), 200)
221
+ }
222
+ })
223
+ .openapi(listRoute, async c => {
224
+ const query = c.req.valid('query')
225
+ try {
226
+ const result = await queryService.listPlugins({
227
+ page: query.page,
228
+ limit: query.limit,
229
+ status: query.status,
230
+ sort: query.sort,
231
+ featured: query.featured,
232
+ })
233
+ return c.json(success(result), 200)
234
+ } catch {
235
+ return c.json(success({ plugins: [], total: 0, page: 1, limit: 20 }), 200)
236
+ }
237
+ })
238
+ .openapi(searchRoute, async c => {
239
+ const { q, page, limit, category } = c.req.valid('query')
240
+ try {
241
+ const result = await queryService.searchPlugins(q, { page, limit, category })
242
+ return c.json(success(result), 200)
243
+ } catch {
244
+ return c.json(success({ plugins: [], total: 0, page: 1, limit: 20 }), 200)
245
+ }
246
+ })
247
+ .openapi(getBySlugRoute, async c => {
248
+ const { slug } = c.req.valid('param')
249
+ try {
250
+ const plugin = await queryService.getPluginBySlug(slug)
251
+ return c.json(success(plugin), 200)
252
+ } catch {
253
+ return c.json({ success: false as const, error: 'Plugin not found' }, 404)
254
+ }
255
+ })
256
+ .openapi(getVersionsRoute, async c => {
257
+ const { slug } = c.req.valid('param')
258
+ try {
259
+ const plugin = await queryService.getPluginBySlug(slug)
260
+ const versions = await queryService.getVersions(plugin.id)
261
+ return c.json(success(versions), 200)
262
+ } catch {
263
+ return c.json(success([]), 200)
264
+ }
265
+ })
266
+ .openapi(createRouteDef, async c => {
267
+ const data = c.req.valid('json')
268
+ const user = getAuthUser(c)
269
+ const plugin = await pluginService.createPlugin({
270
+ ...data,
271
+ authorId: user.id,
272
+ authorName: user.username,
273
+ })
274
+ return c.json(created(plugin), 201)
275
+ })
276
+ .openapi(updateRoute, async c => {
277
+ const { slug } = c.req.valid('param')
278
+ const data = c.req.valid('json')
279
+ const user = getAuthUser(c)
280
+ const plugin = await pluginService.updatePlugin(slug, data, user.id)
281
+ return c.json(success(plugin), 200)
282
+ })
283
+ .openapi(deleteRoute, async c => {
284
+ const { slug } = c.req.valid('param')
285
+ const user = getAuthUser(c)
286
+ await pluginService.deletePlugin(slug, user.id)
287
+ return c.json(success({ slug }), 200)
288
+ })
289
+ .openapi(submitReviewRoute, async c => {
290
+ const { slug } = c.req.valid('param')
291
+ const data = c.req.valid('json')
292
+ const user = getAuthUser(c)
293
+ try {
294
+ const plugin = await queryService.getPluginBySlug(slug)
295
+ const review = await reviewService.submitReview({
296
+ pluginId: plugin.id,
297
+ userId: user.id,
298
+ userName: user.username,
299
+ rating: data.rating,
300
+ title: data.title,
301
+ content: data.content,
302
+ })
303
+ return c.json(created(review), 201)
304
+ } catch {
305
+ return c.json({ success: false as const, error: 'Plugin not found' }, 404)
306
+ }
307
+ })
308
+ .openapi(getReviewsRoute, async c => {
309
+ const { slug } = c.req.valid('param')
310
+ try {
311
+ const plugin = await queryService.getPluginBySlug(slug)
312
+ const reviews = await reviewService.getReviews(plugin.id)
313
+ return c.json(success(reviews), 200)
314
+ } catch {
315
+ return c.json(success([]), 200)
316
+ }
317
+ })
318
+ .openapi(deleteReviewRoute, async c => {
319
+ const { reviewId } = c.req.valid('param')
320
+ const user = getAuthUser(c)
321
+ await reviewService.deleteReview(reviewId, user.id)
322
+ return c.json(success({ id: reviewId }), 200)
323
+ })
324
+ .openapi(trackInstallRoute, async c => {
325
+ const { slug } = c.req.valid('param')
326
+ try {
327
+ await pluginService.trackInstall(slug)
328
+ return c.json(success({ slug }), 200)
329
+ } catch {
330
+ return c.json(success({ slug }), 200)
331
+ }
332
+ })
333
+ .openapi(listCategoriesRoute, async c => {
334
+ try {
335
+ const categories = await queryService.listCategories()
336
+ return c.json(success(categories), 200)
337
+ } catch {
338
+ return c.json(success([]), 200)
339
+ }
340
+ })
341
+ .openapi(getPluginsByCategoryRoute, async c => {
342
+ const { slug } = c.req.valid('param')
343
+ const { page, limit } = c.req.valid('query')
344
+ try {
345
+ const result = await queryService.getPluginsByCategory(slug, { page, limit })
346
+ return c.json(success(result), 200)
347
+ } catch {
348
+ return c.json(success({ plugins: [], total: 0, page: 1, limit: 20 }), 200)
349
+ }
350
+ })
351
+ .openapi(getStatsRoute, async c => {
352
+ const stats = await queryService.getStats()
353
+ return c.json(success(stats), 200)
354
+ })
@@ -0,0 +1,99 @@
1
+ import { eq, asc } from 'drizzle-orm'
2
+ import type { Category } from '@shared/schemas'
3
+ import { getDb } from '@server/db'
4
+ import { pluginCategories } from '@server/db/schema'
5
+ import { generateUUID } from '@server/utils/uuid'
6
+ import { NotFoundError, ConflictError } from '@server/utils/app-error'
7
+
8
+ export interface CreateCategoryData {
9
+ name: string
10
+ slug: string
11
+ description?: string | null
12
+ icon?: string | null
13
+ }
14
+
15
+ export interface UpdateCategoryData {
16
+ name?: string | null
17
+ slug?: string | null
18
+ description?: string | null
19
+ icon?: string | null
20
+ sortOrder?: number | null
21
+ }
22
+
23
+ function mapCategoryRow(row: typeof pluginCategories.$inferSelect): Category {
24
+ return {
25
+ id: row.id,
26
+ name: row.name,
27
+ slug: row.slug,
28
+ description: row.description ?? undefined,
29
+ icon: row.icon ?? undefined,
30
+ sortOrder: row.sortOrder,
31
+ }
32
+ }
33
+
34
+ export async function createCategory(data: CreateCategoryData): Promise<Category> {
35
+ const db = await getDb()
36
+
37
+ const existing = await db
38
+ .select()
39
+ .from(pluginCategories)
40
+ .where(eq(pluginCategories.slug, data.slug))
41
+ if (existing.length > 0) {
42
+ throw new ConflictError(`Category with slug '${data.slug}' already exists`)
43
+ }
44
+
45
+ const id = generateUUID()
46
+ const result = await db
47
+ .insert(pluginCategories)
48
+ .values({
49
+ id,
50
+ name: data.name,
51
+ slug: data.slug,
52
+ description: data.description ?? null,
53
+ icon: data.icon ?? null,
54
+ sortOrder: 0,
55
+ })
56
+ .returning()
57
+
58
+ return mapCategoryRow(result[0])
59
+ }
60
+
61
+ export async function updateCategory(id: string, data: UpdateCategoryData): Promise<Category> {
62
+ const db = await getDb()
63
+
64
+ const rows = await db.select().from(pluginCategories).where(eq(pluginCategories.id, id))
65
+ if (rows.length === 0) {
66
+ throw new NotFoundError('Category', id)
67
+ }
68
+
69
+ const updateData: Partial<typeof pluginCategories.$inferInsert> = {}
70
+ if (data.name !== undefined && data.name !== null) updateData.name = data.name
71
+ if (data.slug !== undefined && data.slug !== null) updateData.slug = data.slug
72
+ if (data.description !== undefined) updateData.description = data.description
73
+ if (data.icon !== undefined) updateData.icon = data.icon
74
+ if (data.sortOrder !== undefined && data.sortOrder !== null) updateData.sortOrder = data.sortOrder
75
+
76
+ const result = await db
77
+ .update(pluginCategories)
78
+ .set(updateData)
79
+ .where(eq(pluginCategories.id, id))
80
+ .returning()
81
+ return mapCategoryRow(result[0])
82
+ }
83
+
84
+ export async function deleteCategory(id: string): Promise<void> {
85
+ const db = await getDb()
86
+
87
+ const rows = await db.select().from(pluginCategories).where(eq(pluginCategories.id, id))
88
+ if (rows.length === 0) {
89
+ throw new NotFoundError('Category', id)
90
+ }
91
+
92
+ await db.delete(pluginCategories).where(eq(pluginCategories.id, id))
93
+ }
94
+
95
+ export async function listAllCategories(): Promise<Category[]> {
96
+ const db = await getDb()
97
+ const rows = await db.select().from(pluginCategories).orderBy(asc(pluginCategories.sortOrder))
98
+ return rows.map(mapCategoryRow)
99
+ }