@robsun/create-keystone-app 0.1.17 → 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 +6 -5
  2. package/bin/create-keystone-app.js +2 -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
package/README.md CHANGED
@@ -8,24 +8,25 @@ pnpm dlx @robsun/create-keystone-app <dir> [options]
8
8
 
9
9
  ## 选项
10
10
  - `<dir>`:目标目录(必填),可为新目录名或 `.`(当前目录)。
11
- - `--profile <starter|full>`:模板档位,默认 `starter`。
12
- - `--demo` / `--no-demo`:是否包含 Demo 模块(`full` 默认包含)。
13
11
  - `--db <sqlite|postgres>`:数据库驱动(默认 `sqlite`)。
14
12
  - `--queue <memory|redis>`:队列驱动(默认 `memory`)。
15
13
  - `--storage <local|s3>`:存储驱动(默认 `local`)。
16
14
 
17
15
  ## 示例
18
16
  ```bash
19
- npx @robsun/create-keystone-app my-app --profile=full --db=postgres --queue=redis
17
+ npx @robsun/create-keystone-app my-app --db=postgres --queue=redis --storage=s3
20
18
  ```
21
19
 
22
20
  ## 初始化后操作
23
21
  ```bash
24
22
  cd <dir>
25
23
  pnpm install
24
+ pnpm server:dev
25
+ pnpm web:dev
26
26
  pnpm dev
27
27
  ```
28
28
 
29
- ## 端口与 Demo
29
+ ## 端口与 Example
30
30
  - Web 默认端口:`3000`;后端默认端口:`8080`。
