@robsun/create-keystone-app 0.1.18 → 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 (44) hide show
  1. package/README.md +4 -5
  2. package/bin/create-keystone-app.js +1 -80
  3. package/package.json +1 -1
  4. package/template/README.md +5 -13
  5. package/template/apps/server/config.example.yaml +0 -1
  6. package/template/apps/server/config.yaml +0 -1
  7. package/template/apps/server/internal/modules/example/api/handler/item_handler.go +162 -0
  8. package/template/apps/server/internal/modules/example/bootstrap/migrations/item.go +21 -0
  9. package/template/apps/server/internal/modules/example/bootstrap/seeds/item.go +33 -0
  10. package/template/apps/server/internal/modules/example/domain/models/item.go +30 -0
  11. package/template/apps/server/internal/modules/{demo → example}/domain/service/errors.go +1 -1
  12. package/template/apps/server/internal/modules/example/domain/service/item_service.go +110 -0
  13. package/template/apps/server/internal/modules/example/infra/repository/item_repository.go +49 -0
  14. package/template/apps/server/internal/modules/example/module.go +55 -17
  15. package/template/apps/server/internal/modules/manifest.go +1 -3
  16. package/template/apps/web/src/app.config.ts +1 -1
  17. package/template/apps/web/src/main.tsx +0 -1
  18. package/template/apps/web/src/modules/example/help/faq.md +23 -0
  19. package/template/apps/web/src/modules/example/help/items.md +26 -0
  20. package/template/apps/web/src/modules/example/help/overview.md +18 -4
  21. package/template/apps/web/src/modules/example/pages/ExampleItemsPage.tsx +227 -0
  22. package/template/apps/web/src/modules/example/routes.tsx +33 -10
  23. package/template/apps/web/src/modules/example/services/exampleItems.ts +32 -0
  24. package/template/apps/web/src/modules/example/types.ts +10 -0
  25. package/template/docs/CONVENTIONS.md +44 -0
  26. package/template/docs/GETTING_STARTED.md +54 -0
  27. package/template/package.json +1 -1
  28. package/template/scripts/check-modules.js +7 -1
  29. package/template/apps/server/internal/modules/demo/api/handler/task_handler.go +0 -152
  30. package/template/apps/server/internal/modules/demo/bootstrap/migrations/task.go +0 -21
  31. package/template/apps/server/internal/modules/demo/bootstrap/seeds/task.go +0 -33
  32. package/template/apps/server/internal/modules/demo/domain/models/task.go +0 -30
  33. package/template/apps/server/internal/modules/demo/domain/service/task_service.go +0 -95
  34. package/template/apps/server/internal/modules/demo/infra/repository/task_repository.go +0 -49
  35. package/template/apps/server/internal/modules/demo/module.go +0 -91
  36. package/template/apps/server/internal/modules/example/handlers.go +0 -19
  37. package/template/apps/web/src/modules/demo/help/overview.md +0 -12
  38. package/template/apps/web/src/modules/demo/index.ts +0 -7
  39. package/template/apps/web/src/modules/demo/pages/DemoTasksPage.tsx +0 -185
  40. package/template/apps/web/src/modules/demo/routes.tsx +0 -43
  41. package/template/apps/web/src/modules/demo/services/demoTasks.ts +0 -28
  42. package/template/apps/web/src/modules/demo/types.ts +0 -9
  43. package/template/apps/web/src/modules/example/pages/ExamplePage.tsx +0 -41
  44. package/template/apps/web/src/modules/example/services/api.ts +0 -8
@@ -41,7 +41,7 @@ function parseServerModules(content) {
41
41
  }
42
42
 
43
43
  function formatList(items) {
44
- return items.map((item) => `- ${item}`).join('\n')
44
+ return items.map((item) => ` - ${item}`).join('\n')
45
45
  }
46
46
 
