@robsun/create-keystone-app 0.1.18 → 0.2.1

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 (45) hide show
  1. package/README.md +18 -5
  2. package/{bin → dist}/create-keystone-app.js +114 -116
  3. package/dist/create-module.js +638 -0
  4. package/package.json +11 -7
  5. package/template/README.md +17 -13
  6. package/template/apps/server/config.example.yaml +0 -1
  7. package/template/apps/server/config.yaml +0 -1
  8. package/template/apps/server/internal/modules/example/api/handler/item_handler.go +162 -0
  9. package/template/apps/server/internal/modules/example/bootstrap/migrations/item.go +21 -0
  10. package/template/apps/server/internal/modules/example/bootstrap/seeds/item.go +33 -0
  11. package/template/apps/server/internal/modules/example/domain/models/item.go +30 -0
  12. package/template/apps/server/internal/modules/{demo → example}/domain/service/errors.go +1 -1
  13. package/template/apps/server/internal/modules/example/domain/service/item_service.go +110 -0
  14. package/template/apps/server/internal/modules/example/infra/repository/item_repository.go +49 -0
  15. package/template/apps/server/internal/modules/example/module.go +55 -17
  16. package/template/apps/server/internal/modules/manifest.go +0 -2
  17. package/template/apps/web/src/app.config.ts +1 -1
  18. package/template/apps/web/src/main.tsx +1 -3
  19. package/template/apps/web/src/modules/example/help/faq.md +23 -0
  20. package/template/apps/web/src/modules/example/help/items.md +26 -0
  21. package/template/apps/web/src/modules/example/help/overview.md +18 -4
  22. package/template/apps/web/src/modules/example/pages/ExampleItemsPage.tsx +227 -0
  23. package/template/apps/web/src/modules/example/routes.tsx +33 -10
  24. package/template/apps/web/src/modules/example/services/exampleItems.ts +32 -0
  25. package/template/apps/web/src/modules/example/types.ts +10 -0
  26. package/template/docs/CONVENTIONS.md +44 -0
  27. package/template/docs/GETTING_STARTED.md +54 -0
  28. package/template/package.json +2 -1
  29. package/template/scripts/check-modules.js +7 -1
  30. package/template/apps/server/internal/modules/demo/api/handler/task_handler.go +0 -152
  31. package/template/apps/server/internal/modules/demo/bootstrap/migrations/task.go +0 -21
  32. package/template/apps/server/internal/modules/demo/bootstrap/seeds/task.go +0 -33
  33. package/template/apps/server/internal/modules/demo/domain/models/task.go +0 -30
  34. package/template/apps/server/internal/modules/demo/domain/service/task_service.go +0 -95
  35. package/template/apps/server/internal/modules/demo/infra/repository/task_repository.go +0 -49
  36. package/template/apps/server/internal/modules/demo/module.go +0 -91
  37. package/template/apps/server/internal/modules/example/handlers.go +0 -19
  38. package/template/apps/web/src/modules/demo/help/overview.md +0 -12
  39. package/template/apps/web/src/modules/demo/index.ts +0 -7
  40. package/template/apps/web/src/modules/demo/pages/DemoTasksPage.tsx +0 -185
  41. package/template/apps/web/src/modules/demo/routes.tsx +0 -43
  42. package/template/apps/web/src/modules/demo/services/demoTasks.ts +0 -28
  43. package/template/apps/web/src/modules/demo/types.ts +0 -9
  44. package/template/apps/web/src/modules/example/pages/ExamplePage.tsx +0 -41
  45. package/template/apps/web/src/modules/example/services/api.ts +0 -8
@@ -1,11 +1,25 @@
1
1
  ---
2
2
  helpKey: "example/overview"
3
- title: "Example Module"
4
- description: "Quick tour of the example module."
3
+ title: "Example Module Overview"
4
+ description: "Overview of the Example module and its full CRUD flow."
5
5
  category: "example"
6
- tags: ["example", "starter"]
6
+ tags: ["example", "items", "crud"]
7
7
  ---
8
8
 
9
9
  # Example Module
10
10
 
