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.
- package/dist/cli/index.js +1673 -696
- package/dist/cli/index.js.map +1 -1
- package/package.json +16 -10
- package/template/.husky/pre-commit +1 -1
- package/template/modules.config.ts +29 -2
- package/template/package.json +12 -12
- package/template/patches/{typescript+5.8.3.patch → typescript+5.9.3.patch} +2 -4
- package/template/playwright.config.ts +7 -1
- package/template/src/admin/App.tsx +2 -2
- package/template/src/admin/components/CaptchaModal.tsx +7 -9
- package/template/src/admin/layouts/Header.tsx +56 -22
- package/template/src/admin/layouts/Layout.tsx +14 -9
- package/template/src/admin/layouts/Sidebar.tsx +122 -105
- package/template/src/admin/pages/CategoryManagementPage.tsx +241 -0
- package/template/src/admin/pages/ContentPage.tsx +2 -0
- package/template/src/admin/pages/DashboardPage.tsx +2 -0
- package/template/src/admin/pages/DisputesPage.tsx +2 -0
- package/template/src/admin/pages/MediaTestPage.tsx +1 -1
- package/template/src/admin/pages/OrdersPage.tsx +2 -0
- package/template/src/admin/pages/PluginDashboardPage.tsx +297 -0
- package/template/src/admin/pages/PluginManagementPage.tsx +340 -0
- package/template/src/admin/pages/PluginReviewPage.tsx +255 -0
- package/template/src/admin/pages/SystemLogsPage.tsx +11 -2
- package/template/src/admin/pages/TicketsPage.tsx +2 -0
- package/template/src/admin/pages/UsersPage.tsx +3 -2
- package/template/src/admin/stores/adminStore.ts +5 -0
- package/template/src/cli/index.ts +17 -19
- package/template/src/cli/modules/auth/index.ts +65 -0
- package/template/src/cli/modules/config/index.ts +74 -77
- package/template/src/cli/modules/index.ts +28 -6
- package/template/src/cli/modules/notification/index.ts +95 -79
- package/template/src/cli/modules/plugin/index.ts +111 -0
- package/template/src/cli/modules/todo/index.ts +99 -51
- package/template/src/cli/utils/auto-command.ts +7 -25
- package/template/src/cli/utils/index.ts +3 -1
- package/template/src/client/App.tsx +36 -16
- package/template/src/client/Layout.tsx +67 -10
- package/template/src/client/components/AuthButton.tsx +25 -18
- package/template/src/client/components/BottomTabBar.tsx +107 -0
- package/template/src/client/components/Navigation.tsx +162 -52
- package/template/src/client/components/__tests__/App.test.tsx +48 -32
- package/template/src/client/components/__tests__/AuthButton.test.tsx +78 -77
- package/template/src/client/components/__tests__/Navigation.test.tsx +31 -20
- package/template/src/client/components/index.ts +1 -0
- package/template/src/client/contexts/PresetContext.tsx +10 -0
- package/template/src/client/main.tsx +63 -8
- package/template/src/client/pages/CartPage.tsx +244 -0
- package/template/src/client/pages/CategoriesPage.tsx +100 -0
- package/template/src/client/pages/ContentDetailPage.tsx +9 -14
- package/template/src/client/pages/ContentListPage.tsx +4 -11
- package/template/src/client/pages/DashboardPage.tsx +261 -0
- package/template/src/client/pages/DeveloperDashboardPage.tsx +211 -0
- package/template/src/client/pages/LoginPage.tsx +127 -0
- package/template/src/client/pages/OrdersPage.tsx +196 -0
- package/template/src/client/pages/PluginDetailPage.tsx +345 -0
- package/template/src/client/pages/PluginsPage.tsx +223 -0
- package/template/src/client/pages/ProfilePage.tsx +206 -0
- package/template/src/client/pages/PublishPage.tsx +336 -0
- package/template/src/client/pages/RegisterPage.tsx +136 -0
- package/template/src/client/pages/SearchPage.tsx +204 -0
- package/template/src/client/pages/SettingsPage.tsx +220 -0
- package/template/src/client/pages/TopicsPage.tsx +180 -0
- package/template/src/client/pages/__tests__/LoginPage.test.tsx +170 -0
- package/template/src/client/pages/__tests__/RegisterPage.test.tsx +168 -0
- package/template/src/client/preset-ui-config.ts +492 -0
- package/template/src/client/services/apiClient.ts +14 -4
- package/template/src/client/stores/__tests__/authStore.test.ts +293 -38
- package/template/src/client/stores/__tests__/todoStore.test.ts +7 -17
- package/template/src/client/stores/authStore.ts +58 -5
- package/template/src/client/stores/chatWSStore.ts +9 -0
- package/template/src/client/stores/notificationStore.ts +4 -0
- package/template/src/client/stores/pluginStore.ts +279 -0
- package/template/src/client/stores/todoStore.ts +2 -6
- package/template/src/server/core/__tests__/isr-cache.test.ts +117 -0
- package/template/src/server/core/__tests__/isr-invalidation.test.ts +72 -0
- package/template/src/server/core/__tests__/ssr-renderer.test.ts +89 -0
- package/template/src/server/core/isr-cache.ts +239 -0
- package/template/src/server/core/isr-invalidation.ts +45 -0
- package/template/src/server/core/module-loader.ts +14 -7
- package/template/src/server/core/ssr-renderer.ts +240 -0
- package/template/src/server/db/init.ts +257 -10
- package/template/src/server/db/schema/developers.ts +20 -0
- package/template/src/server/db/schema/index.ts +2 -0
- package/template/src/server/db/schema/plugins.ts +114 -0
- package/template/src/server/db/test-setup.ts +91 -0
- package/template/src/server/entries/cloudflare.ts +79 -7
- package/template/src/server/entries/node.ts +48 -5
- package/template/src/server/middleware/__tests__/captcha.test.ts +23 -13
- package/template/src/server/middleware/auth.ts +8 -1
- package/template/src/server/middleware/captcha.ts +14 -4
- package/template/src/server/middleware/rate-limit.ts +7 -2
- package/template/src/server/module-admin/module.ts +9 -5
- package/template/src/server/module-admin/routes/admin-notification-routes.ts +43 -14
- package/template/src/server/module-admin/routes/admin-routes.ts +0 -2
- package/template/src/server/module-admin/routes/client-auth-routes.ts +90 -0
- package/template/src/server/module-admin/routes/dashboard-routes.ts +79 -0
- package/template/src/server/module-admin/services/admin-service.ts +68 -11
- package/template/src/server/module-auth/__tests__/auth-service.test.ts +239 -0
- package/template/src/server/module-auth/index.ts +7 -0
- package/template/src/server/module-auth/module.ts +40 -0
- package/template/src/server/module-auth/routes/auth-routes.ts +94 -0
- package/template/src/server/module-auth/routes/profile-routes.ts +31 -0
- package/template/src/server/module-auth/services/auth-service.ts +100 -0
- package/template/src/server/module-content/module.ts +10 -4
- package/template/src/server/module-content/routes/public-content-routes.ts +2 -2
- package/template/src/server/module-content/routes/topics-routes.ts +205 -0
- package/template/src/server/module-content/services/content-service.ts +18 -2
- package/template/src/server/module-dispute/routes/dispute-routes.ts +2 -1
- package/template/src/server/module-dispute/services/dispute-service.ts +1 -1
- package/template/src/server/module-notifications/__tests__/sse-rpc.test.ts +14 -14
- package/template/src/server/module-notifications/routes/notification-routes.ts +34 -8
- package/template/src/server/module-order/__tests__/order-route.test.ts +22 -2
- package/template/src/server/module-order/module.ts +15 -0
- package/template/src/server/module-order/routes/cart-routes.ts +103 -0
- package/template/src/server/module-order/routes/orders-mock-routes.ts +67 -0
- package/template/src/server/module-order/services/order-service.ts +1 -1
- package/template/src/server/module-plugin/__tests__/plugin-query-service.test.ts +203 -0
- package/template/src/server/module-plugin/__tests__/plugin-service.test.ts +234 -0
- package/template/src/server/module-plugin/index.ts +2 -0
- package/template/src/server/module-plugin/module.ts +52 -0
- package/template/src/server/module-plugin/routes/plugin-admin-routes.ts +261 -0
- package/template/src/server/module-plugin/routes/plugin-routes.ts +354 -0
- package/template/src/server/module-plugin/services/admin-category-service.ts +99 -0
- package/template/src/server/module-plugin/services/admin-plugin-service.ts +170 -0
- package/template/src/server/module-plugin/services/admin-stats-service.ts +41 -0
- package/template/src/server/module-plugin/services/plugin-query-service.ts +360 -0
- package/template/src/server/module-plugin/services/plugin-review-service.ts +95 -0
- package/template/src/server/module-plugin/services/plugin-service.ts +163 -0
- package/template/src/server/module-ticket/services/ticket-service.ts +1 -1
- package/template/src/server/module-todos/routes/todos-routes.ts +0 -4
- package/template/src/server/route-registry.ts +16 -1
- package/template/src/server/test-utils/test-client.ts +1 -2
- package/template/src/server/utils/auth.ts +6 -0
- package/template/src/server/utils/json.ts +13 -0
- package/template/src/shared/core/module-manifest.ts +3 -6
- package/template/src/shared/modules/auth/index.ts +12 -0
- package/template/src/shared/modules/auth/schemas.ts +50 -0
- package/template/src/shared/modules/cart/index.ts +1 -0
- package/template/src/shared/modules/cart/schemas.ts +41 -0
- package/template/src/shared/modules/community/index.ts +1 -0
- package/template/src/shared/modules/community/schemas.ts +58 -0
- package/template/src/shared/modules/dashboard/index.ts +1 -0
- package/template/src/shared/modules/dashboard/schemas.ts +35 -0
- package/template/src/shared/modules/index.ts +41 -0
- package/template/src/shared/modules/order/schemas.ts +29 -0
- package/template/src/shared/modules/plugins/index.ts +48 -0
- package/template/src/shared/modules/plugins/schemas.ts +227 -0
- package/template/src/shared/schemas/index.ts +130 -0
- package/template/tests/e2e/todo.spec.ts +23 -18
- package/template/tests/e2e/visual-screenshots.spec.ts +1824 -0
- package/template/uploads/.gitkeep +0 -0
- package/template/vite.config.ts +2 -1
- package/template/vitest.setup.ts +11 -0
- package/template/wrangler.toml +4 -3
- package/template/package-lock.json +0 -14554
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { getDb } from './driver'
|
|
2
2
|
import { permissions, roles, rolePermissions } from './schema'
|
|
3
3
|
import { logger } from '../utils/logger'
|
|
4
|
-
import {
|
|
5
|
-
import { seedTicketsIfEmpty } from '../module-ticket/services/ticket-service'
|
|
6
|
-
import { seedDisputesIfEmpty } from '../module-dispute/services/dispute-service'
|
|
7
|
-
import { seedContentsIfEmpty } from '../module-content/services/content-service'
|
|
4
|
+
import { generateUUID } from '../utils/uuid'
|
|
8
5
|
|
|
9
6
|
const log = logger.db()
|
|
10
7
|
|
|
@@ -355,6 +352,254 @@ const initialRolePermissions = [
|
|
|
355
352
|
{ roleId: 'role_user', permissionId: 'perm_order_view' },
|
|
356
353
|
]
|
|
357
354
|
|
|
355
|
+
async function seedPluginsIfEmpty() {
|
|
356
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- dynamic import for presets that may not have plugin module
|
|
357
|
+
let plugins: any
|
|
358
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
359
|
+
let pluginVersions: any
|
|
360
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
361
|
+
let pluginCategories: any
|
|
362
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
363
|
+
let pluginCategoryMappings: any
|
|
364
|
+
try {
|
|
365
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
366
|
+
const schema = (await import('./schema')) as Record<string, any>
|
|
367
|
+
plugins = schema.plugins
|
|
368
|
+
pluginVersions = schema.pluginVersions
|
|
369
|
+
pluginCategories = schema.pluginCategories
|
|
370
|
+
pluginCategoryMappings = schema.pluginCategoryMappings
|
|
371
|
+
if (!plugins) return
|
|
372
|
+
} catch {
|
|
373
|
+
return
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const db = await getDb()
|
|
377
|
+
|
|
378
|
+
const existing = await db.select().from(plugins)
|
|
379
|
+
if (existing.length > 0) return
|
|
380
|
+
|
|
381
|
+
log.info({}, 'Seeding plugins...')
|
|
382
|
+
|
|
383
|
+
const sampleCategories = [
|
|
384
|
+
{
|
|
385
|
+
id: generateUUID(),
|
|
386
|
+
name: 'UI & Design',
|
|
387
|
+
slug: 'ui-design',
|
|
388
|
+
description: 'Visual customization and theming',
|
|
389
|
+
icon: 'palette',
|
|
390
|
+
sortOrder: 1,
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
id: generateUUID(),
|
|
394
|
+
name: 'Analytics',
|
|
395
|
+
slug: 'analytics',
|
|
396
|
+
description: 'Data tracking and reporting',
|
|
397
|
+
icon: 'chart',
|
|
398
|
+
sortOrder: 2,
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
id: generateUUID(),
|
|
402
|
+
name: 'SEO & Marketing',
|
|
403
|
+
slug: 'seo-marketing',
|
|
404
|
+
description: 'Search optimization and growth tools',
|
|
405
|
+
icon: 'trending-up',
|
|
406
|
+
sortOrder: 3,
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
id: generateUUID(),
|
|
410
|
+
name: 'Developer Tools',
|
|
411
|
+
slug: 'developer-tools',
|
|
412
|
+
description: 'Utilities for developers',
|
|
413
|
+
icon: 'code',
|
|
414
|
+
sortOrder: 4,
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
id: generateUUID(),
|
|
418
|
+
name: 'Integration',
|
|
419
|
+
slug: 'integration',
|
|
420
|
+
description: 'Third-party service integrations',
|
|
421
|
+
icon: 'plug',
|
|
422
|
+
sortOrder: 5,
|
|
423
|
+
},
|
|
424
|
+
]
|
|
425
|
+
|
|
426
|
+
await db.insert(pluginCategories).values(sampleCategories)
|
|
427
|
+
|
|
428
|
+
const samplePlugins = [
|
|
429
|
+
{
|
|
430
|
+
id: generateUUID(),
|
|
431
|
+
name: 'Theme Engine',
|
|
432
|
+
slug: 'theme-engine',
|
|
433
|
+
description:
|
|
434
|
+
'Advanced theming system with dark mode, custom color palettes, and responsive layout controls.',
|
|
435
|
+
readme:
|
|
436
|
+
'# Theme Engine\n\nA powerful theming plugin for customizing the look and feel of your application.',
|
|
437
|
+
authorId: '1',
|
|
438
|
+
authorName: 'superadmin',
|
|
439
|
+
repositoryUrl: 'https://github.com/example/theme-engine',
|
|
440
|
+
homepageUrl: 'https://theme-engine.example.com',
|
|
441
|
+
license: 'MIT',
|
|
442
|
+
version: '2.1.0',
|
|
443
|
+
status: 'approved' as const,
|
|
444
|
+
downloadCount: 1520,
|
|
445
|
+
viewCount: 4320,
|
|
446
|
+
featured: true,
|
|
447
|
+
screenshotUrl: 'https://picsum.photos/seed/theme/800/400',
|
|
448
|
+
tags: '["theme","dark-mode","customization"]',
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
id: generateUUID(),
|
|
452
|
+
slug: 'analytics-dashboard',
|
|
453
|
+
name: 'Analytics Dashboard',
|
|
454
|
+
description:
|
|
455
|
+
'Real-time analytics dashboard with charts, heatmaps, and user behavior tracking.',
|
|
456
|
+
readme: '# Analytics Dashboard\n\nTrack user engagement and visualize data in real time.',
|
|
457
|
+
authorId: '1',
|
|
458
|
+
authorName: 'superadmin',
|
|
459
|
+
repositoryUrl: 'https://github.com/example/analytics-dashboard',
|
|
460
|
+
homepageUrl: 'https://analytics.example.com',
|
|
461
|
+
license: 'Apache-2.0',
|
|
462
|
+
version: '1.5.3',
|
|
463
|
+
status: 'approved' as const,
|
|
464
|
+
downloadCount: 980,
|
|
465
|
+
viewCount: 2100,
|
|
466
|
+
featured: true,
|
|
467
|
+
screenshotUrl: 'https://picsum.photos/seed/analytics/800/400',
|
|
468
|
+
tags: '["analytics","dashboard","charts"]',
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
id: generateUUID(),
|
|
472
|
+
slug: 'seo-optimizer',
|
|
473
|
+
name: 'SEO Optimizer',
|
|
474
|
+
description:
|
|
475
|
+
'Automated SEO optimization with meta tags, sitemap generation, and structured data support.',
|
|
476
|
+
authorId: '2',
|
|
477
|
+
authorName: 'customerservice',
|
|
478
|
+
repositoryUrl: 'https://github.com/example/seo-optimizer',
|
|
479
|
+
license: 'MIT',
|
|
480
|
+
version: '1.2.0',
|
|
481
|
+
status: 'approved' as const,
|
|
482
|
+
downloadCount: 740,
|
|
483
|
+
viewCount: 1800,
|
|
484
|
+
featured: false,
|
|
485
|
+
screenshotUrl: 'https://picsum.photos/seed/seo/800/400',
|
|
486
|
+
tags: '["seo","meta","sitemap"]',
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
id: generateUUID(),
|
|
490
|
+
slug: 'api-toolkit',
|
|
491
|
+
name: 'API Toolkit',
|
|
492
|
+
description:
|
|
493
|
+
'Developer toolkit for building and testing REST APIs with auto-documentation and mock servers.',
|
|
494
|
+
authorId: '2',
|
|
495
|
+
authorName: 'customerservice',
|
|
496
|
+
repositoryUrl: 'https://github.com/example/api-toolkit',
|
|
497
|
+
homepageUrl: 'https://api-toolkit.example.com',
|
|
498
|
+
license: 'MIT',
|
|
499
|
+
version: '3.0.1',
|
|
500
|
+
status: 'approved' as const,
|
|
501
|
+
downloadCount: 2100,
|
|
502
|
+
viewCount: 5600,
|
|
503
|
+
featured: true,
|
|
504
|
+
screenshotUrl: 'https://picsum.photos/seed/apitoolkit/800/400',
|
|
505
|
+
tags: '["api","developer","testing","documentation"]',
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
id: generateUUID(),
|
|
509
|
+
slug: 'notification-hub',
|
|
510
|
+
name: 'Notification Hub',
|
|
511
|
+
description:
|
|
512
|
+
'Unified notification management supporting email, SMS, push, and in-app notifications.',
|
|
513
|
+
authorId: '3',
|
|
514
|
+
authorName: 'user1',
|
|
515
|
+
repositoryUrl: 'https://github.com/example/notification-hub',
|
|
516
|
+
license: 'ISC',
|
|
517
|
+
version: '1.0.0',
|
|
518
|
+
status: 'pending' as const,
|
|
519
|
+
downloadCount: 0,
|
|
520
|
+
viewCount: 50,
|
|
521
|
+
featured: false,
|
|
522
|
+
screenshotUrl: 'https://picsum.photos/seed/notify/800/400',
|
|
523
|
+
tags: '["notification","email","push"]',
|
|
524
|
+
},
|
|
525
|
+
]
|
|
526
|
+
|
|
527
|
+
await db.insert(plugins).values(samplePlugins)
|
|
528
|
+
|
|
529
|
+
const sampleVersions = [
|
|
530
|
+
{
|
|
531
|
+
id: generateUUID(),
|
|
532
|
+
pluginId: samplePlugins[0].id,
|
|
533
|
+
version: '2.1.0',
|
|
534
|
+
changelog: 'Added responsive layout controls',
|
|
535
|
+
status: 'approved' as const,
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
id: generateUUID(),
|
|
539
|
+
pluginId: samplePlugins[0].id,
|
|
540
|
+
version: '2.0.0',
|
|
541
|
+
changelog: 'Major rewrite with dark mode support',
|
|
542
|
+
status: 'approved' as const,
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
id: generateUUID(),
|
|
546
|
+
pluginId: samplePlugins[1].id,
|
|
547
|
+
version: '1.5.3',
|
|
548
|
+
changelog: 'Fixed heatmap rendering',
|
|
549
|
+
status: 'approved' as const,
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
id: generateUUID(),
|
|
553
|
+
pluginId: samplePlugins[1].id,
|
|
554
|
+
version: '1.5.0',
|
|
555
|
+
changelog: 'Added real-time tracking',
|
|
556
|
+
status: 'approved' as const,
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
id: generateUUID(),
|
|
560
|
+
pluginId: samplePlugins[2].id,
|
|
561
|
+
version: '1.2.0',
|
|
562
|
+
changelog: 'Structured data support',
|
|
563
|
+
status: 'approved' as const,
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
id: generateUUID(),
|
|
567
|
+
pluginId: samplePlugins[3].id,
|
|
568
|
+
version: '3.0.1',
|
|
569
|
+
changelog: 'Auto-documentation improvements',
|
|
570
|
+
status: 'approved' as const,
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
id: generateUUID(),
|
|
574
|
+
pluginId: samplePlugins[3].id,
|
|
575
|
+
version: '3.0.0',
|
|
576
|
+
changelog: 'Major version with mock server',
|
|
577
|
+
status: 'approved' as const,
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
id: generateUUID(),
|
|
581
|
+
pluginId: samplePlugins[4].id,
|
|
582
|
+
version: '1.0.0',
|
|
583
|
+
changelog: 'Initial release',
|
|
584
|
+
status: 'pending' as const,
|
|
585
|
+
},
|
|
586
|
+
]
|
|
587
|
+
|
|
588
|
+
await db.insert(pluginVersions).values(sampleVersions)
|
|
589
|
+
|
|
590
|
+
const sampleMappings = [
|
|
591
|
+
{ pluginId: samplePlugins[0].id, categoryId: sampleCategories[0].id },
|
|
592
|
+
{ pluginId: samplePlugins[1].id, categoryId: sampleCategories[1].id },
|
|
593
|
+
{ pluginId: samplePlugins[2].id, categoryId: sampleCategories[2].id },
|
|
594
|
+
{ pluginId: samplePlugins[3].id, categoryId: sampleCategories[3].id },
|
|
595
|
+
{ pluginId: samplePlugins[4].id, categoryId: sampleCategories[4].id },
|
|
596
|
+
]
|
|
597
|
+
|
|
598
|
+
await db.insert(pluginCategoryMappings).values(sampleMappings)
|
|
599
|
+
|
|
600
|
+
log.info({}, 'Plugin seeding complete!')
|
|
601
|
+
}
|
|
602
|
+
|
|
358
603
|
export async function initializeDatabase() {
|
|
359
604
|
const db = await getDb()
|
|
360
605
|
|
|
@@ -402,11 +647,13 @@ export async function initializeDatabase() {
|
|
|
402
647
|
log.info({}, 'Database initialization complete!')
|
|
403
648
|
|
|
404
649
|
log.info({}, 'Seeding module data...')
|
|
405
|
-
|
|
406
|
-
seedOrdersIfEmpty(),
|
|
407
|
-
seedTicketsIfEmpty(),
|
|
408
|
-
seedDisputesIfEmpty(),
|
|
409
|
-
seedContentsIfEmpty(),
|
|
410
|
-
|
|
650
|
+
const moduleSeeders = [
|
|
651
|
+
() => import('../module-order/services/order-service').then(m => m.seedOrdersIfEmpty()),
|
|
652
|
+
() => import('../module-ticket/services/ticket-service').then(m => m.seedTicketsIfEmpty()),
|
|
653
|
+
() => import('../module-dispute/services/dispute-service').then(m => m.seedDisputesIfEmpty()),
|
|
654
|
+
() => import('../module-content/services/content-service').then(m => m.seedContentsIfEmpty()),
|
|
655
|
+
() => seedPluginsIfEmpty(),
|
|
656
|
+
]
|
|
657
|
+
await Promise.all(moduleSeeders.map(fn => fn().catch(() => {})))
|
|
411
658
|
log.info({}, 'Module data seeding complete!')
|
|
412
659
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core'
|
|
2
|
+
import { sql } from 'drizzle-orm'
|
|
3
|
+
|
|
4
|
+
export const developers = sqliteTable('developers', {
|
|
5
|
+
id: text('id').primaryKey(),
|
|
6
|
+
username: text('username').notNull().unique(),
|
|
7
|
+
email: text('email').notNull().unique(),
|
|
8
|
+
passwordHash: text('password_hash').notNull(),
|
|
9
|
+
role: text('role').notNull().default('developer'),
|
|
10
|
+
apiKey: text('api_key').notNull().unique(),
|
|
11
|
+
createdAt: integer('created_at', { mode: 'timestamp' })
|
|
12
|
+
.notNull()
|
|
13
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
14
|
+
updatedAt: integer('updated_at', { mode: 'timestamp' })
|
|
15
|
+
.notNull()
|
|
16
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
export type DeveloperTable = typeof developers.$inferSelect
|
|
20
|
+
export type NewDeveloper = typeof developers.$inferInsert
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { sqliteTable, text, integer, uniqueIndex } from 'drizzle-orm/sqlite-core'
|
|
2
|
+
import { sql } from 'drizzle-orm'
|
|
3
|
+
|
|
4
|
+
export const pluginStatus = ['pending', 'approved', 'rejected'] as const
|
|
5
|
+
export type PluginStatus = (typeof pluginStatus)[number]
|
|
6
|
+
|
|
7
|
+
export const plugins = sqliteTable('plugins', {
|
|
8
|
+
id: text('id').primaryKey(),
|
|
9
|
+
name: text('name').notNull(),
|
|
10
|
+
slug: text('slug').notNull().unique(),
|
|
11
|
+
description: text('description').notNull().default(''),
|
|
12
|
+
readme: text('readme'),
|
|
13
|
+
authorId: text('author_id').notNull(),
|
|
14
|
+
authorName: text('author_name').notNull(),
|
|
15
|
+
repositoryUrl: text('repository_url'),
|
|
16
|
+
homepageUrl: text('homepage_url'),
|
|
17
|
+
npmPackage: text('npm_package'),
|
|
18
|
+
license: text('license'),
|
|
19
|
+
version: text('version').notNull().default('0.0.1'),
|
|
20
|
+
status: text('status', { enum: pluginStatus }).notNull().default('pending'),
|
|
21
|
+
downloadCount: integer('download_count').notNull().default(0),
|
|
22
|
+
viewCount: integer('view_count').notNull().default(0),
|
|
23
|
+
featured: integer('featured', { mode: 'boolean' }).notNull().default(false),
|
|
24
|
+
screenshotUrl: text('screenshot_url'),
|
|
25
|
+
siteUrls: text('site_urls'),
|
|
26
|
+
tags: text('tags'),
|
|
27
|
+
commands: text('commands'),
|
|
28
|
+
rejectReason: text('reject_reason'),
|
|
29
|
+
createdAt: integer('created_at', { mode: 'timestamp' })
|
|
30
|
+
.notNull()
|
|
31
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
32
|
+
updatedAt: integer('updated_at', { mode: 'timestamp' })
|
|
33
|
+
.notNull()
|
|
34
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
export const pluginVersions = sqliteTable(
|
|
38
|
+
'plugin_versions',
|
|
39
|
+
{
|
|
40
|
+
id: text('id').primaryKey(),
|
|
41
|
+
pluginId: text('plugin_id')
|
|
42
|
+
.notNull()
|
|
43
|
+
.references(() => plugins.id, { onDelete: 'cascade' }),
|
|
44
|
+
version: text('version').notNull(),
|
|
45
|
+
changelog: text('changelog'),
|
|
46
|
+
packageUrl: text('package_url'),
|
|
47
|
+
fileSize: integer('file_size'),
|
|
48
|
+
checksum: text('checksum'),
|
|
49
|
+
status: text('status', { enum: pluginStatus }).notNull().default('pending'),
|
|
50
|
+
publishedAt: integer('published_at', { mode: 'timestamp' })
|
|
51
|
+
.notNull()
|
|
52
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
53
|
+
},
|
|
54
|
+
table => ({
|
|
55
|
+
pluginVersionUnique: uniqueIndex('plugin_version_unique').on(table.pluginId, table.version),
|
|
56
|
+
})
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
export const pluginReviews = sqliteTable(
|
|
60
|
+
'plugin_reviews',
|
|
61
|
+
{
|
|
62
|
+
id: text('id').primaryKey(),
|
|
63
|
+
pluginId: text('plugin_id')
|
|
64
|
+
.notNull()
|
|
65
|
+
.references(() => plugins.id, { onDelete: 'cascade' }),
|
|
66
|
+
userId: text('user_id').notNull(),
|
|
67
|
+
userName: text('user_name').notNull(),
|
|
68
|
+
rating: integer('rating').notNull(),
|
|
69
|
+
title: text('title'),
|
|
70
|
+
content: text('content'),
|
|
71
|
+
createdAt: integer('created_at', { mode: 'timestamp' })
|
|
72
|
+
.notNull()
|
|
73
|
+
.default(sql`(unixepoch() * 1000)`),
|
|
74
|
+
},
|
|
75
|
+
table => ({
|
|
76
|
+
pluginUserUnique: uniqueIndex('plugin_review_user_unique').on(table.pluginId, table.userId),
|
|
77
|
+
})
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
export const pluginCategories = sqliteTable('plugin_categories', {
|
|
81
|
+
id: text('id').primaryKey(),
|
|
82
|
+
name: text('name').notNull().unique(),
|
|
83
|
+
slug: text('slug').notNull().unique(),
|
|
84
|
+
description: text('description'),
|
|
85
|
+
icon: text('icon'),
|
|
86
|
+
sortOrder: integer('sort_order').notNull().default(0),
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
export const pluginCategoryMappings = sqliteTable(
|
|
90
|
+
'plugin_category_mappings',
|
|
91
|
+
{
|
|
92
|
+
pluginId: text('plugin_id')
|
|
93
|
+
.notNull()
|
|
94
|
+
.references(() => plugins.id, { onDelete: 'cascade' }),
|
|
95
|
+
categoryId: text('category_id')
|
|
96
|
+
.notNull()
|
|
97
|
+
.references(() => pluginCategories.id, { onDelete: 'cascade' }),
|
|
98
|
+
},
|
|
99
|
+
table => ({
|
|
100
|
+
mappingUnique: uniqueIndex('plugin_category_mapping_unique').on(
|
|
101
|
+
table.pluginId,
|
|
102
|
+
table.categoryId
|
|
103
|
+
),
|
|
104
|
+
})
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
export type PluginTable = typeof plugins.$inferSelect
|
|
108
|
+
export type NewPlugin = typeof plugins.$inferInsert
|
|
109
|
+
export type PluginVersionTable = typeof pluginVersions.$inferSelect
|
|
110
|
+
export type NewPluginVersion = typeof pluginVersions.$inferInsert
|
|
111
|
+
export type PluginReviewTable = typeof pluginReviews.$inferSelect
|
|
112
|
+
export type NewPluginReview = typeof pluginReviews.$inferInsert
|
|
113
|
+
export type PluginCategoryTable = typeof pluginCategories.$inferSelect
|
|
114
|
+
export type NewPluginCategory = typeof pluginCategories.$inferInsert
|
|
@@ -191,6 +191,87 @@ export async function setupTestDatabase(): Promise<void> {
|
|
|
191
191
|
created_at INTEGER DEFAULT (unixepoch() * 1000) NOT NULL,
|
|
192
192
|
updated_at INTEGER DEFAULT (unixepoch() * 1000) NOT NULL
|
|
193
193
|
);
|
|
194
|
+
|
|
195
|
+
CREATE TABLE IF NOT EXISTS developers (
|
|
196
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
197
|
+
username TEXT NOT NULL UNIQUE,
|
|
198
|
+
email TEXT NOT NULL UNIQUE,
|
|
199
|
+
password_hash TEXT NOT NULL,
|
|
200
|
+
role TEXT DEFAULT 'developer' NOT NULL,
|
|
201
|
+
api_key TEXT NOT NULL UNIQUE,
|
|
202
|
+
created_at INTEGER DEFAULT (unixepoch() * 1000) NOT NULL,
|
|
203
|
+
updated_at INTEGER DEFAULT (unixepoch() * 1000) NOT NULL
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
CREATE TABLE IF NOT EXISTS plugins (
|
|
207
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
208
|
+
name TEXT NOT NULL,
|
|
209
|
+
slug TEXT NOT NULL UNIQUE,
|
|
210
|
+
description TEXT NOT NULL DEFAULT '',
|
|
211
|
+
readme TEXT,
|
|
212
|
+
author_id TEXT NOT NULL,
|
|
213
|
+
author_name TEXT NOT NULL,
|
|
214
|
+
repository_url TEXT,
|
|
215
|
+
homepage_url TEXT,
|
|
216
|
+
npm_package TEXT,
|
|
217
|
+
license TEXT,
|
|
218
|
+
version TEXT NOT NULL DEFAULT '0.0.1',
|
|
219
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
220
|
+
download_count INTEGER NOT NULL DEFAULT 0,
|
|
221
|
+
view_count INTEGER NOT NULL DEFAULT 0,
|
|
222
|
+
featured INTEGER NOT NULL DEFAULT 0,
|
|
223
|
+
screenshot_url TEXT,
|
|
224
|
+
site_urls TEXT,
|
|
225
|
+
tags TEXT,
|
|
226
|
+
commands TEXT,
|
|
227
|
+
reject_reason TEXT,
|
|
228
|
+
created_at INTEGER DEFAULT (unixepoch() * 1000) NOT NULL,
|
|
229
|
+
updated_at INTEGER DEFAULT (unixepoch() * 1000) NOT NULL
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
CREATE TABLE IF NOT EXISTS plugin_versions (
|
|
233
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
234
|
+
plugin_id TEXT NOT NULL,
|
|
235
|
+
version TEXT NOT NULL,
|
|
236
|
+
changelog TEXT,
|
|
237
|
+
package_url TEXT,
|
|
238
|
+
file_size INTEGER,
|
|
239
|
+
checksum TEXT,
|
|
240
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
241
|
+
published_at INTEGER DEFAULT (unixepoch() * 1000) NOT NULL,
|
|
242
|
+
FOREIGN KEY (plugin_id) REFERENCES plugins(id) ON DELETE CASCADE,
|
|
243
|
+
UNIQUE(plugin_id, version)
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
CREATE TABLE IF NOT EXISTS plugin_reviews (
|
|
247
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
248
|
+
plugin_id TEXT NOT NULL,
|
|
249
|
+
user_id TEXT NOT NULL,
|
|
250
|
+
user_name TEXT NOT NULL,
|
|
251
|
+
rating INTEGER NOT NULL,
|
|
252
|
+
title TEXT,
|
|
253
|
+
content TEXT,
|
|
254
|
+
created_at INTEGER DEFAULT (unixepoch() * 1000) NOT NULL,
|
|
255
|
+
FOREIGN KEY (plugin_id) REFERENCES plugins(id) ON DELETE CASCADE,
|
|
256
|
+
UNIQUE(plugin_id, user_id)
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
CREATE TABLE IF NOT EXISTS plugin_categories (
|
|
260
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
261
|
+
name TEXT NOT NULL UNIQUE,
|
|
262
|
+
slug TEXT NOT NULL UNIQUE,
|
|
263
|
+
description TEXT,
|
|
264
|
+
icon TEXT,
|
|
265
|
+
sort_order INTEGER NOT NULL DEFAULT 0
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
CREATE TABLE IF NOT EXISTS plugin_category_mappings (
|
|
269
|
+
plugin_id TEXT NOT NULL,
|
|
270
|
+
category_id TEXT NOT NULL,
|
|
271
|
+
FOREIGN KEY (plugin_id) REFERENCES plugins(id) ON DELETE CASCADE,
|
|
272
|
+
FOREIGN KEY (category_id) REFERENCES plugin_categories(id) ON DELETE CASCADE,
|
|
273
|
+
UNIQUE(plugin_id, category_id)
|
|
274
|
+
);
|
|
194
275
|
`
|
|
195
276
|
|
|
196
277
|
const statements = migrationSQL.split(';').filter(s => s.trim())
|
|
@@ -643,5 +724,15 @@ export async function cleanupTestDatabase(): Promise<void> {
|
|
|
643
724
|
await client.execute('DELETE FROM orders')
|
|
644
725
|
await client.execute('DELETE FROM disputes')
|
|
645
726
|
await client.execute('DELETE FROM contents')
|
|
727
|
+
try {
|
|
728
|
+
await client.execute('DELETE FROM plugin_category_mappings')
|
|
729
|
+
await client.execute('DELETE FROM plugin_reviews')
|
|
730
|
+
await client.execute('DELETE FROM plugin_versions')
|
|
731
|
+
await client.execute('DELETE FROM plugins')
|
|
732
|
+
await client.execute('DELETE FROM plugin_categories')
|
|
733
|
+
await client.execute('DELETE FROM developers')
|
|
734
|
+
} catch {
|
|
735
|
+
// Plugin tables may not exist in all test environments
|
|
736
|
+
}
|
|
646
737
|
}
|
|
647
738
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @framework-baseline e350401421193896
|
|
3
3
|
* @framework-modify
|
|
4
|
-
* @reason
|
|
5
|
-
* @impact 影响 Cloudflare Workers
|
|
4
|
+
* @reason 集成 ISR 缓存层,为页面路由提供增量静态再生能力
|
|
5
|
+
* @impact 影响 Cloudflare Workers 环境的页面响应流程,新增 ISR 缓存查找/存储/失效逻辑
|
|
6
6
|
*
|
|
7
7
|
* Note: In Cloudflare Workers, each request runs in its own isolate,
|
|
8
8
|
* so globalThis is request-scoped and there's no race condition risk.
|
|
@@ -15,6 +15,9 @@ import { getDb } from '../db/driver-cloudflare'
|
|
|
15
15
|
import { RealtimeDurableObject } from '@server/core'
|
|
16
16
|
import { setRuntimeAdapter } from '@server/core/runtime'
|
|
17
17
|
import { getCloudflareRuntimeAdapter } from '@server/core/runtime-cloudflare'
|
|
18
|
+
import { createISRCache, isISRRoute } from '@server/core/isr-cache'
|
|
19
|
+
import { renderPage } from '@server/core/ssr-renderer'
|
|
20
|
+
import { setISRCache } from '@server/core/isr-invalidation'
|
|
18
21
|
|
|
19
22
|
export interface CloudflareBindings extends AppBindings {
|
|
20
23
|
DB: D1Database
|
|
@@ -24,8 +27,15 @@ export interface CloudflareBindings extends AppBindings {
|
|
|
24
27
|
const runtimeAdapter = getCloudflareRuntimeAdapter()
|
|
25
28
|
setRuntimeAdapter(runtimeAdapter)
|
|
26
29
|
|
|
30
|
+
runtimeAdapter.handleWS('/api/chat/ws')
|
|
31
|
+
runtimeAdapter.handleSSE('/api/notifications/stream')
|
|
32
|
+
runtimeAdapter.handleSSE('/api/admin/notifications/stream')
|
|
33
|
+
|
|
27
34
|
const app = createApp<CloudflareBindings>()
|
|
28
35
|
|
|
36
|
+
const isrCache = createISRCache()
|
|
37
|
+
setISRCache(isrCache)
|
|
38
|
+
|
|
29
39
|
const wrappedApp = app
|
|
30
40
|
.use('*', async (c, next) => {
|
|
31
41
|
;(globalThis as unknown as { DB: D1Database }).DB = c.env.DB
|
|
@@ -41,21 +51,73 @@ const wrappedApp = app
|
|
|
41
51
|
.onError((err, c) => {
|
|
42
52
|
console.error('Server error:', err)
|
|
43
53
|
c.res.headers.set('Content-Type', 'application/json')
|
|
44
|
-
|
|
45
|
-
|
|
54
|
+
let statusCode = 500
|
|
55
|
+
if ('statusCode' in err && typeof (err as { statusCode: unknown }).statusCode === 'number') {
|
|
56
|
+
statusCode = (err as { statusCode: number }).statusCode
|
|
57
|
+
} else if ('status' in err && typeof (err as { status: unknown }).status === 'number') {
|
|
58
|
+
statusCode = (err as { status: number }).status
|
|
59
|
+
}
|
|
46
60
|
const message = err.message || 'Internal server error'
|
|
47
|
-
|
|
48
|
-
|
|
61
|
+
return c.json(
|
|
62
|
+
{ success: false as const, error: message, status: statusCode },
|
|
63
|
+
statusCode as 500
|
|
64
|
+
)
|
|
49
65
|
})
|
|
50
66
|
|
|
51
67
|
export default {
|
|
52
68
|
fetch: async (request: Request, env: CloudflareBindings, ctx: ExecutionContext) => {
|
|
69
|
+
;(globalThis as unknown as { DB: D1Database }).DB = env.DB
|
|
70
|
+
|
|
53
71
|
const url = new URL(request.url)
|
|
72
|
+
const pathname = url.pathname
|
|
54
73
|
|
|
55
|
-
if (
|
|
74
|
+
if (pathname.startsWith('/api/') || pathname === '/health') {
|
|
56
75
|
return wrappedApp.fetch(request, env, ctx)
|
|
57
76
|
}
|
|
58
77
|
|
|
78
|
+
if (env.ASSETS) {
|
|
79
|
+
if (
|
|
80
|
+
pathname.startsWith('/assets/') ||
|
|
81
|
+
pathname.endsWith('.js') ||
|
|
82
|
+
pathname.endsWith('.css') ||
|
|
83
|
+
pathname.endsWith('.svg') ||
|
|
84
|
+
pathname.endsWith('.png') ||
|
|
85
|
+
pathname.endsWith('.ico') ||
|
|
86
|
+
pathname === '/vite.svg'
|
|
87
|
+
) {
|
|
88
|
+
const assetResponse = await env.ASSETS.fetch(request)
|
|
89
|
+
if (assetResponse.status !== 404) {
|
|
90
|
+
return assetResponse
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (isISRRoute(pathname)) {
|
|
96
|
+
const result = await isrCache.lookup(pathname)
|
|
97
|
+
|
|
98
|
+
if (result.status === 'fresh' && result.html) {
|
|
99
|
+
return new Response(result.html, {
|
|
100
|
+
headers: { 'Content-Type': 'text/html;charset=UTF-8', 'X-ISR-Status': 'fresh' },
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (result.status === 'stale' && result.html) {
|
|
105
|
+
ctx.waitUntil(regeneratePage(pathname, env))
|
|
106
|
+
return new Response(result.html, {
|
|
107
|
+
headers: { 'Content-Type': 'text/html;charset=UTF-8', 'X-ISR-Status': 'stale' },
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const rendered = await renderPage(pathname)
|
|
112
|
+
|
|
113
|
+
ctx.waitUntil(isrCache.store(pathname, rendered.html))
|
|
114
|
+
|
|
115
|
+
return new Response(rendered.html, {
|
|
116
|
+
status: rendered.status,
|
|
117
|
+
headers: { ...rendered.headers, 'X-ISR-Status': 'miss' },
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
59
121
|
if (env.ASSETS) {
|
|
60
122
|
const assetResponse = await env.ASSETS.fetch(request)
|
|
61
123
|
if (assetResponse.status !== 404) {
|
|
@@ -67,5 +129,15 @@ export default {
|
|
|
67
129
|
},
|
|
68
130
|
}
|
|
69
131
|
|
|
132
|
+
async function regeneratePage(pathname: string, _env: CloudflareBindings): Promise<void> {
|
|
133
|
+
try {
|
|
134
|
+
const rendered = await renderPage(pathname)
|
|
135
|
+
await isrCache.store(pathname, rendered.html)
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error('ISR regeneration failed:', error)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export { isrCache }
|
|
70
142
|
export { RealtimeDurableObject, getDb }
|
|
71
143
|
export type AppType = typeof wrappedApp
|