31
- - Demo API:`/api/v1/demo/tasks`(仅在包含 Demo 时可用)。
31
+ - Example API:`/api/v1/example/items`。
32
+ - 权限:`example:item:view`、`example:item:manage`。
@@ -6,9 +6,6 @@ const usage = [
6
6
  'Usage: create-keystone-app <dir> [options]',
7
7
  '',
8
8
  'Options:',
9
- ' --profile <starter|full> Template profile (default: starter)',
10
- ' --demo Include demo module',
11
- ' --no-demo Exclude demo module',
12
9
  ' --db <sqlite|postgres> Database driver (default: sqlite)',
13
10
  ' --queue <memory|redis> Queue driver (default: memory)',
14
11
  ' --storage <local|s3> Storage driver (default: local)',
@@ -26,19 +23,10 @@ if (!args.target) {
26
23
  process.exit(1);
27
24
  }
28
25
 
29
- const profile = normalizeChoice(args.profile, ['starter', 'full'], 'profile') || 'starter';
30
26
  const db = normalizeChoice(args.db, ['sqlite', 'postgres'], 'db') || 'sqlite';
31
27
  const queue = normalizeChoice(args.queue, ['memory', 'redis'], 'queue') || 'memory';
32
28
  const storage = normalizeChoice(args.storage, ['local', 's3'], 'storage') || 'local';
33
29
 
34
- let includeDemo = profile === 'full';
35
- if (args.demo === true) {
36
- includeDemo = true;
37
- }
38
- if (args.demo === false) {
39
- includeDemo = false;
40
- }
41
-
42
30
  const targetDir = path.resolve(process.cwd(), args.target);
43
31
  const targetName = args.target === '.'
44
32
  ? path.basename(process.cwd())
@@ -62,15 +50,12 @@ copyDir(templateDir, targetDir, {
62
50
 
63
51
  applyConfigOptions(targetDir, { db, queue, storage });
64
52
 
65
- if (!includeDemo) {
66
- stripDemo(targetDir);
67
- }
68
-
69
53
  console.log(`Created ${targetName}`);
70
54
  console.log('Next steps:');
71
55
  console.log(` cd ${args.target}`);
72
56
  console.log(' pnpm install');
73
57
  console.log(' pnpm server:dev');
58
+ console.log(' pnpm web:dev');
74
59
  console.log(' pnpm dev');
75
60
 
76
61
  function normalizePackageName(name) {
@@ -119,49 +104,6 @@ function shouldSkipDir(name) {
119
104
  return name === 'node_modules' || name === '.git';
120
105
  }
121
106
 
122
- function stripDemo(targetDir) {
123
- removePath(path.join(targetDir, 'apps', 'web', 'src', 'modules', 'demo'));
124
- removePath(path.join(targetDir, 'apps', 'server', 'internal', 'modules', 'demo'));
125
-
126
- updateFile(path.join(targetDir, 'apps', 'web', 'src', 'main.tsx'), (content) =>
127
- content.replace(/^\s*import ['"]\.\/modules\/demo['"];?\r?\n/m, '')
128
- );
129
- updateFile(path.join(targetDir, 'apps', 'web', 'src', 'app.config.ts'), (content) =>
130
- content.replace(/,\s*['"]demo['"]/, '')
131
- );
132
- updateFile(path.join(targetDir, 'apps', 'server', 'config.yaml'), (content) =>
133
- content.replace(/\r?\n\s*-\s*['"]demo['"]\s*/g, '')
134
- );
135
- updateFile(path.join(targetDir, 'apps', 'server', 'config.example.yaml'), (content) =>
136
- content.replace(/\r?\n\s*-\s*['"]demo['"]\s*/g, '')
137
- );
138
- updateFile(path.join(targetDir, 'README.md'), (content) =>
139
- content.replace(/<!-- DEMO_START -->[\s\S]*?<!-- DEMO_END -->\s*/m, '')
140
- );
141
-
142
- const manifestPath = path.join(
143
- targetDir,
144
- 'apps',
145
- 'server',
146
- 'internal',
147
- 'modules',
148
- 'manifest.go'
149
- );
150
- const manifest = `package modules
151
-
152
- import (
153
- \texample "__APP_NAME__/apps/server/internal/modules/example"
154
- )
155
-
156
- // RegisterAll wires the module registry for this app.
157
- func RegisterAll() {
158
- \tClear()
159
- \tRegister(example.NewModule())
160
- }
161
- `;
162
- fs.writeFileSync(manifestPath, manifest, 'utf8');
163
- }
164
-
165
107
  function applyConfigOptions(targetDir, options) {
166
108
  const configFiles = [
167
109
  path.join(targetDir, 'apps', 'server', 'config.yaml'),
@@ -201,12 +143,6 @@ function updateYamlSectionValue(content, section, key, value) {
201
143
  return content.replace(pattern, `$1${value}$3`);
202
144
  }
203
145
 
204
- function removePath(target) {
205
- if (fs.existsSync(target)) {
206
- fs.rmSync(target, { recursive: true, force: true });
207
- }
208
- }
209
-
210
146
  function updateFile(filePath, updater) {
211
147
  if (!fs.existsSync(filePath)) {
212
148
  return;
@@ -227,28 +163,14 @@ function shouldMakeExecutable(filePath) {
227
163
  }
228
164
 
229
165
  function parseArgs(argv) {
230
- const out = { demo: null };
166
+ const out = {};
231
167
  for (let i = 0; i < argv.length; i++) {
232
168
  const arg = argv[i];
233
- if (arg === '--demo') {
234
- out.demo = true;
235
- continue;
236
- }
237
- if (arg === '--no-demo') {
238
- out.demo = false;
239
- continue;
240
- }
241
169
  if (arg === '--help' || arg === '-h') {
242
170
  out.help = true;
243
171
  continue;
244
172
  }
245
173
 
246
- const profile = readValueOption(arg, argv, i, '--profile');
247
- if (profile) {
248
- out.profile = profile.value;
249
- i += profile.skip;
250
- continue;
251
- }
252
174
  const db = readValueOption(arg, argv, i, '--db');
253
175
  if (db) {
254
176
  out.db = db.value;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robsun/create-keystone-app",
3
- "version": "0.1.17",
3
+ "version": "0.2.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -23,7 +23,7 @@ pnpm web:dev
23
23
  - 复制 `apps/web/.env.example` 为 `apps/web/.env`(Vite 会读取)。
24
24
  - 检查 `apps/server/config.yaml`(Go 运行配置)。
25
25
  - 需要外部依赖时参考 `docker-compose.yml`。
26
- - 建议阅读:`docs/CONVENTIONS.md`、`docs/CODE_STYLE.md`、`apps/web/README.md`、`apps/server/README.md`。
26
+ - 建议阅读:`docs/GETTING_STARTED.md`、`docs/CONVENTIONS.md`、`docs/CODE_STYLE.md`、`apps/web/README.md`、`apps/server/README.md`。
27
27
 
28
28
  ## 常用命令
29
29
  - `pnpm dev`:跨平台开发启动(Air + Vite)。
@@ -50,18 +50,10 @@ pnpm web:dev
50
50
  ```
51
51
 
52
52
  ## Example 模块(默认)
53
- Starter Full 模式默认包含 Example 模块,用于展示模块注册、权限、路由与 API。
54
- - 菜单:Example
55
- - API:`/api/v1/example/hello`
56
- - 权限:`example:view`
57
-
58
- <!-- DEMO_START -->
59
- ## Demo 模块(可选)
60
- 如果需要 Demo,可在创建时使用 `--demo` 或 `--profile=full`。
61
- - 菜单:Demo Tasks
62
- - API:`/api/v1/demo/tasks`
63
- - 权限:`demo:task:view`、`demo:task:manage`
64
- <!-- DEMO_END -->
53
+ Example 模块展示完整的 CRUD 模式、权限命名与 DDD 分层结构。
54
+ - 菜单:Example Items
55
+ - API:`/api/v1/example/items`
56
+ - 权限:`example:item:view`、`example:item:manage`
65
57
 
66
58
  ## 默认登录
67
59
  - Tenant code: `default`
@@ -7,7 +7,6 @@ modules:
7
7
  enabled:
8
8
  - "keystone"
9
9
  - "example"
10
- - "demo"
11
10
 
12
11
  database:
13
12
  driver: "sqlite"
@@ -7,7 +7,6 @@ modules:
7
7
  enabled:
8
8
  - "keystone"
9
9
  - "example"
10
- - "demo"
11
10
 
12
11
  database:
13
12
  driver: "sqlite"
@@ -0,0 +1,162 @@
1
+ package handler
2
+
3
+ import (
4
+ "errors"
5
+
6
+ "github.com/gin-gonic/gin"
7
+ hcommon "github.com/robsuncn/keystone/api/handler/common"
8
+ "github.com/robsuncn/keystone/api/response"
9
+
10
+ "__APP_NAME__/apps/server/internal/modules/example/domain/models"
11
+ "__APP_NAME__/apps/server/internal/modules/example/domain/service"
12
+ )
13
+
14
+ type ItemHandler struct {
15
+ items *service.ItemService
16
+ }
17
+
18
+ func NewItemHandler(items *service.ItemService) *ItemHandler {
19
+ if items == nil {
20
+ return nil
21
+ }
22
+ return &ItemHandler{items: items}
23
+ }
24
+
25
+ type itemInput struct {
26
+ Title string `json:"title"`
27
+ Description string `json:"description"`
28
+ Status models.ItemStatus `json:"status"`
29
+ }
30
+
31
+ type itemUpdateInput struct {
32
+ Title *string `json:"title"`
33
+ Description *string `json:"description"`
34
+ Status *models.ItemStatus `json:"status"`
35
+ }
36
+
37
+ const defaultTenantID uint = 1
38
+
39
+ func (h *ItemHandler) List(c *gin.Context) {
40
+ if h == nil || h.items == nil {
41
+ response.ServiceUnavailable(c, "example items unavailable")
42
+ return
43
+ }
44
+ tenantID := resolveTenantID(c)
45
+
46
+ items, err := h.items.List(c.Request.Context(), tenantID)
47
+ if err != nil {
48
+ response.InternalError(c, "failed to load example items")
49
+ return
50
+ }
51
+
52
+ response.Success(c, gin.H{"items": items})
53
+ }
54
+
55
+ func (h *ItemHandler) Create(c *gin.Context) {
56
+ if h == nil || h.items == nil {
57
+ response.ServiceUnavailable(c, "example items unavailable")
58
+ return
59
+ }
60
+ tenantID := resolveTenantID(c)
61
+
62
+ var input itemInput
63
+ if err := c.ShouldBindJSON(&input); err != nil {
64
+ response.BadRequest(c, "invalid payload")
65
+ return
66
+ }
67
+
68
+ item, err := h.items.Create(c.Request.Context(), tenantID, service.ItemInput{
69
+ Title: input.Title,
70
+ Description: input.Description,
71
+ Status: input.Status,
72
+ })
73
+ if err != nil {
74
+ switch {
75
+ case errors.Is(err, service.ErrTitleRequired):
76
+ response.BadRequest(c, "title is required")
77
+ case errors.Is(err, service.ErrStatusInvalid):
78
+ response.BadRequest(c, "invalid status")
79
+ default:
80
+ response.InternalError(c, "failed to create example item")
81
+ }
82
+ return
83
+ }
84
+
85
+ response.Created(c, item)
86
+ }
87
+
88
+ func (h *ItemHandler) Update(c *gin.Context) {
89
+ if h == nil || h.items == nil {
90
+ response.ServiceUnavailable(c, "example items unavailable")
91
+ return
92
+ }
93
+ tenantID := resolveTenantID(c)
94
+
95
+ id, err := hcommon.ParseUintParam(c, "id")
96
+ if err != nil || id == 0 {
97
+ response.BadRequest(c, "invalid id")
98
+ return
99
+ }
100
+
101
+ var input itemUpdateInput
102
+ if err := c.ShouldBindJSON(&input); err != nil {
103
+ response.BadRequest(c, "invalid payload")
104
+ return
105
+ }
106
+
107
+ item, err := h.items.Update(c.Request.Context(), tenantID, id, service.ItemUpdateInput{
108
+ Title: input.Title,
109
+ Description: input.Description,
110
+ Status: input.Status,
111
+ })
112
+ if err != nil {
113
+ switch {
114
+ case errors.Is(err, service.ErrItemNotFound):
115
+ response.NotFound(c, "item not found")
116
+ case errors.Is(err, service.ErrTitleRequired):
117
+ response.BadRequest(c, "title is required")
118
+ case errors.Is(err, service.ErrStatusInvalid):
119
+ response.BadRequest(c, "invalid status")
120
+ default:
121
+ response.InternalError(c, "failed to update example item")
122
+ }
123
+ return
124
+ }
125
+
126
+ response.SuccessWithMessage(c, "updated", item)
127
+ }
128
+
129
+ func (h *ItemHandler) Delete(c *gin.Context) {
130
+ if h == nil || h.items == nil {
131
+ response.ServiceUnavailable(c, "example items unavailable")
132
+ return
133
+ }
134
+ tenantID := resolveTenantID(c)
135
+
136
+ id, err := hcommon.ParseUintParam(c, "id")
137
+ if err != nil || id == 0 {
138
+ response.BadRequest(c, "invalid id")
139
+ return
140
+ }
141
+
142
+ if err := h.items.Delete(c.Request.Context(), tenantID, id); err != nil {
143
+ if errors.Is(err, service.ErrItemNotFound) {
144
+ response.NotFound(c, "item not found")
145
+ return
146
+ }
147
+ response.InternalError(c, "failed to delete example item")
148
+ return
149
+ }
150
+
151
+ response.SuccessWithMessage(c, "deleted", gin.H{"id": id})
152
+ }
153
+
154
+ func resolveTenantID(c *gin.Context) uint {
155
+ if c == nil {
156
+ return defaultTenantID
157
+ }
158
+ if tenantID, ok := hcommon.GetTenantID(c); ok && tenantID > 0 {
159
+ return tenantID
160
+ }
161
+ return defaultTenantID
162
+ }
@@ -0,0 +1,21 @@
1
+ package migrations
2
+
3
+ import (
4
+ "fmt"
5
+ "log"
6
+
7
+ "gorm.io/gorm"
8
+
9
+ "__APP_NAME__/apps/server/internal/modules/example/domain/models"
10
+ )
11
+
12
+ func Migrate(db *gorm.DB) error {
13
+ if db == nil {
14
+ return nil
15
+ }
16
+ log.Println("[example] Running migrations...")
17
+ if err := db.AutoMigrate(&models.ExampleItem{}); err != nil {
18
+ return fmt.Errorf("auto migrate example_items: %w", err)
19
+ }
20
+ return nil
21
+ }
@@ -0,0 +1,33 @@
1
+ package seeds
2
+
3
+ import (
4
+ "log"
5
+
6
+ "gorm.io/gorm"
7
+
8
+ "__APP_NAME__/apps/server/internal/modules/example/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.ExampleItem{}).Count(&count).Error; err != nil {
18
+ return err
19
+ }
20
+ if count > 0 {
21
+ return nil
22
+ }
23
+
24
+ log.Println("[example] Seeding initial data...")
25
+ items := []models.ExampleItem{
26
+ {Title: "First example item", Description: "A seeded record to explore CRUD.", Status: models.StatusActive},
27
+ {Title: "Inactive example item", Description: "Use status to drive UI state.", Status: models.StatusInactive},
28
+ }
29
+ for i := range items {
30
+ items[i].TenantID = 1
31
+ }
32
+ return db.Create(&items).Error
33
+ }
@@ -0,0 +1,30 @@
1
+ package models
2
+
3
+ import "github.com/robsuncn/keystone/domain/models"
4
+
5
+ type ItemStatus string
6
+
7
+ const (
8
+ StatusActive ItemStatus = "active"
9
+ StatusInactive ItemStatus = "inactive"
10
+ )
11
+
12
+ func (s ItemStatus) IsValid() bool {
13
+ switch s {
14
+ case StatusActive, StatusInactive:
15
+ return true
16
+ default:
17
+ return false
18
+ }
19
+ }
20
+
21
+ type ExampleItem struct {
22
+ models.BaseModel
23
+ Title string `gorm:"size:200;not null" json:"title"`
24
+ Description string `gorm:"size:1000" json:"description"`
25
+ Status ItemStatus `gorm:"size:20;not null;default:'active'" json:"status"`
26
+ }
27
+
28
+ func (ExampleItem) TableName() string {
29
+ return "example_items"
30
+ }
@@ -3,7 +3,7 @@ package service
3
3
  import "errors"
4
4
 
5
5
  var (
6
- ErrTaskNotFound = errors.New("task not found")
6
+ ErrItemNotFound = errors.New("item not found")
7
7
  ErrTitleRequired = errors.New("title is required")
8
8
  ErrStatusInvalid = errors.New("status is invalid")
9
9
  )
@@ -0,0 +1,110 @@
1
+ package service
2
+
3
+ import (
4
+ "context"
5
+ "strings"
6
+
7
+ "__APP_NAME__/apps/server/internal/modules/example/domain/models"
8
+ )
9
+
10
+ type ItemRepository interface {
11
+ List(ctx context.Context, tenantID uint) ([]models.ExampleItem, error)
12
+ FindByID(tenantID, id uint) (*models.ExampleItem, error)
13
+ Create(ctx context.Context, item *models.ExampleItem) error
14
+ Update(ctx context.Context, item *models.ExampleItem) error
15
+ Delete(ctx context.Context, item *models.ExampleItem) error
16
+ }
17
+
18
+ type ItemService struct {
19
+ items ItemRepository
20
+ }
21
+
22
+ type ItemInput struct {
23
+ Title string
24
+ Description string
25
+ Status models.ItemStatus
26
+ }
27
+
28
+ type ItemUpdateInput struct {
29
+ Title *string
30
+ Description *string
31
+ Status *models.ItemStatus
32
+ }
33
+
34
+ func NewItemService(items ItemRepository) *ItemService {
35
+ return &ItemService{items: items}
36
+ }
37
+
38
+ func (s *ItemService) List(ctx context.Context, tenantID uint) ([]models.ExampleItem, error) {
39
+ return s.items.List(ctx, tenantID)
40
+ }
41
+
42
+ func (s *ItemService) Create(ctx context.Context, tenantID uint, input ItemInput) (*models.ExampleItem, error) {
43
+ title := strings.TrimSpace(input.Title)
44
+ if title == "" {
45
+ return nil, ErrTitleRequired
46
+ }
47
+
48
+ status := input.Status
49
+ if status == "" {
50
+ status = models.StatusActive
51
+ }
52
+ if !status.IsValid() {
53
+ return nil, ErrStatusInvalid
54
+ }
55
+
56
+ item := &models.ExampleItem{
57
+ Title: title,
58
+ Description: strings.TrimSpace(input.Description),
59
+ Status: status,
60
+ }
61
+ item.TenantID = tenantID
62
+
63
+ if err := s.items.Create(ctx, item); err != nil {
64
+ return nil, err
65
+ }
66
+ return item, nil
67
+ }
68
+
69
+ func (s *ItemService) Update(
70
+ ctx context.Context,
71
+ tenantID, id uint,
72
+ input ItemUpdateInput,
73
+ ) (*models.ExampleItem, error) {
74
+ item, err := s.items.FindByID(tenantID, id)
75
+ if err != nil {
76
+ return nil, err
77
+ }
78
+
79
+ if input.Title != nil {
80
+ title := strings.TrimSpace(*input.Title)
81
+ if title == "" {
82
+ return nil, ErrTitleRequired
83
+ }
84
+ item.Title = title
85
+ }
86
+
87
+ if input.Description != nil {
88
+ item.Description = strings.TrimSpace(*input.Description)
89
+ }
90
+
91
+ if input.Status != nil {
92
+ if !input.Status.IsValid() {
93
+ return nil, ErrStatusInvalid
94
+ }
95
+ item.Status = *input.Status
96
+ }
97
+
98
+ if err := s.items.Update(ctx, item); err != nil {
99
+ return nil, err
100
+ }
101
+ return item, nil
102
+ }
103
+
104
+ func (s *ItemService) Delete(ctx context.Context, tenantID, id uint) error {
105
+ item, err := s.items.FindByID(tenantID, id)
106
+ if err != nil {
107
+ return err
108
+ }
109
+ return s.items.Delete(ctx, item)
110
+ }
@@ -0,0 +1,49 @@
1
+ package repository
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+
7
+ "gorm.io/gorm"
8
+
9
+ "__APP_NAME__/apps/server/internal/modules/example/domain/models"
10
+ "__APP_NAME__/apps/server/internal/modules/example/domain/service"
11
+ )
12
+
13
+ type ItemRepository struct {
14
+ db *gorm.DB
15
+ }
16
+
17
+ func NewItemRepository(db *gorm.DB) *ItemRepository {
18
+ return &ItemRepository{db: db}
19
+ }
20
+
21
+ func (r *ItemRepository) List(ctx context.Context, tenantID uint) ([]models.ExampleItem, error) {
22
+ var items []models.ExampleItem
23
+ err := r.db.WithContext(ctx).
24
+ Where("tenant_id = ?", tenantID).
25
+ Order("created_at desc").
26
+ Find(&items).Error
27
+ return items, err
28
+ }
29
+
30
+ func (r *ItemRepository) FindByID(tenantID, id uint) (*models.ExampleItem, error) {
31
+ var item models.ExampleItem
32
+ err := r.db.Where("tenant_id = ? AND id = ?", tenantID, id).First(&item).Error
33
+ if errors.Is(err, gorm.ErrRecordNotFound) {
34
+ return nil, service.ErrItemNotFound
35
+ }
36
+ return &item, err
37
+ }
38
+
39
+ func (r *ItemRepository) Create(ctx context.Context, item *models.ExampleItem) error {
40
+ return r.db.WithContext(ctx).Create(item).Error
41
+ }
42
+
43
+ func (r *ItemRepository) Update(ctx context.Context, item *models.ExampleItem) error {
44
+ return r.db.WithContext(ctx).Save(item).Error
45
+ }
46
+
47
+ func (r *ItemRepository) Delete(ctx context.Context, item *models.ExampleItem) error {
48
+ return r.db.WithContext(ctx).Delete(item).Error
49
+ }