11
- Use this page to see how routes, permissions, and API calls connect together.
11
+ Example 模块演示完整的 CRUD、权限命名与前后端分层结构,适合作为新模块开发参考。
12
+
13
+ ## 你能看到什么
14
+ - 前端模块注册(`index.ts`)与路由配置(`routes.tsx`)
15
+ - CRUD 页面与 API 调用(`pages/` + `services/`)
16
+ - 后端 DDD 分层(`domain/` + `infra/` + `bootstrap/`)
17
+ - 权限注册与菜单同步(`RegisterPermissions`)
18
+
19
+ ## 关键文件
20
+ - 前端:`apps/web/src/modules/example/`
21
+ - 后端:`apps/server/internal/modules/example/`
22
+
23
+ ## API 与权限
24
+ - API:`/api/v1/example/items`
25
+ - 权限:`example:item:view`、`example:item:manage`
@@ -0,0 +1,227 @@
1
+ import { useCallback, useEffect, useMemo, useState } from 'react'
2
+ import { App, Button, Card, Form, Input, Modal, Popconfirm, Select, Space, Table, Tag, Typography } from 'antd'
3
+ import type { ColumnsType } from 'antd/es/table'
4
+ import dayjs from 'dayjs'
5
+ import {
6
+ createExampleItem,
7
+ deleteExampleItem,
8
+ listExampleItems,
9
+ updateExampleItem,
10
+ } from '../services/exampleItems'
11
+ import type { ExampleItem, ExampleItemStatus } from '../types'
12
+
13
+ type ExampleItemFormValues = {
14
+ title: string
15
+ description?: string
16
+ status: ExampleItemStatus
17
+ }
18
+
19
+ const statusMeta: Record<ExampleItemStatus, { label: string; color: string }> = {
20
+ active: { label: 'Active', color: 'success' },
21
+ inactive: { label: 'Inactive', color: 'default' },
22
+ }
23
+
24
+ const statusOptions = [
25
+ { value: 'active', label: 'Active' },
26
+ { value: 'inactive', label: 'Inactive' },
27
+ ]
28
+
29
+ export function ExampleItemsPage() {
30
+ const { message } = App.useApp()
31
+ const [items, setItems] = useState<ExampleItem[]>([])
32
+ const [loading, setLoading] = useState(false)
33
+ const [modalOpen, setModalOpen] = useState(false)
34
+ const [saving, setSaving] = useState(false)
35
+ const [editingItem, setEditingItem] = useState<ExampleItem | null>(null)
36
+ const [form] = Form.useForm<ExampleItemFormValues>()
37
+
38
+ const fetchItems = useCallback(async () => {
39
+ setLoading(true)
40
+ try {
41
+ const data = await listExampleItems()
42
+ setItems(data)
43
+ } catch (err) {
44
+ const detail = err instanceof Error ? err.message : 'Failed to load items'
45
+ message.error(detail)
46
+ } finally {
47
+ setLoading(false)
48
+ }
49
+ }, [message])
50
+
51
+ useEffect(() => {
52
+ void fetchItems()
53
+ }, [fetchItems])
54
+
55
+ const openCreate = useCallback(() => {
56
+ setEditingItem(null)
57
+ form.resetFields()
58
+ form.setFieldsValue({ status: 'active' })
59
+ setModalOpen(true)
60
+ }, [form])
61
+
62
+ const openEdit = useCallback(
63
+ (item: ExampleItem) => {
64
+ setEditingItem(item)
65
+ form.setFieldsValue({
66
+ title: item.title,
67
+ description: item.description,
68
+ status: item.status,
69
+ })
70
+ setModalOpen(true)
71
+ },
72
+ [form]
73
+ )
74
+
75
+ const closeModal = useCallback(() => {
76
+ setModalOpen(false)
77
+ setEditingItem(null)
78
+ form.resetFields()
79
+ }, [form])
80
+
81
+ const handleSubmit = useCallback(async () => {
82
+ let values: ExampleItemFormValues
83
+ try {
84
+ values = await form.validateFields()
85
+ } catch {
86
+ return
87
+ }
88
+
89
+ const payload = {
90
+ title: values.title.trim(),
91
+ description: values.description?.trim() ?? '',
92
+ status: values.status,
93
+ }
94
+
95
+ setSaving(true)
96
+ try {
97
+ if (editingItem) {
98
+ await updateExampleItem(editingItem.id, payload)
99
+ message.success('Item updated')
100
+ } else {
101
+ await createExampleItem(payload)
102
+ message.success('Item created')
103
+ }
104
+ closeModal()
105
+ await fetchItems()
106
+ } catch (err) {
107
+ const detail = err instanceof Error ? err.message : 'Failed to save item'
108
+ message.error(detail)
109
+ } finally {
110
+ setSaving(false)
111
+ }
112
+ }, [closeModal, editingItem, fetchItems, form, message])
113
+
114
+ const handleDelete = useCallback(
115
+ async (id: number) => {
116
+ try {
117
+ await deleteExampleItem(id)
118
+ await fetchItems()
119
+ message.success('Item deleted')
120
+ } catch (err) {
121
+ const detail = err instanceof Error ? err.message : 'Failed to delete item'
122
+ message.error(detail)
123
+ }
124
+ },
125
+ [fetchItems, message]
126
+ )
127
+
128
+ const columns: ColumnsType<ExampleItem> = useMemo(
129
+ () => [
130
+ { title: 'Title', dataIndex: 'title', key: 'title' },
131
+ {
132
+ title: 'Description',
133
+ dataIndex: 'description',
134
+ key: 'description',
135
+ render: (value: string) =>
136
+ value ? <Typography.Text type="secondary">{value}</Typography.Text> : '-',
137
+ },
138
+ {
139
+ title: 'Status',
140
+ dataIndex: 'status',
141
+ key: 'status',
142
+ render: (value: ExampleItemStatus) => {
143
+ const meta = statusMeta[value]
144
+ return <Tag color={meta.color}>{meta.label}</Tag>
145
+ },
146
+ },
147
+ {
148
+ title: 'Updated',
149
+ dataIndex: 'updated_at',
150
+ key: 'updated_at',
151
+ render: (value: string) => (value ? dayjs(value).format('YYYY-MM-DD HH:mm') : '-'),
152
+ },
153
+ {
154
+ title: 'Actions',
155
+ key: 'actions',
156
+ render: (_, record) => (
157
+ <Space>
158
+ <Button type="link" onClick={() => openEdit(record)}>
159
+ Edit
160
+ </Button>
161
+ <Popconfirm title="Delete this item?" onConfirm={() => handleDelete(record.id)}>
162
+ <Button type="link" danger>
163
+ Delete
164
+ </Button>
165
+ </Popconfirm>
166
+ </Space>
167
+ ),
168
+ },
169
+ ],
170
+ [handleDelete, openEdit]
171
+ )
172
+
173
+ return (
174
+ <Card
175
+ title="Example Items"
176
+ extra={
177
+ <Space>
178
+ <Button onClick={fetchItems} loading={loading}>
179
+ Refresh
180
+ </Button>
181
+ <Button type="primary" onClick={openCreate}>
182
+ New Item
183
+ </Button>
184
+ </Space>
185
+ }
186
+ >
187
+ <Space direction="vertical" size="middle" style={{ width: '100%' }}>
188
+ <Typography.Text type="secondary">
189
+ This page demonstrates a full CRUD workflow wired to the Example module API.
190
+ </Typography.Text>
191
+ <Table<ExampleItem>
192
+ rowKey="id"
193
+ loading={loading}
194
+ columns={columns}
195
+ dataSource={items}
196
+ pagination={false}
197
+ />
198
+ </Space>
199
+
200
+ <Modal
201
+ title={editingItem ? 'Edit Item' : 'New Item'}
202
+ open={modalOpen}
203
+ onCancel={closeModal}
204
+ onOk={handleSubmit}
205
+ confirmLoading={saving}
206
+ okText={editingItem ? 'Save' : 'Create'}
207
+ destroyOnClose
208
+ >
209
+ <Form form={form} layout="vertical" initialValues={{ status: 'active' }}>
210
+ <Form.Item
211
+ label="Title"
212
+ name="title"
213
+ rules={[{ required: true, whitespace: true, message: 'Title is required' }]}
214
+ >
215
+ <Input placeholder="Item title" allowClear />
216
+ </Form.Item>
217
+ <Form.Item label="Description" name="description">
218
+ <Input.TextArea rows={3} placeholder="Short description" />
219
+ </Form.Item>
220
+ <Form.Item label="Status" name="status" rules={[{ required: true, message: 'Select status' }]}>
221
+ <Select options={statusOptions} />
222
+ </Form.Item>
223
+ </Form>
224
+ </Modal>
225
+ </Card>
226
+ )
227
+ }
@@ -1,20 +1,43 @@
1
- import { lazy } from 'react'
1
+ import { lazy, Suspense, type ComponentType, type ReactElement } from 'react'
2
2
  import type { RouteObject } from 'react-router-dom'
