@ddd-tool/domain-designer-cli 0.3.1 → 0.3.2
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/bin/domain-designer-cli.cjs +19148 -19148
- package/package.json +2 -2
- package/packages/core/dist/package.json +1 -1
- package/packages/playground/package.json +19 -19
- package/packages/playground/vite.config.ts +27 -22
- package/packages/ui-component/dist/package.json +1 -1
- package/scripts/build-ts.mjs +52 -52
- package/scripts/sync-version.mjs +22 -22
- package/templates/CLAUDE.md +273 -12
- package/templates/complex-example-detail/book.ts +6 -3
- package/templates/node_modules/version.txt +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ddd-tool/domain-designer-cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "",
|
|
6
6
|
"type": "module",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"dev:ui": "pnpm -F @ddd-tool/domain-designer-ui-component dev",
|
|
62
62
|
"dev:page": "pnpm -F github-pages dev",
|
|
63
63
|
"build": "pnpm syncver && pnpm -r build && pnpm fmt",
|
|
64
|
-
"fmt": "pnpm exec prettier --write \"**/*.{vue,ts}\"",
|
|
64
|
+
"fmt": "pnpm exec prettier --write \"**/*.{vue,ts,mjs}\"",
|
|
65
65
|
"test": "pnpm -r test",
|
|
66
66
|
"test:cli": "pnpm -F cli test",
|
|
67
67
|
"verify": "pnpm -r verify",
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "playground",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"private": true,
|
|
5
|
-
"description": "",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"scripts": {
|
|
8
|
-
"dev": "vite",
|
|
9
|
-
"test": "exit 0",
|
|
10
|
-
"build": "exit 0",
|
|
11
|
-
"verify": "vue-tsc -p ./tsconfig.json"
|
|
12
|
-
},
|
|
13
|
-
"devDependencies": {
|
|
14
|
-
"@primeuix/themes": "^2.0.3",
|
|
15
|
-
"primeicons": "^7.0.0",
|
|
16
|
-
"primevue": "^4.5.4",
|
|
17
|
-
"vue": "^3.5.27"
|
|
18
|
-
}
|
|
19
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "playground",
|
|
3
|
+
"version": "0.3.2",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "vite",
|
|
9
|
+
"test": "exit 0",
|
|
10
|
+
"build": "exit 0",
|
|
11
|
+
"verify": "vue-tsc -p ./tsconfig.json"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@primeuix/themes": "^2.0.3",
|
|
15
|
+
"primeicons": "^7.0.0",
|
|
16
|
+
"primevue": "^4.5.4",
|
|
17
|
+
"vue": "^3.5.27"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -1,22 +1,27 @@
|
|
|
1
|
-
import { fileURLToPath, URL } from 'node:url'
|
|
2
|
-
|
|
3
|
-
import { defineConfig } from 'vite'
|
|
4
|
-
import vue from '@vitejs/plugin-vue'
|
|
5
|
-
import vueDevTools from 'vite-plugin-vue-devtools'
|
|
6
|
-
|
|
7
|
-
export default defineConfig({
|
|
8
|
-
plugins: [vue(), vueDevTools()],
|
|
9
|
-
esbuild: {
|
|
10
|
-
drop: ['console', 'debugger'], // 移除 console 和 debugger 语句
|
|
11
|
-
},
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
1
|
+
import { fileURLToPath, URL } from 'node:url'
|
|
2
|
+
|
|
3
|
+
import { defineConfig } from 'vite'
|
|
4
|
+
import vue from '@vitejs/plugin-vue'
|
|
5
|
+
import vueDevTools from 'vite-plugin-vue-devtools'
|
|
6
|
+
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
plugins: [vue(), vueDevTools()],
|
|
9
|
+
esbuild: {
|
|
10
|
+
drop: ['console', 'debugger'], // 移除 console 和 debugger 语句
|
|
11
|
+
},
|
|
12
|
+
server: {
|
|
13
|
+
fs: {
|
|
14
|
+
// Allow accessing files from global installation directories
|
|
15
|
+
strict: false,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
resolve: {
|
|
19
|
+
alias: {
|
|
20
|
+
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
|
21
|
+
'@ddd-tool/domain-designer-core': fileURLToPath(new URL('../core/dist', import.meta.url)),
|
|
22
|
+
'@ddd-tool/domain-designer-ui-component': fileURLToPath(
|
|
23
|
+
new URL('../ui-component/dist', import.meta.url),
|
|
24
|
+
),
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
})
|
package/scripts/build-ts.mjs
CHANGED
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
import * as esbuild from 'esbuild'
|
|
2
|
-
import fs from 'fs'
|
|
3
|
-
import path from 'path'
|
|
4
|
-
|
|
5
|
-
// 获取命令行参数
|
|
6
|
-
const args = process.argv.slice(2) // 从第3个元素开始截取(跳过node和脚本路径)
|
|
7
|
-
// 查找并解析 --source 参数
|
|
8
|
-
let sourcePath = null
|
|
9
|
-
for (const arg of args) {
|
|
10
|
-
if (arg.startsWith('--source=')) {
|
|
11
|
-
sourcePath = arg.split('=')[1] // 提取等号后面的值
|
|
12
|
-
break
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
if (!sourcePath) {
|
|
16
|
-
throw new Error('missing --source=<path>')
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if (
|
|
20
|
-
!fs.existsSync(path.join(sourcePath, 'node_modules')) ||
|
|
21
|
-
!fs.statSync(path.join(sourcePath, 'node_modules')).isDirectory() ||
|
|
22
|
-
fs.existsSync(path.join(sourcePath, 'package.json'))
|
|
23
|
-
) {
|
|
24
|
-
throw new Error('not a workspace: ' + sourcePath)
|
|
25
|
-
}
|
|
26
|
-
const esmPath = path.resolve(sourcePath, '.output', 'esm')
|
|
27
|
-
if (fs.existsSync(esmPath)) {
|
|
28
|
-
fs.rmSync(esmPath, { recursive: true, force: true })
|
|
29
|
-
}
|
|
30
|
-
fs.mkdirSync(esmPath, { recursive: true })
|
|
31
|
-
|
|
32
|
-
const files = fs.readdirSync(sourcePath)
|
|
33
|
-
for (const file of files) {
|
|
34
|
-
if (file.endsWith('.ts')) {
|
|
35
|
-
console.log('compile', `${sourcePath.replaceAll('\\', '/')}/${file}`)
|
|
36
|
-
esbuild.build({
|
|
37
|
-
bundle: true,
|
|
38
|
-
entryPoints: [`${sourcePath.replaceAll('\\', '/')}/${file}`],
|
|
39
|
-
drop: ['debugger'],
|
|
40
|
-
minify: true,
|
|
41
|
-
outfile: `${esmPath.replaceAll('\\', '/')}/${file.replace(/\.ts$/, '.mjs')}`,
|
|
42
|
-
// sourcemap: true,
|
|
43
|
-
format: 'esm',
|
|
44
|
-
// format: 'cjs',
|
|
45
|
-
platform: 'node',
|
|
46
|
-
plugins: [],
|
|
47
|
-
target: 'node18',
|
|
48
|
-
// tsconfig: path.join(__dirname, '..', 'tsconfig.build-cli.json'),
|
|
49
|
-
tsconfig: path.join(__dirname, '..', 'tsconfig.json'),
|
|
50
|
-
})
|
|
51
|
-
}
|
|
52
|
-
}
|
|
1
|
+
import * as esbuild from 'esbuild'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
|
|
5
|
+
// 获取命令行参数
|
|
6
|
+
const args = process.argv.slice(2) // 从第3个元素开始截取(跳过node和脚本路径)
|
|
7
|
+
// 查找并解析 --source 参数
|
|
8
|
+
let sourcePath = null
|
|
9
|
+
for (const arg of args) {
|
|
10
|
+
if (arg.startsWith('--source=')) {
|
|
11
|
+
sourcePath = arg.split('=')[1] // 提取等号后面的值
|
|
12
|
+
break
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
if (!sourcePath) {
|
|
16
|
+
throw new Error('missing --source=<path>')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (
|
|
20
|
+
!fs.existsSync(path.join(sourcePath, 'node_modules')) ||
|
|
21
|
+
!fs.statSync(path.join(sourcePath, 'node_modules')).isDirectory() ||
|
|
22
|
+
fs.existsSync(path.join(sourcePath, 'package.json'))
|
|
23
|
+
) {
|
|
24
|
+
throw new Error('not a workspace: ' + sourcePath)
|
|
25
|
+
}
|
|
26
|
+
const esmPath = path.resolve(sourcePath, '.output', 'esm')
|
|
27
|
+
if (fs.existsSync(esmPath)) {
|
|
28
|
+
fs.rmSync(esmPath, { recursive: true, force: true })
|
|
29
|
+
}
|
|
30
|
+
fs.mkdirSync(esmPath, { recursive: true })
|
|
31
|
+
|
|
32
|
+
const files = fs.readdirSync(sourcePath)
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
if (file.endsWith('.ts')) {
|
|
35
|
+
console.log('compile', `${sourcePath.replaceAll('\\', '/')}/${file}`)
|
|
36
|
+
esbuild.build({
|
|
37
|
+
bundle: true,
|
|
38
|
+
entryPoints: [`${sourcePath.replaceAll('\\', '/')}/${file}`],
|
|
39
|
+
drop: ['debugger'],
|
|
40
|
+
minify: true,
|
|
41
|
+
outfile: `${esmPath.replaceAll('\\', '/')}/${file.replace(/\.ts$/, '.mjs')}`,
|
|
42
|
+
// sourcemap: true,
|
|
43
|
+
format: 'esm',
|
|
44
|
+
// format: 'cjs',
|
|
45
|
+
platform: 'node',
|
|
46
|
+
plugins: [],
|
|
47
|
+
target: 'node18',
|
|
48
|
+
// tsconfig: path.join(__dirname, '..', 'tsconfig.build-cli.json'),
|
|
49
|
+
tsconfig: path.join(__dirname, '..', 'tsconfig.json'),
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
}
|
package/scripts/sync-version.mjs
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import fs from 'node:fs'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
|
|
4
|
-
function syncVersion() {
|
|
5
|
-
const rootPath = path.resolve(import.meta.dirname, '..')
|
|
6
|
-
const packageInfo = JSON.parse(fs.readFileSync(path.join(rootPath, 'package.json'), 'utf-8'))
|
|
7
|
-
const version = packageInfo.version
|
|
8
|
-
if (!version) {
|
|
9
|
-
console.error('version not found')
|
|
10
|
-
process.exit(1)
|
|
11
|
-
}
|
|
12
|
-
fs.readdirSync(path.resolve(rootPath, 'packages')).forEach((pkgName) => {
|
|
13
|
-
const pkgPath = path.resolve(rootPath, 'packages', pkgName)
|
|
14
|
-
if (fs.existsSync(path.join(pkgPath, 'package.json'))) {
|
|
15
|
-
const pkgInfo = JSON.parse(fs.readFileSync(path.join(pkgPath, 'package.json'), 'utf-8'))
|
|
16
|
-
pkgInfo.version = version
|
|
17
|
-
fs.writeFileSync(path.join(pkgPath, 'package.json'), JSON.stringify(pkgInfo, null, 2) + '\n', 'utf-8')
|
|
18
|
-
}
|
|
19
|
-
})
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
syncVersion()
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
function syncVersion() {
|
|
5
|
+
const rootPath = path.resolve(import.meta.dirname, '..')
|
|
6
|
+
const packageInfo = JSON.parse(fs.readFileSync(path.join(rootPath, 'package.json'), 'utf-8'))
|
|
7
|
+
const version = packageInfo.version
|
|
8
|
+
if (!version) {
|
|
9
|
+
console.error('version not found')
|
|
10
|
+
process.exit(1)
|
|
11
|
+
}
|
|
12
|
+
fs.readdirSync(path.resolve(rootPath, 'packages')).forEach((pkgName) => {
|
|
13
|
+
const pkgPath = path.resolve(rootPath, 'packages', pkgName)
|
|
14
|
+
if (fs.existsSync(path.join(pkgPath, 'package.json'))) {
|
|
15
|
+
const pkgInfo = JSON.parse(fs.readFileSync(path.join(pkgPath, 'package.json'), 'utf-8'))
|
|
16
|
+
pkgInfo.version = version
|
|
17
|
+
fs.writeFileSync(path.join(pkgPath, 'package.json'), JSON.stringify(pkgInfo, null, 2) + '\n', 'utf-8')
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
syncVersion()
|
package/templates/CLAUDE.md
CHANGED
|
@@ -18,6 +18,37 @@ import { createDomainDesigner } from '@ddd-tool/domain-designer-core'
|
|
|
18
18
|
const d = createDomainDesigner()
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
### Initialization Options
|
|
22
|
+
|
|
23
|
+
Optional parameters for `createDomainDesigner()`:
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
const d = createDomainDesigner({
|
|
27
|
+
moduleName: 'order', // Module name for code generation
|
|
28
|
+
ignoreValueObjects: ['time'], // Value object names to ignore during code generation
|
|
29
|
+
})
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
| Parameter | Type | Description | Default Value |
|
|
33
|
+
| -------------------- | -------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
34
|
+
| `moduleName` | string | Module name for code bundling | File name |
|
|
35
|
+
| `ignoreValueObjects` | string[] | Value object names to skip during code generation | ['time', 'id', 'pid', 'name', 'state', 'status', 'version', 'code', 'message', 'type', 'result', 'data', 'payload', 'meta', 'context', 'sorting'] |
|
|
36
|
+
|
|
37
|
+
**Why `ignoreValueObjects`?** Generic names like `time` or `name` have weak business meaning. During code generation, they should be treated as primitive types rather than value objects.
|
|
38
|
+
|
|
39
|
+
### Common Pattern: `const i = d.info`
|
|
40
|
+
|
|
41
|
+
It's common to alias `d.info` as `i` for brevity:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
const d = createDomainDesigner()
|
|
45
|
+
const i = d.info
|
|
46
|
+
|
|
47
|
+
// Now you can use i instead of d.info
|
|
48
|
+
const userId = i.id('userId')
|
|
49
|
+
const userName = i.valueObj('userName', '用户名')
|
|
50
|
+
```
|
|
51
|
+
|
|
21
52
|
### Available Methods
|
|
22
53
|
|
|
23
54
|
| Method | Purpose |
|
|
@@ -47,6 +78,32 @@ const workflowName = d.startWorkflow('CreateOrder')
|
|
|
47
78
|
actor.command(createOrder).agg(orderAgg).event(orderCreated)
|
|
48
79
|
```
|
|
49
80
|
|
|
81
|
+
### Node Method Reference
|
|
82
|
+
|
|
83
|
+
Each type of node has specific methods that can be called in a workflow:
|
|
84
|
+
|
|
85
|
+
| Node Type | Available Methods | Description |
|
|
86
|
+
| :---------------- | :----------------------------------------------------- | :--------------------------------------- |
|
|
87
|
+
| **Actor** | `.command()`, `.facadeCmd()`, `.readModel()` | Initiates commands or reads data |
|
|
88
|
+
| **Command** | `.agg()` | Targets an aggregate |
|
|
89
|
+
| **FacadeCommand** | `.agg()`, `.service()` | Targets aggregate or calls service |
|
|
90
|
+
| **Aggregate** | `.event()`, `.command()` | Emits event or creates nested command |
|
|
91
|
+
| **Event** | `.policy()`, `.system()`, `.readModel()`, `.command()` | Triggers policy/system/readModel/command |
|
|
92
|
+
| **Policy** | `.command()`, `.facadeCmd()`, `.service()` | Issues commands or calls service |
|
|
93
|
+
| **Service** | `.command()`, `.facadeCmd()`, `.agg()` | Issues commands or targets aggregate |
|
|
94
|
+
| **System** | `.command()`, `.facadeCmd()`, `.event()` | Issues commands or emits events |
|
|
95
|
+
|
|
96
|
+
**Important**: Each method call in a workflow returns a reference to the target node, allowing you to continue chaining:
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
d.startWorkflow('Example')
|
|
100
|
+
// Actor → Command → Aggregate → Event
|
|
101
|
+
customer.command(createOrder).agg(orderAgg).event(orderCreated)
|
|
102
|
+
// ↓
|
|
103
|
+
// Event → Policy
|
|
104
|
+
// orderCreated.policy(autoConfirmPolicy)
|
|
105
|
+
```
|
|
106
|
+
|
|
50
107
|
### Complete Example
|
|
51
108
|
|
|
52
109
|
```typescript
|
|
@@ -114,14 +171,15 @@ For larger domains, split into modules:
|
|
|
114
171
|
// common.ts - Shared setup
|
|
115
172
|
import { createDomainDesigner } from '@ddd-tool/domain-designer-core'
|
|
116
173
|
export const d = createDomainDesigner({ moduleName: 'my-domain' })
|
|
174
|
+
export const i = d.info
|
|
117
175
|
|
|
118
176
|
// user.ts - User domain
|
|
119
|
-
import { d } from './common'
|
|
177
|
+
import { d, i } from './common'
|
|
120
178
|
const user = d.actor('用户')
|
|
121
179
|
// ... more definitions
|
|
122
180
|
|
|
123
181
|
// book.ts - Book domain
|
|
124
|
-
import { d } from './common'
|
|
182
|
+
import { d, i } from './common'
|
|
125
183
|
const book = d.agg('图书')
|
|
126
184
|
// ... more definitions
|
|
127
185
|
|
|
@@ -132,14 +190,193 @@ import './book'
|
|
|
132
190
|
export default d
|
|
133
191
|
```
|
|
134
192
|
|
|
193
|
+
### Value Pool Pattern ("常量池")
|
|
194
|
+
|
|
195
|
+
For domains with multiple related aggregates, organize reusable values into "value pools":
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
// book.ts
|
|
199
|
+
import { d, i } from './common'
|
|
200
|
+
|
|
201
|
+
export const bookValues = {
|
|
202
|
+
ISBN: i.id('isbn', '国际标准书号 - 一书一码'),
|
|
203
|
+
图书名称: i.valueObj('bookName', '图书名称'),
|
|
204
|
+
图书价格: i.valueObj('bookPrice', '图书价格'),
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const 图书聚合 = d.agg('BookAgg', [bookValues.ISBN, bookValues.图书名称], '图书聚合')
|
|
208
|
+
|
|
209
|
+
// order.ts
|
|
210
|
+
import { d, i } from './common'
|
|
211
|
+
import { bookValues } from './book'
|
|
212
|
+
|
|
213
|
+
export const orderValues = {
|
|
214
|
+
订单流水号: i.id('orderSequence', '订单流水号'),
|
|
215
|
+
订购数量: i.valueObj('quantity', '订购数量'),
|
|
216
|
+
最终价格: i.func(
|
|
217
|
+
'最终价格',
|
|
218
|
+
[bookValues.图书价格, orderValues.订购数量],
|
|
219
|
+
'最终价格 = 图书价格 * 订购数量',
|
|
220
|
+
),
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Cross-reference values using d.note()
|
|
224
|
+
const 全局流水号 = i.valueObj('globalSequence', d.note`全局流水号由${上游系统}接口统一生成`)
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**Benefits of value pools:**
|
|
228
|
+
|
|
229
|
+
- Avoids repeated definitions
|
|
230
|
+
- Serves as a "glossary" for domain terms
|
|
231
|
+
- Makes cross-references explicit using `d.note()`
|
|
232
|
+
|
|
135
233
|
## Value Object Types
|
|
136
234
|
|
|
137
235
|
```typescript
|
|
138
236
|
d.info.id('name') // ID type
|
|
139
|
-
d.info.document('name') // Document type
|
|
140
|
-
d.info.func('name') // Function type
|
|
237
|
+
d.info.document('name') // Document type (experimental)
|
|
238
|
+
d.info.func('name') // Function type (experimental)
|
|
141
239
|
d.info.valueObj('name') // Value Object type
|
|
142
|
-
d.info.version('name') // Version type
|
|
240
|
+
d.info.version('name') // Version type (experimental)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Naming Conventions
|
|
244
|
+
|
|
245
|
+
**Variable names**: Use Chinese/native language for better domain understanding:
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
const 图书价格 = i.valueObj('bookPrice', '图书价格')
|
|
249
|
+
const 用户名 = i.valueObj('userName', '用户名')
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
**Type/Node names**: Use English for the first parameter (actual code name), Chinese for documentation:
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
// First parameter = code name (English)
|
|
256
|
+
// Second parameter = documentation (Chinese)
|
|
257
|
+
const 图书价格 = i.valueObj('bookPrice', '图书价格')
|
|
258
|
+
d.command('CreateUser', [用户名, 邮箱], '创建用户')
|
|
259
|
+
d.agg('UserAgg', [用户id], '用户聚合')
|
|
260
|
+
d.event('UserCreated', [用户id], '用户已创建')
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Shorthand Syntax
|
|
264
|
+
|
|
265
|
+
All nodes accept plain strings as parameters. The shorthand `['fieldName', '备注']` syntax is equivalent to `i.valueObj('fieldName', '备注')`:
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
// Both are equivalent - use d.info functions for clarity
|
|
269
|
+
d.command('CreateUser', [i.valueObj('userName'), i.valueObj('email'), i.valueObj('gender', '性别')])
|
|
270
|
+
|
|
271
|
+
// Shorthand form
|
|
272
|
+
d.command('CreateUser', ['userName', 'email', ['gender', '性别']])
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
**Recommendation**: Prefer `d.info` functions for explicit type marking, especially for value objects that have business meaning. Use shorthand only for generic fields like `time` or `id`.
|
|
276
|
+
|
|
277
|
+
### Experimental Features
|
|
278
|
+
|
|
279
|
+
The following value types are experimental and their necessity is still being evaluated:
|
|
280
|
+
|
|
281
|
+
- **`document`** - For document or binary file values
|
|
282
|
+
- **`func`** - For values computed by functions
|
|
283
|
+
- **`version`** - For version numbers with business meaning
|
|
284
|
+
|
|
285
|
+
## Adding Documentation (Notes)
|
|
286
|
+
|
|
287
|
+
Most node functions accept a **last parameter** for documentation:
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
// Simple documentation using a string
|
|
291
|
+
const orderCreated = d.event('OrderCreated', [orderId], '订单已创建')
|
|
292
|
+
const orderAgg = d.agg('OrderAgg', [orderId], '订单聚合')
|
|
293
|
+
const createOrder = d.command('CreateOrder', [orderId], '创建订单')
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Advanced Documentation with `d.note()`
|
|
297
|
+
|
|
298
|
+
`d.note()` is an advanced form of documentation that enables:
|
|
299
|
+
|
|
300
|
+
1. **Referencing other nodes** using `${node}` syntax (creates traceability)
|
|
301
|
+
2. **Multi-line business rules** with numbered lists
|
|
302
|
+
3. **Cross-module references** between domain concepts
|
|
303
|
+
|
|
304
|
+
#### When to Use `d.note()`
|
|
305
|
+
|
|
306
|
+
**Use simple strings** for basic descriptions:
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
const addBook = d.command('AddToAvailableBooksCmd', [isbn, qrCode], '添加可预订书')
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
**Use `d.note()` when you need to:**
|
|
313
|
+
|
|
314
|
+
- Reference other nodes/values in the domain
|
|
315
|
+
- Document complex business rules
|
|
316
|
+
- Explain domain-specific concepts
|
|
317
|
+
|
|
318
|
+
#### Referencing Other Nodes
|
|
319
|
+
|
|
320
|
+
Use the `${node}` syntax to create explicit links between concepts:
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
// In user.ts
|
|
324
|
+
export const userValues = {
|
|
325
|
+
用户id: i.id('userId'),
|
|
326
|
+
逾期次数: i.valueObj('overdueCount', '逾期次数'),
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// In book.ts - reference using ${userValues.逾期次数}
|
|
330
|
+
const timeoutCommand = d.command(
|
|
331
|
+
'TimeOutBorrowing',
|
|
332
|
+
['借书id'],
|
|
333
|
+
d.note`逾期
|
|
334
|
+
1. 书被借出,且1个月未还
|
|
335
|
+
2. 增加借书会员的${userValues.逾期次数}`,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
// Creates traceability - shows which value is being modified
|
|
339
|
+
const suspendPolicy = d.policy('SuspendAccountWhenTimeOut', d.note`增加账户${userValues.逾期次数}`)
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
#### Business Rules with Lists
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
const reserveBook = d.command(
|
|
346
|
+
'ReserveBook',
|
|
347
|
+
[isbn, 'userId'],
|
|
348
|
+
d.note`预定书
|
|
349
|
+
1. 有可预定的书
|
|
350
|
+
2. 会员账户没有被暂停
|
|
351
|
+
3. 会员已预定或者借出的书小于3本`,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
const borrowService = d.service(
|
|
355
|
+
'BorrowBookService',
|
|
356
|
+
d.note`借书服务
|
|
357
|
+
书可预定则可借出
|
|
358
|
+
书已预定时借书人是预定则可借`,
|
|
359
|
+
)
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
#### Complex Value Objects
|
|
363
|
+
|
|
364
|
+
For domain-specific or complex types:
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
const qrCodeSet = i.valueObj('二维码集合', d.note`${bookValues.二维码} // 一书一码,业务唯一标识`)
|
|
368
|
+
|
|
369
|
+
const isbn = i.valueObj('isbn', d.note`国际标准书号 (ISBN-13)`)
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
**Key Point**: `d.note()` creates traceability. When a business rule mentions another concept, reference it explicitly:
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
// Good - Creates explicit link
|
|
376
|
+
d.note`增加${userValues.逾期次数}`
|
|
377
|
+
|
|
378
|
+
// Avoid - No traceability
|
|
379
|
+
d.note`增加逾期计数`
|
|
143
380
|
```
|
|
144
381
|
|
|
145
382
|
## Modeling Guidelines
|
|
@@ -191,20 +428,44 @@ const orderCreated = d.command('订单已创建') // Avoid - sounds like an even
|
|
|
191
428
|
Policies automate business rules:
|
|
192
429
|
|
|
193
430
|
```typescript
|
|
194
|
-
const
|
|
431
|
+
const paymentTimeoutPolicy = d.policy('支付超时取消规则')
|
|
195
432
|
const cancelOrder = d.command('取消订单')
|
|
433
|
+
const orderCancelled = d.event('订单已取消')
|
|
434
|
+
const orderAgg = d.agg('订单聚合', [d.info.id('订单号')])
|
|
196
435
|
|
|
197
|
-
// When payment timeout event occurs,
|
|
198
|
-
|
|
436
|
+
// When payment timeout event occurs, policy triggers cancellation
|
|
437
|
+
d.startWorkflow('支付超时取消流程')
|
|
438
|
+
paymentTimeout.policy(paymentTimeoutPolicy).command(cancelOrder).agg(orderAgg).event(orderCancelled)
|
|
199
439
|
```
|
|
200
440
|
|
|
201
|
-
### External Systems
|
|
441
|
+
### Event to External Systems
|
|
202
442
|
|
|
203
|
-
|
|
443
|
+
Events can trigger actions in external systems:
|
|
204
444
|
|
|
205
445
|
```typescript
|
|
206
446
|
const emailSystem = d.system('邮件系统')
|
|
207
|
-
|
|
447
|
+
const smsSystem = d.system('短信系统')
|
|
448
|
+
|
|
449
|
+
// In a workflow, connect events to systems
|
|
450
|
+
d.startWorkflow('订单创建通知流程')
|
|
451
|
+
orderCreated.system(emailSystem)
|
|
452
|
+
orderCreated.system(smsSystem)
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Event to Read Models
|
|
456
|
+
|
|
457
|
+
Events update read models:
|
|
458
|
+
|
|
459
|
+
```typescript
|
|
460
|
+
const orderSummary = d.readModel('订单汇总读模型', [
|
|
461
|
+
d.info.id('订单号'),
|
|
462
|
+
d.info.document('商品信息'),
|
|
463
|
+
d.info.version('订单状态'),
|
|
464
|
+
])
|
|
465
|
+
|
|
466
|
+
// In a workflow, connect events to read models
|
|
467
|
+
d.startWorkflow('订单查询流程')
|
|
468
|
+
orderCreated.readModel(orderSummary)
|
|
208
469
|
```
|
|
209
470
|
|
|
210
471
|
### Read Models
|
|
@@ -265,7 +526,7 @@ When helping with domain modeling:
|
|
|
265
526
|
|
|
266
527
|
If you see errors like:
|
|
267
528
|
|
|
268
|
-
```
|
|
529
|
+
```text
|
|
269
530
|
The requested module does not provide an export named 'X'
|
|
270
531
|
```
|
|
271
532
|
|
|
@@ -144,19 +144,22 @@ const 逾期 = d.command(
|
|
|
144
144
|
['借书id'],
|
|
145
145
|
d.note`逾期
|
|
146
146
|
1.书被借出,且1个月未还
|
|
147
|
-
2
|
|
147
|
+
2.增加借书会员的${userValues.逾期次数}`,
|
|
148
148
|
)
|
|
149
149
|
|
|
150
150
|
const 书聚合 = d.agg('BookAgg', [bookValues.二维码], '书聚合')
|
|
151
151
|
|
|
152
152
|
const 用户占用书聚合 = d.agg('UserOcuppyBookAgg', [userValues.用户id, '占用数量'], '用户占用书聚合')
|
|
153
153
|
|
|
154
|
-
const 增加账户逾期次数规则 = d.policy(
|
|
154
|
+
const 增加账户逾期次数规则 = d.policy(
|
|
155
|
+
'SuspendAccountWhenTimeOut',
|
|
156
|
+
d.note`增加账户${userValues.逾期次数}`,
|
|
157
|
+
)
|
|
155
158
|
|
|
156
159
|
const 增加逾期次数 = d.command(
|
|
157
160
|
'IncreaseAccountTimeOutCount',
|
|
158
161
|
[userValues.用户id],
|
|
159
|
-
d.note
|
|
162
|
+
d.note`增加${userValues.逾期次数}
|
|
160
163
|
1.会员累计逾期达到3次暂停账户
|
|
161
164
|
2.会员账户当前是启用才能被暂停`,
|
|
162
165
|
)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.3.
|
|
1
|
+
0.3.2
|