@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
package/README.md CHANGED
@@ -8,15 +8,13 @@ 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
  ## 初始化后操作
@@ -28,6 +26,7 @@ pnpm web:dev
28
26
  pnpm dev
29
27
  ```
30
28
 
31
- ## 端口与 Demo
29
+ ## 端口与 Example
32
30
  - Web 默认端口:`3000`;后端默认端口:`8080`。
33
- - 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,10 +50,6 @@ 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}`);
@@ -120,49 +104,6 @@ function shouldSkipDir(name) {
120
104
  return name === 'node_modules' || name === '.git';
121
105
  }
122
106
 
123
- function stripDemo(targetDir) {
124
- removePath(path.join(targetDir, 'apps', 'web', 'src', 'modules', 'demo'));
125
- removePath(path.join(targetDir, 'apps', 'server', 'internal', 'modules', 'demo'));
126
-
127
- updateFile(path.join(targetDir, 'apps', 'web', 'src', 'main.tsx'), (content) =>
128
- content.replace(/^\s*import ['"]\.\/modules\/demo['"];?\r?\n/m, '')
129
- );
130
- updateFile(path.join(targetDir, 'apps', 'web', 'src', 'app.config.ts'), (content) =>
131
- content.replace(/,\s*['"]demo['"]/, '')
132
- );
133
- updateFile(path.join(targetDir, 'apps', 'server', 'config.yaml'), (content) =>
134
- content.replace(/\r?\n\s*-\s*['"]demo['"]\s*/g, '')
135
- );
136
- updateFile(path.join(targetDir, 'apps', 'server', 'config.example.yaml'), (content) =>
137
- content.replace(/\r?\n\s*-\s*['"]demo['"]\s*/g, '')
138
- );
139
- updateFile(path.join(targetDir, 'README.md'), (content) =>
140
- content.replace(/<!-- DEMO_START -->[\s\S]*?<!-- DEMO_END -->\s*/m, '')
141
- );
142
-
143
- const manifestPath = path.join(
144
- targetDir,
145
- 'apps',
146
- 'server',
147
- 'internal',
148
- 'modules',
149
- 'manifest.go'
150
- );
151
- const manifest = `package modules
152
-
153
- import (
154
- \texample "__APP_NAME__/apps/server/internal/modules/example"
155
- )
156
-
157
- // RegisterAll wires the module registry for this app.
158
- func RegisterAll() {
159
- \tClear()
160
- \tRegister(example.NewModule())
161
- }
162
- `;
163
- fs.writeFileSync(manifestPath, manifest, 'utf8');
164
- }
165
-
166
107
  function applyConfigOptions(targetDir, options) {
167
108
  const configFiles = [
168
109
  path.join(targetDir, 'apps', 'server', 'config.yaml'),
@@ -202,12 +143,6 @@ function updateYamlSectionValue(content, section, key, value) {
202
143
  return content.replace(pattern, `$1${value}$3`);
203
144
  }
204
145
 
205
- function removePath(target) {
206
- if (fs.existsSync(target)) {
207
- fs.rmSync(target, { recursive: true, force: true });
208
- }
209
- }
210
-
211
146
  function updateFile(filePath, updater) {
212
147
  if (!fs.existsSync(filePath)) {
213
148
  return;
@@ -228,28 +163,14 @@ function shouldMakeExecutable(filePath) {
228
163
  }
229
164
 
230
165
  function parseArgs(argv) {
231
- const out = { demo: null };
166
+ const out = {};
232
167
  for (let i = 0; i < argv.length; i++) {
233
168
  const arg = argv[i];
234
- if (arg === '--demo') {
235
- out.demo = true;
236
- continue;
237
- }
238
- if (arg === '--no-demo') {
239
- out.demo = false;
240
- continue;
241
- }
242
169
  if (arg === '--help' || arg === '-h') {
243
170
  out.help = true;
244
171
  continue;
245
172
  }
246
173
 
247
- const profile = readValueOption(arg, argv, i, '--profile');
248
- if (profile) {
249
- out.profile = profile.value;
250
- i += profile.skip;
251
- continue;
252
- }
253
174
  const db = readValueOption(arg, argv, i, '--db');
254
175
  if (db) {
255
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.18",
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
+ }