3
- import { QuestionCircleOutlined } from '@ant-design/icons'
3
+ import { AppstoreOutlined } from '@ant-design/icons'
4
+ import { Spin } from 'antd'
4
5
 
5
- const ExamplePage = lazy(() =>
6
- import('./pages/ExamplePage').then((m) => ({ default: m.ExamplePage }))
6
+ const lazyNamed = <T extends Record<string, ComponentType>, K extends keyof T>(
7
+ factory: () => Promise<T>,
8
+ name: K
9
+ ) =>
10
+ lazy(async () => {
11
+ const module = await factory()
12
+ return { default: module[name] }
13
+ })
14
+
15
+ const withSuspense = (element: ReactElement) => (
16
+ <Suspense
17
+ fallback={
18
+ <div style={{ padding: 24, display: 'flex', justifyContent: 'center' }}>
19
+ <Spin />
20
+ </div>
21
+ }
22
+ >
23
+ {element}
24
+ </Suspense>
7
25
  )
8
26
 
27
+ const ExampleItemsPage = lazyNamed(() => import('./pages/ExampleItemsPage'), 'ExampleItemsPage')
28
+
9
29
  export const exampleRoutes: RouteObject[] = [
10
30
  {
11
31
  path: 'example',
12
- element: <ExamplePage />,
32
+ element: <ExampleItemsPage />,
13
33
  handle: {
14
- menu: { label: 'Example', icon: <QuestionCircleOutlined /> },
15
- breadcrumb: 'Example',
16
- permission: 'example:view',
17
- helpKey: 'example/overview',
34
+ menu: { label: 'Example Items', icon: <AppstoreOutlined />, permission: 'example:item:view' },
35
+ breadcrumb: 'Example Items',
36
+ permission: 'example:item:view',
37
+ helpKey: 'example/items',
18
38
  },
19
39
  },
20
- ]
40
+ ].map((route) => ({
41
+ ...route,
42
+ element: route.element ? withSuspense(route.element) : route.element,
43
+ }))
@@ -0,0 +1,32 @@
1
+ import { api, type ApiResponse } from '@robsun/keystone-web-core'
2
+ import type { ExampleItem, ExampleItemStatus } from '../types'
3
+
4
+ type ItemListResponse = {
5
+ items: ExampleItem[]
6
+ }
7
+
8
+ export const listExampleItems = async () => {
9
+ const { data } = await api.get<ApiResponse<ItemListResponse>>('/example/items')
10
+ return data.data.items
11
+ }
12
+
13
+ export const createExampleItem = async (payload: {
14
+ title: string
15
+ description?: string
16
+ status?: ExampleItemStatus
17
+ }) => {
18
+ const { data } = await api.post<ApiResponse<ExampleItem>>('/example/items', payload)
19
+ return data.data
20
+ }
21
+
22
+ export const updateExampleItem = async (
23
+ id: number,
24
+ payload: { title?: string; description?: string; status?: ExampleItemStatus }
25
+ ) => {
26
+ const { data } = await api.patch<ApiResponse<ExampleItem>>(`/example/items/${id}`, payload)
27
+ return data.data
28
+ }
29
+
30
+ export const deleteExampleItem = async (id: number) => {
31
+ await api.delete<ApiResponse<{ id: number }>>(`/example/items/${id}`)
32
+ }
@@ -0,0 +1,10 @@
1
+ export type ExampleItemStatus = 'active' | 'inactive'
2
+
3
+ export interface ExampleItem {
4
+ id: number
5
+ title: string
6
+ description: string
7
+ status: ExampleItemStatus
8
+ created_at: string
9
+ updated_at: string
10
+ }
@@ -88,6 +88,50 @@ type Module interface {
88
88
  - 在 `apps/server/config.yaml` 的 `modules.enabled` 启用模块。
89
89
  - Web 端 `app.config.ts` 保持同名启用列表。
90
90
 
91
+ ## 权限命名与同步
92
+ - 命名规范:`{module}:{resource}:{action}`,例如:`example:item:view`。
93
+ - 菜单权限通常使用 `{module}:{resource}`,用于菜单与分组,例如:`example:item`。
94
+ - 前端使用 `routes.tsx` 的 `handle.permission` 与 `handle.menu.permission` 控制路由/菜单访问。
95
+ - 后端使用 `RegisterPermissions` 注册同名权限,确保权限种子与管理后台一致。
96
+
97
+ 示例(后端注册):
98
+ ```go
99
+ if err := reg.CreateMenu("example:item", "Example Items", "example", 10); err != nil {
100
+ return err
101
+ }
102
+ if err := reg.CreateAction("example:item:view", "View Items", "example", "example:item"); err != nil {
103
+ return err
104
+ }
105
+ if err := reg.CreateAction("example:item:manage", "Manage Items", "example", "example:item"); err != nil {
106
+ return err
107
+ }
108
+ ```
109
+
110
+ 示例(前端路由):
111
+ ```tsx
112
+ {
113
+ path: 'example',
114
+ element: <ExampleItemsPage />,
115
+ handle: {
116
+ menu: { label: 'Example Items', icon: <AppstoreOutlined />, permission: 'example:item:view' },
117
+ breadcrumb: 'Example Items',
118
+ permission: 'example:item:view',
119
+ helpKey: 'example/items',
120
+ },
121
+ }
122
+ ```
123
+
124
+ 权限同步流程(简化):
125
+ ```
126
+ routes.tsx handle.permission
127
+
128
+ 登录接口返回权限列表(permissions)
129
+
130
+ 前端菜单/路由/按钮显示与否
131
+
132
+ RegisterPermissions 负责定义权限种子与后台管理项
133
+ ```
134
+
91
135
  ## 迁移 / 种子 / 权限
92
136
  - `startup.InitializeDatabase` 统一执行:核心迁移 + 模块迁移 + 种子 + 权限注册。
93
137
  - 模块权限通过 `RegisterPermissions` 注入。
@@ -0,0 +1,54 @@
1
+ # 10 分钟上手 Keystone
2
+
3
+ ## 1) 快速启动(约 2 分钟)
4
+ ```bash
5
+ pnpm install
6
+ pnpm dev
7
+ ```
8
+ - Web 端口:`http://localhost:3000`
9
+ - Server 端口:`http://localhost:8080`
10
+ - 默认账号:`admin` / `Admin123!`(Tenant: `default`)
11
+
12
+ ## 2) 认识项目结构
13
+ - `apps/web/`:React + Vite 壳与业务模块(`src/modules/*`)。
14
+ - `apps/server/`:Go 服务端与模块注册(`internal/modules/*`)。
15
+ - `docs/`:开发规范与约定。
16
+ - `scripts/`:跨平台 dev/build/test/clean。
17
+
18
+ ## 3) 理解 Example 模块(前后端对照)
19
+ - 前端模块:
20
+ - `apps/web/src/modules/example/`
21
+ - 入口:`index.ts`(模块注册)
22
+ - 路由与权限:`routes.tsx`
23
+ - 页面与 API:`pages/` + `services/`
24
+ - 后端模块:
25
+ - `apps/server/internal/modules/example/`
26
+ - 入口:`module.go`
27
+ - API:`api/handler/`
28
+ - 领域:`domain/` + `infra/` + `bootstrap/`
29
+
30
+ ## 4) 创建你的第一个模块(复制 Example)
31
+ 1. 复制前端模块:
32
+ - `apps/web/src/modules/example` → `apps/web/src/modules/orders`
33
+ 2. 修改模块名称与权限:
34
+ - 更新 `routes.tsx`、`help/*.md`、`services/*` 中的路径与权限前缀。
35
+ 3. 注册前端模块:
36
+ - 在 `apps/web/src/main.tsx` 添加 `import './modules/orders'`
37
+ - 在 `apps/web/src/app.config.ts` 的 `modules.enabled` 加入 `orders`
38
+ 4. 复制后端模块:
39
+ - `apps/server/internal/modules/example` → `apps/server/internal/modules/orders`
40
+ 5. 修改后端模块名称:
41
+ - 更新 `module.go` 中的 `Name()` 与权限注册
42
+ 6. 注册后端模块:
43
+ - 在 `apps/server/internal/modules/manifest.go` 注册新模块
44
+ - 在 `apps/server/config.yaml` 的 `modules.enabled` 加入 `orders`
45
+
46
+ ## 5) 常见问题(FAQ)
47
+ **Q: 页面空白或 404?**
48
+ A: 确认 `pnpm dev` 正常启动,`modules.enabled` 前后端一致,且前端已在 `main.tsx` 导入模块。
49
+
50
+ **Q: 权限不足导致按钮不可用?**
51
+ A: 检查后端 `RegisterPermissions` 是否注册对应权限,并确认当前用户拥有权限。
52
+
53
+ **Q: API 报错或返回空列表?**
54
+ A: 检查服务端日志、数据库连接、`apps/server/config.yaml` 是否正确。
@@ -7,7 +7,8 @@
7
7
  "build": "node scripts/build.js",
8
8
  "build:web": "pnpm --filter web build",
9
9
  "check-modules": "node scripts/check-modules.js",
10
- "test": "node scripts/test.js",
10
+ "create:module": "pnpm dlx --package @robsun/create-keystone-app create-keystone-module",
11
+ "test": "pnpm check-modules && node scripts/test.js",
11
12
  "lint": "pnpm --filter web lint",
12
13
  "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md,yml,yaml}\"",
13
14
  "clean": "node scripts/clean.js",
@@ -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
- }