47
47
  try {
@@ -60,6 +60,10 @@ try {
60
60
  }
61
61
 
62
62
  console.error('Module configuration mismatch:')
63
+ console.error('\napps/web/src/app.config.ts modules.enabled:')
64
+ console.error(formatList(webModules))
65
+ console.error('\napps/server/config.yaml modules.enabled:')
66
+ console.error(formatList(serverModules))
63
67
  if (missingInServer.length > 0) {
64
68
  console.error('\nMissing in apps/server/config.yaml:')
65
69
  console.error(formatList(missingInServer))
@@ -68,9 +72,11 @@ try {
68
72
  console.error('\nMissing in apps/web/src/app.config.ts:')
69
73
  console.error(formatList(missingInWeb))
70
74
  }
75
+ console.error('\nFix: keep module names identical in both files and register modules in main.tsx/manifest.go.')
71
76
  process.exit(1)
72
77
  } catch (err) {
73
78
  console.error('Failed to check modules:')
74
79
  console.error(err instanceof Error ? err.message : String(err))
80
+ console.error('Hint: verify apps/web/src/app.config.ts and apps/server/config.yaml exist and are valid.')
75
81
  process.exit(2)
76
82
  }
@@ -1,152 +0,0 @@
1
- package handler
2
-
3
- import (
4
- "errors"
5
-
6
- "github.com/gin-gonic/gin"
7
-
8
- hcommon "github.com/robsuncn/keystone/api/handler/common"
9
- "github.com/robsuncn/keystone/api/response"
10
-
11
- "__APP_NAME__/apps/server/internal/modules/demo/domain/models"
12
- "__APP_NAME__/apps/server/internal/modules/demo/domain/service"
13
- )
14
-
15
- type TaskHandler struct {
16
- tasks *service.TaskService
17
- }
18
-
19
- func NewTaskHandler(tasks *service.TaskService) *TaskHandler {
20
- if tasks == nil {
21
- return nil
22
- }
23
- return &TaskHandler{tasks: tasks}
24
- }
25
-
26
- type taskInput struct {
27
- Title string `json:"title"`
28
- Status models.TaskStatus `json:"status"`
29
- }
30
-
31
- const defaultTenantID uint = 1
32
-
33
- func (h *TaskHandler) List(c *gin.Context) {
34
- if h == nil || h.tasks == nil {
35
- response.ServiceUnavailable(c, "demo tasks unavailable")
36
- return
37
- }
38
- tenantID := resolveTenantID(c)
39
-
40
- items, err := h.tasks.List(c.Request.Context(), tenantID)
41
- if err != nil {
42
- response.InternalError(c, "failed to load demo tasks")
43
- return
44
- }
45
-
46
- response.Success(c, gin.H{"items": items})
47
- }
48
-
49
- func (h *TaskHandler) Create(c *gin.Context) {
50
- if h == nil || h.tasks == nil {
51
- response.ServiceUnavailable(c, "demo tasks unavailable")
52
- return
53
- }
54
- tenantID := resolveTenantID(c)
55
-
56
- var input taskInput
57
- if err := c.ShouldBindJSON(&input); err != nil {
58
- response.BadRequest(c, "invalid payload")
59
- return
60
- }
61
-
62
- task, err := h.tasks.Create(c.Request.Context(), tenantID, service.TaskInput{
63
- Title: input.Title,
64
- Status: input.Status,
65
- })
66
- if err != nil {
67
- switch {
68
- case errors.Is(err, service.ErrTitleRequired):
69
- response.BadRequest(c, "title is required")
70
- case errors.Is(err, service.ErrStatusInvalid):
71
- response.BadRequest(c, "invalid status")
72
- default:
73
- response.InternalError(c, "failed to create demo task")
74
- }
75
- return
76
- }
77
-
78
- response.Created(c, task)
79
- }
80
-
81
- func (h *TaskHandler) Update(c *gin.Context) {
82
- if h == nil || h.tasks == nil {
83
- response.ServiceUnavailable(c, "demo tasks unavailable")
84
- return
85
- }
86
- tenantID := resolveTenantID(c)
87
-
88
- id, err := hcommon.ParseUintParam(c, "id")
89
- if err != nil || id == 0 {
90
- response.BadRequest(c, "invalid id")
91
- return
92
- }
93
-
94
- var input taskInput
95
- if err := c.ShouldBindJSON(&input); err != nil {
96
- response.BadRequest(c, "invalid payload")
97
- return
98
- }
99
-
100
- task, err := h.tasks.Update(c.Request.Context(), tenantID, id, service.TaskUpdateInput{
101
- Title: input.Title,
102
- Status: input.Status,
103
- })
104
- if err != nil {
105
- switch {
106
- case errors.Is(err, service.ErrTaskNotFound):
107
- response.NotFound(c, "task not found")
108
- case errors.Is(err, service.ErrStatusInvalid):
109
- response.BadRequest(c, "invalid status")
110
- default:
111
- response.InternalError(c, "failed to update demo task")
112
- }
113
- return
114
- }
115
-
116
- response.SuccessWithMessage(c, "updated", task)
117
- }
118
-
119
- func (h *TaskHandler) Delete(c *gin.Context) {
120
- if h == nil || h.tasks == nil {
121
- response.ServiceUnavailable(c, "demo tasks unavailable")
122
- return
123
- }
124
- tenantID := resolveTenantID(c)
125
-
126
- id, err := hcommon.ParseUintParam(c, "id")
127
- if err != nil || id == 0 {
128
- response.BadRequest(c, "invalid id")
129
- return
130
- }
131
-
132
- if err := h.tasks.Delete(c.Request.Context(), tenantID, id); err != nil {
133
- if errors.Is(err, service.ErrTaskNotFound) {
134
- response.NotFound(c, "task not found")
135
- return
136
- }
137
- response.InternalError(c, "failed to delete demo task")
138
- return
139
- }
140
-
141
- response.SuccessWithMessage(c, "deleted", gin.H{"id": id})
142
- }
143
-
144
- func resolveTenantID(c *gin.Context) uint {
145
- if c == nil {
146
- return defaultTenantID
147
- }
148
- if tenantID, ok := hcommon.GetTenantID(c); ok && tenantID > 0 {
149
- return tenantID
150
- }
151
- return defaultTenantID
152
- }
@@ -1,21 +0,0 @@
1
- package migrations
2
-
3
- import (
4
- "fmt"
5
- "log"
6
-
7
- "gorm.io/gorm"
8
-
9
- "__APP_NAME__/apps/server/internal/modules/demo/domain/models"
10
- )
11
-
12
- func Migrate(db *gorm.DB) error {
13
- if db == nil {
14
- return nil
15
- }
16
- log.Println("[demo] Running migrations...")
17
- if err := db.AutoMigrate(&models.DemoTask{}); err != nil {
18
- return fmt.Errorf("auto migrate demo_tasks: %w", err)
19
- }
20
- return nil
21
- }
@@ -1,33 +0,0 @@
1
- package seeds
2
-
3
- import (
4
- "log"
5
-
6
- "gorm.io/gorm"
7
-
8
- "__APP_NAME__/apps/server/internal/modules/demo/domain/models"
9
- )
10
-
11
- func Seed(db *gorm.DB) error {
12
- if db == nil {
13
- return nil
14
- }
15
-
16
- var count int64
17
- if err := db.Model(&models.DemoTask{}).Count(&count).Error; err != nil {
18
- return err
19
- }
20
- if count > 0 {
21
- return nil
22
- }
23
-
24
- log.Println("[demo] Seeding initial data...")
25
- tasks := []models.DemoTask{
26
- {Title: "Set up project", Status: models.StatusTodo},
27
- {Title: "Review UI states", Status: models.StatusDone},
28
- }
29
- for i := range tasks {
30
- tasks[i].TenantID = 1
31
- }
32
- return db.Create(&tasks).Error
33
- }
@@ -1,30 +0,0 @@
1
- package models
2
-
3
- import "github.com/robsuncn/keystone/domain/models"
4
-
5
- type TaskStatus string
6
-
7
- const (
8
- StatusTodo TaskStatus = "todo"
9
- StatusInProgress TaskStatus = "in_progress"
10
- StatusDone TaskStatus = "done"
11
- )
12
-
13
- func (s TaskStatus) IsValid() bool {
14
- switch s {
15
- case StatusTodo, StatusInProgress, StatusDone:
16
- return true
17
- default:
18
- return false
19
- }
20
- }
21
-
22
- type DemoTask struct {
23
- models.BaseModel
24
- Title string `gorm:"size:200;not null" json:"title"`
25
- Status TaskStatus `gorm:"size:20;not null;default:'todo'" json:"status"`
26
- }
27
-
28
- func (DemoTask) TableName() string {
29
- return "demo_tasks"
30
- }
@@ -1,95 +0,0 @@
1
- package service
2
-
3
- import (
4
- "context"
5
- "strings"
6
-
7
- "__APP_NAME__/apps/server/internal/modules/demo/domain/models"
8
- )
9
-
10
- type TaskRepository interface {
11
- List(ctx context.Context, tenantID uint) ([]models.DemoTask, error)
12
- FindByID(tenantID, id uint) (*models.DemoTask, error)
13
- Create(ctx context.Context, task *models.DemoTask) error
14
- Update(ctx context.Context, task *models.DemoTask) error
15
- Delete(ctx context.Context, task *models.DemoTask) error
16
- }
17
-
18
- type TaskService struct {
19
- tasks TaskRepository
20
- }
21
-
22
- type TaskInput struct {
23
- Title string
24
- Status models.TaskStatus
25
- }
26
-
27
- type TaskUpdateInput struct {
28
- Title string
29
- Status models.TaskStatus
30
- }
31
-
32
- func NewTaskService(tasks TaskRepository) *TaskService {
33
- return &TaskService{tasks: tasks}
34
- }
35
-
36
- func (s *TaskService) List(ctx context.Context, tenantID uint) ([]models.DemoTask, error) {
37
- return s.tasks.List(ctx, tenantID)
38
- }
39
-
40
- func (s *TaskService) Create(ctx context.Context, tenantID uint, input TaskInput) (*models.DemoTask, error) {
41
- title := strings.TrimSpace(input.Title)
42
- if title == "" {
43
- return nil, ErrTitleRequired
44
- }
45
-
46
- status := input.Status
47
- if status == "" {
48
- status = models.StatusTodo
49
- }
50
- if !status.IsValid() {
51
- return nil, ErrStatusInvalid
52
- }
53
-
54
- task := &models.DemoTask{
55
- Title: title,
56
- Status: status,
57
- }
58
- task.TenantID = tenantID
59
-
60
- if err := s.tasks.Create(ctx, task); err != nil {
61
- return nil, err
62
- }
63
- return task, nil
64
- }
65
-
66
- func (s *TaskService) Update(ctx context.Context, tenantID, id uint, input TaskUpdateInput) (*models.DemoTask, error) {
67
- task, err := s.tasks.FindByID(tenantID, id)
68
- if err != nil {
69
- return nil, err
70
- }
71
-
72
- if title := strings.TrimSpace(input.Title); title != "" {
73
- task.Title = title
74
- }
75
-
76
- if input.Status != "" {
77
- if !input.Status.IsValid() {
78
- return nil, ErrStatusInvalid
79
- }
80
- task.Status = input.Status
81
- }
82
-
83
- if err := s.tasks.Update(ctx, task); err != nil {
84
- return nil, err
85
- }
86
- return task, nil
87
- }
88
-
89
- func (s *TaskService) Delete(ctx context.Context, tenantID, id uint) error {
90
- task, err := s.tasks.FindByID(tenantID, id)
91
- if err != nil {
92
- return err
93
- }
94
- return s.tasks.Delete(ctx, task)
95
- }
@@ -1,49 +0,0 @@
1
- package repository
2
-
3
- import (
4
- "context"
5
- "errors"
6
-
7
- "gorm.io/gorm"
8
-
9
- "__APP_NAME__/apps/server/internal/modules/demo/domain/models"
10
- "__APP_NAME__/apps/server/internal/modules/demo/domain/service"
11
- )
12
-
13
- type TaskRepository struct {
14
- db *gorm.DB
15
- }
16
-
17
- func NewTaskRepository(db *gorm.DB) *TaskRepository {
18
- return &TaskRepository{db: db}
19
- }
20
-
21
- func (r *TaskRepository) List(ctx context.Context, tenantID uint) ([]models.DemoTask, error) {
22
- var tasks []models.DemoTask
23
- err := r.db.WithContext(ctx).
24
- Where("tenant_id = ?", tenantID).
25
- Order("created_at desc").
26
- Find(&tasks).Error
27
- return tasks, err
28
- }
29
-
30
- func (r *TaskRepository) FindByID(tenantID, id uint) (*models.DemoTask, error) {
31
- var task models.DemoTask
32
- err := r.db.Where("tenant_id = ? AND id = ?", tenantID, id).First(&task).Error
33
- if errors.Is(err, gorm.ErrRecordNotFound) {
34
- return nil, service.ErrTaskNotFound
35
- }
36
- return &task, err
37
- }
38
-
39
- func (r *TaskRepository) Create(ctx context.Context, task *models.DemoTask) error {
40
- return r.db.WithContext(ctx).Create(task).Error
41
- }
42
-
43
- func (r *TaskRepository) Update(ctx context.Context, task *models.DemoTask) error {
44
- return r.db.WithContext(ctx).Save(task).Error
45
- }
46
-
47
- func (r *TaskRepository) Delete(ctx context.Context, task *models.DemoTask) error {
48
- return r.db.WithContext(ctx).Delete(task).Error
49
- }
@@ -1,91 +0,0 @@
1
- package demo
2
-
3
- import (
4
- "github.com/gin-gonic/gin"
5
- "gorm.io/gorm"
6
-
7
- "github.com/robsuncn/keystone/domain/permissions"
8
- "github.com/robsuncn/keystone/infra/jobs"
9
-
10
- demohandler "__APP_NAME__/apps/server/internal/modules/demo/api/handler"
11
- demomigrations "__APP_NAME__/apps/server/internal/modules/demo/bootstrap/migrations"
12
- demoseeds "__APP_NAME__/apps/server/internal/modules/demo/bootstrap/seeds"
13
- demomodels "__APP_NAME__/apps/server/internal/modules/demo/domain/models"
14
- demoservice "__APP_NAME__/apps/server/internal/modules/demo/domain/service"
15
- demorepository "__APP_NAME__/apps/server/internal/modules/demo/infra/repository"
16
- )
17
-
18
- type Module struct {
19
- tasks *demoservice.TaskService
20
- }
21
-
22
- func NewModule() *Module {
23
- return &Module{}
24
- }
25
-
26
- func (m *Module) Name() string {
27
- return "demo"
28
- }
29
-
30
- func (m *Module) RegisterRoutes(rg *gin.RouterGroup) {
31
- if rg == nil || m == nil {
32
- return
33
- }
34
- handler := demohandler.NewTaskHandler(m.tasks)
35
- if handler == nil {
36
- return
37
- }
38
- group := rg.Group("/demo")
39
- group.GET("/tasks", handler.List)
40
- group.POST("/tasks", handler.Create)
41
- group.PATCH("/tasks/:id", handler.Update)
42
- group.DELETE("/tasks/:id", handler.Delete)
43
- }
44
-
45
- func (m *Module) RegisterModels() []interface{} {
46
- return []interface{}{&demomodels.DemoTask{}}
47
- }
48
-
49
- func (m *Module) RegisterPermissions(reg *permissions.Registry) error {
50
- if reg == nil {
51
- return nil
52
- }
53
- if err := reg.CreateMenu("demo:task", "Demo Tasks", "demo", 10); err != nil {
54
- return err
55
- }
56
- if err := reg.CreateAction("demo:task:view", "View Tasks", "demo", "demo:task"); err != nil {
57
- return err
58
- }
59
- if err := reg.CreateAction("demo:task:manage", "Manage Tasks", "demo", "demo:task"); err != nil {
60
- return err
61
- }
62
- return nil
63
- }
64
-
65
- func (m *Module) RegisterJobs(_ *jobs.Registry) error {
66
- return nil
67
- }
68
-
69
- func (m *Module) Migrate(db *gorm.DB) error {
70
- if db == nil {
71
- return nil
72
- }
73
- m.ensureServices(db)
74
- return demomigrations.Migrate(db)
75
- }
76
-
77
- func (m *Module) Seed(db *gorm.DB) error {
78
- if db == nil {
79
- return nil
80
- }
81
- m.ensureServices(db)
82
- return demoseeds.Seed(db)
83
- }
84
-
85
- func (m *Module) ensureServices(db *gorm.DB) {
86
- if m == nil || db == nil || m.tasks != nil {
87
- return
88
- }
89
- repo := demorepository.NewTaskRepository(db)
90
- m.tasks = demoservice.NewTaskService(repo)
91
- }
@@ -1,19 +0,0 @@
1
- package example
2
-
3
- import (
4
- "github.com/gin-gonic/gin"
5
-
6
- "github.com/robsuncn/keystone/api/response"
7
- )
8
-
9
- type HelloResponse struct {
10
- Message string `json:"message"`
11
- Module string `json:"module"`
12
- }
13
-
14
- func handleHello(c *gin.Context) {
15
- response.Success(c, HelloResponse{
16
- Message: "Hello from Example module!",
17
- Module: "example",
18
- })
19
- }
@@ -1,12 +0,0 @@
1
- ---
2
- helpKey: "demo/tasks"
3
- title: "Demo Tasks"
4
- description: "Manage sample tasks in the scaffold."
5
- category: "demo"
6
- tags: ["demo", "tasks"]
7
- ---
8
-
9
- # Demo Tasks
10
-
11
- Use this module as a reference for list + create + update flows. Replace it
12
- with real business logic when you build new modules.
@@ -1,7 +0,0 @@
1
- import { registerModule } from '@robsun/keystone-web-core'
2
- import { demoRoutes } from './routes'
3
-
4
- registerModule({
5
- name: 'demo',
6
- routes: demoRoutes,
7
- })