tshex-cli 1.0.23 → 1.0.25
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/build/main.js +3 -3
- package/docs/context-ports.md +70 -109
- package/docs/generated-file-reference.md +6 -6
- package/docs/library-structure.md +62 -49
- package/docs/shared/application/data.md +9 -4
- package/docs/shared/application/events.md +7 -5
- package/docs/shared/application/http.md +6 -3
- package/docs/shared/application/loggers.md +6 -3
- package/docs/shared/application/services.md +7 -3
- package/docs/shared/domain/aggregates.md +125 -25
- package/package.json +2 -1
- package/source/main.ts +9 -3
package/build/main.js
CHANGED
|
@@ -45,7 +45,7 @@ function executeCreateReactContext(templatesDir, contextDir) {
|
|
|
45
45
|
console.error(err);
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
-
function executeCreateTests(sourceDir, destinationDir, fileContents, ignoredSourceDir) {
|
|
48
|
+
function executeCreateTests(sourceDir, destinationDir, fileContents, ignoredSourceDir, rootSourceDir = sourceDir) {
|
|
49
49
|
const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
|
|
50
50
|
if (fs.existsSync(destinationDir) === false) {
|
|
51
51
|
fs.mkdirSync(destinationDir, { recursive: true });
|
|
@@ -54,7 +54,7 @@ function executeCreateTests(sourceDir, destinationDir, fileContents, ignoredSour
|
|
|
54
54
|
const sourcePath = path.join(sourceDir, entry.name);
|
|
55
55
|
const destinationPath = path.join(destinationDir, entry.name);
|
|
56
56
|
if (entry.isDirectory()) {
|
|
57
|
-
if (entry.name === 'shared') {
|
|
57
|
+
if (entry.name === 'shared' && sourceDir === rootSourceDir) {
|
|
58
58
|
continue;
|
|
59
59
|
}
|
|
60
60
|
if (ignoredSourceDir !== undefined && sourcePath.startsWith(ignoredSourceDir)) {
|
|
@@ -63,7 +63,7 @@ function executeCreateTests(sourceDir, destinationDir, fileContents, ignoredSour
|
|
|
63
63
|
if (fs.existsSync(destinationPath) && fs.statSync(destinationPath).isDirectory() === false) {
|
|
64
64
|
continue;
|
|
65
65
|
}
|
|
66
|
-
executeCreateTests(sourcePath, destinationPath, fileContents, ignoredSourceDir);
|
|
66
|
+
executeCreateTests(sourcePath, destinationPath, fileContents, ignoredSourceDir, rootSourceDir);
|
|
67
67
|
continue;
|
|
68
68
|
}
|
|
69
69
|
if (entry.isFile() && entry.name.endsWith('.ts') && fs.existsSync(destinationPath) === false) {
|
package/docs/context-ports.md
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
### Context Ports
|
|
2
2
|
|
|
3
3
|
Context ports define the communication available at a context boundary.
|
|
4
|
-
They describe
|
|
5
|
-
|
|
4
|
+
They describe which concrete capability a context exposes, what data enters
|
|
5
|
+
that capability, and what data it returns.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
In practice a port is a specific boundary element of the system with identity:
|
|
8
|
+
a command handler, a query entry point, an event consumer, a published
|
|
9
|
+
endpoint, or another concrete interaction mechanism that exists because the
|
|
10
|
+
running system exposes it.
|
|
11
|
+
|
|
12
|
+
Types and interfaces still matter, but they are secondary. Their role is to
|
|
13
|
+
make the port explicit. The main concern is the port as a real executable
|
|
14
|
+
surface that another actor can call or observe.
|
|
10
15
|
|
|
11
16
|
> **Hint**
|
|
12
17
|
> The generated `example-ports.ts` file is only a placeholder. Replace it when
|
|
@@ -17,149 +22,105 @@ application process.
|
|
|
17
22
|
The generated context starts with a single root file for ports.
|
|
18
23
|
|
|
19
24
|
```ts title="users/example-ports.ts"
|
|
20
|
-
export
|
|
21
|
-
|
|
25
|
+
export class ExamplePort {
|
|
26
|
+
public doSomething(): void {
|
|
27
|
+
// ...
|
|
28
|
+
}
|
|
22
29
|
}
|
|
23
30
|
```
|
|
24
31
|
|
|
25
|
-
This placeholder
|
|
26
|
-
|
|
32
|
+
This placeholder marks the context root as the place where boundary-facing
|
|
33
|
+
capabilities are declared. Replace it with the first module that defines a real
|
|
34
|
+
port of the context when that capability becomes clear.
|
|
35
|
+
|
|
36
|
+
Context root `.ts` files belong to this same boundary surface. Each one is
|
|
37
|
+
expected to be a module that defines one or more context ports.
|
|
27
38
|
|
|
28
|
-
#### First
|
|
39
|
+
#### First Port
|
|
29
40
|
|
|
30
|
-
In the following example we replace the placeholder with a
|
|
41
|
+
In the following example we replace the placeholder with a concrete port for
|
|
31
42
|
creating a user.
|
|
32
43
|
|
|
33
44
|
```ts title="users/example-ports.ts"
|
|
34
|
-
export
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
active: boolean | null
|
|
39
|
-
}
|
|
45
|
+
export interface User {
|
|
46
|
+
id: string
|
|
47
|
+
email: string
|
|
48
|
+
active: boolean | null
|
|
40
49
|
}
|
|
41
50
|
|
|
42
|
-
export
|
|
43
|
-
user: {
|
|
44
|
-
|
|
45
|
-
email: string
|
|
46
|
-
active: boolean | null
|
|
51
|
+
export class UsersRegistry {
|
|
52
|
+
public async createUser(user: User): Promise<User> {
|
|
53
|
+
// ...
|
|
47
54
|
}
|
|
48
55
|
}
|
|
49
|
-
|
|
50
|
-
export interface CreateUserPort {
|
|
51
|
-
create(request: CreateUserRequest): Promise<CreateUserResponse>
|
|
52
|
-
}
|
|
53
56
|
```
|
|
54
57
|
|
|
55
|
-
`
|
|
56
|
-
the
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
This contract says nothing about HTTP, queues, or databases. It only declares
|
|
60
|
-
the shape of the communication.
|
|
61
|
-
|
|
62
|
-
#### Adapter Implementation
|
|
63
|
-
|
|
64
|
-
Now that the port exists, an adapter can implement it and delegate the work to
|
|
65
|
-
an application service.
|
|
66
|
-
|
|
67
|
-
```ts title="users/adapters/create-user.ts"
|
|
68
|
-
import type {
|
|
69
|
-
CreateUserPort,
|
|
70
|
-
CreateUserRequest,
|
|
71
|
-
CreateUserResponse,
|
|
72
|
-
} from '../example-ports.js'
|
|
58
|
+
`User` expresses the user data that crosses the boundary. `UsersRegistry` is a
|
|
59
|
+
boundary object of the context and `createUser()` is one concrete capability
|
|
60
|
+
that it exposes.
|
|
73
61
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
email: string
|
|
78
|
-
active: boolean | null
|
|
79
|
-
}): Promise<CreateUserResponse>
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export class CreateUserAdapter implements CreateUserPort {
|
|
83
|
-
public constructor(
|
|
84
|
-
private readonly service: CreateUserService,
|
|
85
|
-
) {}
|
|
86
|
-
|
|
87
|
-
public async create(
|
|
88
|
-
request: CreateUserRequest,
|
|
89
|
-
): Promise<CreateUserResponse> {
|
|
90
|
-
return this.service.execute({
|
|
91
|
-
id: request.user.id,
|
|
92
|
-
email: request.user.email,
|
|
93
|
-
active: request.user.active,
|
|
94
|
-
})
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
```
|
|
62
|
+
This definition focuses on the interaction the context makes available. The
|
|
63
|
+
port keeps its identity as one boundary capability while making its data and
|
|
64
|
+
action explicit.
|
|
98
65
|
|
|
99
|
-
The
|
|
100
|
-
|
|
101
|
-
application service.
|
|
66
|
+
The port is the executable object that this context exposes. Types help
|
|
67
|
+
describe it and make its boundary explicit.
|
|
102
68
|
|
|
103
|
-
This is the normal flow
|
|
69
|
+
This is the normal flow inside the context boundary:
|
|
104
70
|
|
|
105
|
-
```
|
|
106
|
-
|
|
71
|
+
```mermaid
|
|
72
|
+
flowchart LR
|
|
73
|
+
caller["Caller"] --> port[Concrete port]
|
|
74
|
+
port --> application[Application]
|
|
75
|
+
application --> domain[Domain]
|
|
107
76
|
```
|
|
108
77
|
|
|
109
|
-
The port belongs to the boundary
|
|
110
|
-
application process executes the use case
|
|
78
|
+
The port belongs to the boundary because it is part of what the context
|
|
79
|
+
exposes. The application process executes the use case behind the exposed
|
|
80
|
+
capability and uses domain capabilities.
|
|
111
81
|
|
|
112
82
|
#### Multiple Port Files
|
|
113
83
|
|
|
114
|
-
As the context grows, you can keep several
|
|
84
|
+
As the context grows, you can keep several port modules at the context root.
|
|
115
85
|
|
|
116
|
-
```ts title="users/
|
|
117
|
-
export
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
export type ListUsersResponse = {
|
|
122
|
-
users: Array<{
|
|
123
|
-
id: string
|
|
124
|
-
email: string
|
|
125
|
-
active: boolean | null
|
|
126
|
-
}>
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export interface ListUsersPort {
|
|
130
|
-
list(request: ListUsersRequest): Promise<ListUsersResponse>
|
|
86
|
+
```ts title="users/registry.ts"
|
|
87
|
+
export class UsersRegistry {
|
|
88
|
+
public async listUsers(): Promise<User[]> {
|
|
89
|
+
// ...
|
|
90
|
+
}
|
|
131
91
|
}
|
|
132
92
|
```
|
|
133
93
|
|
|
134
|
-
|
|
94
|
+
Another caller can then import the port from the file that owns it.
|
|
135
95
|
|
|
136
|
-
This arrangement is useful when one context exposes several independent
|
|
137
|
-
|
|
138
|
-
become easier to maintain when
|
|
96
|
+
This arrangement is useful when one context exposes several independent
|
|
97
|
+
capabilities. A single file works well for a small context. Separate files
|
|
98
|
+
become easier to maintain when each port has its own identity and
|
|
99
|
+
responsibility.
|
|
139
100
|
|
|
140
101
|
> **Warning**
|
|
141
|
-
> A port should define
|
|
142
|
-
> rules, repository logic,
|
|
102
|
+
> A port should define an exposed boundary capability.
|
|
103
|
+
> Keep business rules, repository logic, and infrastructure details in their
|
|
104
|
+
> corresponding layers.
|
|
143
105
|
|
|
144
106
|
#### Example Layout
|
|
145
107
|
|
|
146
108
|
The following structure keeps ports at the root while the implementation lives
|
|
147
109
|
in the generated folders.
|
|
148
110
|
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
├── application/
|
|
155
|
-
└── domain/
|
|
111
|
+
```mermaid
|
|
112
|
+
flowchart TD
|
|
113
|
+
users["users/"] --> registry["registry.ts"]
|
|
114
|
+
users --> application["application/"]
|
|
115
|
+
users --> domain["domain/"]
|
|
156
116
|
```
|
|
157
117
|
|
|
158
118
|
This layout keeps the context boundary visible from the top level. It also
|
|
159
|
-
|
|
119
|
+
makes each exposed capability easy to locate because the port modules stay at
|
|
120
|
+
the root of the context.
|
|
160
121
|
|
|
161
122
|
#### Next Step
|
|
162
123
|
|
|
163
|
-
After defining a port, implement the corresponding
|
|
164
|
-
application service. The surrounding structure is described in
|
|
165
|
-
`library-structure.md`.
|
|
124
|
+
After defining a port, implement the corresponding executable path and connect
|
|
125
|
+
it to an application service. The surrounding structure is described in
|
|
126
|
+
`library-structure.md`.
|
|
@@ -61,14 +61,14 @@ application coordinates data access without forcing a specific driver.
|
|
|
61
61
|
|
|
62
62
|
#### Context Files
|
|
63
63
|
|
|
64
|
-
Each generated context starts with
|
|
64
|
+
Each generated context starts with an example port file and three directories.
|
|
65
65
|
|
|
66
66
|
| Path | Responsibility |
|
|
67
67
|
| --- | --- |
|
|
68
|
-
| `users/example-ports.ts` |
|
|
68
|
+
| `users/example-ports.ts` | Example module for context ports. All context root `.ts` files are expected to define context ports. |
|
|
69
69
|
| `users/domain/` | Domain capabilities and rules for the context. |
|
|
70
70
|
| `users/application/` | Processes that use domain capabilities to fulfill system purposes. |
|
|
71
|
-
| `users/adapters/` | Integrations that
|
|
71
|
+
| `users/adapters/` | Integrations that wraps third-party libraries or context ports. |
|
|
72
72
|
|
|
73
73
|
The `users/` path is an example context name. Your project can generate one or
|
|
74
74
|
more contexts with the same internal layout.
|
|
@@ -81,8 +81,8 @@ Use the following sequence when deciding where new code belongs.
|
|
|
81
81
|
2. Put reusable application contracts in `shared/application`.
|
|
82
82
|
3. Put context-specific rules in `<context>/domain`.
|
|
83
83
|
4. Put use cases in `<context>/application`.
|
|
84
|
-
5. Put
|
|
85
|
-
6. Put boundary
|
|
84
|
+
5. Put transport and infrastructure integrations in `<context>/adapters`.
|
|
85
|
+
6. Put concrete boundary ports in context root `.ts` files.
|
|
86
86
|
|
|
87
87
|
This reference explains placement. The architectural rationale is described in
|
|
88
|
-
`library-structure.md`.
|
|
88
|
+
`library-structure.md`.
|
|
@@ -13,11 +13,12 @@ language, rules, and operations.
|
|
|
13
13
|
|
|
14
14
|
The root contains the entry points of the generated library.
|
|
15
15
|
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
```mermaid
|
|
17
|
+
flowchart TD
|
|
18
|
+
root["Library root"] --> index["index.d.ts"]
|
|
19
|
+
root --> main["main.ts"]
|
|
20
|
+
root --> shared["shared/"]
|
|
21
|
+
root --> users["users/"]
|
|
21
22
|
```
|
|
22
23
|
|
|
23
24
|
`index.d.ts` defines root-level types. `main.ts` starts as a placeholder for
|
|
@@ -29,10 +30,10 @@ or more context directories.
|
|
|
29
30
|
The `shared` directory contains concepts that can be reused by multiple
|
|
30
31
|
contexts.
|
|
31
32
|
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
```mermaid
|
|
34
|
+
flowchart TD
|
|
35
|
+
shared["shared/"] --> application["application/"]
|
|
36
|
+
shared --> domain["domain/"]
|
|
36
37
|
```
|
|
37
38
|
|
|
38
39
|
`shared/domain` contains modeling foundations such as value objects, entities,
|
|
@@ -47,11 +48,12 @@ Until then, keep it close to the context that owns the rule.
|
|
|
47
48
|
A context groups the vocabulary, rules, and operations of one application
|
|
48
49
|
capability.
|
|
49
50
|
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
```mermaid
|
|
52
|
+
flowchart TD
|
|
53
|
+
contexts["Contexts"] --> users["users/"]
|
|
54
|
+
contexts --> billing["billing/"]
|
|
55
|
+
contexts --> inventory["inventory/"]
|
|
56
|
+
contexts --> sales["sales/"]
|
|
55
57
|
```
|
|
56
58
|
|
|
57
59
|
Each context can evolve independently while still reusing the abstractions from
|
|
@@ -62,17 +64,22 @@ the system.
|
|
|
62
64
|
|
|
63
65
|
Every generated context starts with the same internal structure.
|
|
64
66
|
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
```mermaid
|
|
68
|
+
flowchart TD
|
|
69
|
+
users["users/"] --> ports["example-ports.ts"]
|
|
70
|
+
users --> adapters["adapters/"]
|
|
71
|
+
users --> application["application/"]
|
|
72
|
+
users --> domain["domain/"]
|
|
71
73
|
```
|
|
72
74
|
|
|
73
|
-
`example-ports.ts` is the root communication surface of
|
|
75
|
+
`example-ports.ts` is an example module in the root communication surface of
|
|
76
|
+
the context.
|
|
74
77
|
`domain/` contains capabilities and rules. `application/` contains processes.
|
|
75
|
-
`adapters/` contains
|
|
78
|
+
`adapters/` contains integrations that wrap third-party libraries or context
|
|
79
|
+
ports.
|
|
80
|
+
|
|
81
|
+
Context root `.ts` files belong to the boundary surface of the context. Each
|
|
82
|
+
one is expected to be a module that defines one or more context ports.
|
|
76
83
|
|
|
77
84
|
#### Domain
|
|
78
85
|
|
|
@@ -103,34 +110,40 @@ business rules themselves.
|
|
|
103
110
|
The adapters layer contains the integrations that connect a context to other
|
|
104
111
|
systems.
|
|
105
112
|
|
|
106
|
-
An adapter
|
|
107
|
-
|
|
108
|
-
|
|
113
|
+
An adapter wraps a third-party library or a context port so that the context
|
|
114
|
+
can interact with a concrete transport or infrastructure path. An adapter can
|
|
115
|
+
expose an HTTP handler, consume a message, call a remote API, implement a data
|
|
116
|
+
driver, or connect to an event bus.
|
|
109
117
|
|
|
110
118
|
#### Ports
|
|
111
119
|
|
|
112
120
|
Ports define the communication available at the context boundary.
|
|
113
121
|
|
|
114
122
|
They live at the context root because they describe how the context is used
|
|
115
|
-
from the outside.
|
|
116
|
-
work
|
|
123
|
+
from the outside. A port is the concrete object the context exposes. Adapters
|
|
124
|
+
or other callers can use that port object and route work into an application
|
|
125
|
+
process.
|
|
117
126
|
|
|
118
|
-
The generated template starts with `example-ports.ts
|
|
119
|
-
|
|
120
|
-
`context-ports.md`.
|
|
127
|
+
The generated template starts with `example-ports.ts`. As the context grows,
|
|
128
|
+
additional context root `.ts` modules can define more ports. The detailed
|
|
129
|
+
guidance for that layout lives in `context-ports.md`.
|
|
121
130
|
|
|
122
131
|
#### Dependency Direction
|
|
123
132
|
|
|
124
133
|
The normal dependency direction is the following:
|
|
125
134
|
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
adapter
|
|
129
|
-
|
|
135
|
+
```mermaid
|
|
136
|
+
flowchart LR
|
|
137
|
+
adapter[Adapter] --> thirdParty["Third-party library"]
|
|
138
|
+
adapter --> port[Port]
|
|
139
|
+
port --> application[Application]
|
|
140
|
+
application --> domain
|
|
130
141
|
```
|
|
131
142
|
|
|
132
143
|
This direction keeps the core model isolated from transport and infrastructure
|
|
133
|
-
details.
|
|
144
|
+
details. Adapters integrate with external libraries and context ports. Ports
|
|
145
|
+
connect the context boundary to application processes. The deeper a layer is,
|
|
146
|
+
the less it should know about the outside.
|
|
134
147
|
|
|
135
148
|
> **Warning**
|
|
136
149
|
> Avoid importing adapter-specific concerns into the domain layer. Once a domain
|
|
@@ -141,23 +154,23 @@ details. The deeper a layer is, the less it should know about the outside.
|
|
|
141
154
|
|
|
142
155
|
The following diagram shows the runtime flow of a typical operation.
|
|
143
156
|
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
v
|
|
154
|
-
Domain capability
|
|
157
|
+
```mermaid
|
|
158
|
+
flowchart TD
|
|
159
|
+
main["main.ts"] --> system["Own system"]
|
|
160
|
+
system --> application["Application services"]
|
|
161
|
+
application --> domain["Domain capabilities"]
|
|
162
|
+
system --> adapter[Adapters]
|
|
163
|
+
adapter --> port[Port]
|
|
164
|
+
adapter --> thirdParty["Third-party libraries"]
|
|
165
|
+
thirdParty --> external["External systems"]
|
|
155
166
|
```
|
|
156
167
|
|
|
157
|
-
|
|
158
|
-
|
|
168
|
+
`main.ts` is the runtime entry point into the own system. Inside that system,
|
|
169
|
+
application services use domain capabilities, while adapters can depend on
|
|
170
|
+
ports and third-party libraries. The port branch stops at the boundary because
|
|
171
|
+
what exists beyond that port depends on the system that implements it.
|
|
159
172
|
|
|
160
173
|
#### Next Step
|
|
161
174
|
|
|
162
175
|
Use this structure as the default layout for new code. When you need to inspect
|
|
163
|
-
the purpose of a generated file, consult `generated-file-reference.md`.
|
|
176
|
+
the purpose of a generated file, consult `generated-file-reference.md`.
|
|
@@ -176,10 +176,15 @@ and the representation used by the context.
|
|
|
176
176
|
|
|
177
177
|
The normal flow of the data abstractions is the following:
|
|
178
178
|
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
service
|
|
179
|
+
```mermaid
|
|
180
|
+
flowchart LR
|
|
181
|
+
service[Service] --> repository[Repository]
|
|
182
|
+
repository --> driver[Driver]
|
|
183
|
+
driver --> manager["Data manager"]
|
|
184
|
+
manager --> raw["Raw records"]
|
|
185
|
+
repository --> transformed["Transformed records"]
|
|
186
|
+
transformed --> service
|
|
182
187
|
```
|
|
183
188
|
|
|
184
189
|
This separation keeps the application service focused on orchestration while
|
|
185
|
-
the repository focuses on transformation.
|
|
190
|
+
the repository focuses on transformation.
|
|
@@ -135,10 +135,12 @@ registered or executed.
|
|
|
135
135
|
|
|
136
136
|
The normal flow is the following:
|
|
137
137
|
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
138
|
+
```mermaid
|
|
139
|
+
flowchart LR
|
|
140
|
+
service[Service] --> dispatch["dispatch(event)"]
|
|
141
|
+
dispatch --> dispatcher[Dispatcher]
|
|
142
|
+
dispatcher --> handlers["Matching handlers"]
|
|
143
|
+
handlers --> effects["Side effects"]
|
|
142
144
|
```
|
|
143
145
|
|
|
144
|
-
This keeps the main process separate from secondary reactions.
|
|
146
|
+
This keeps the main process separate from secondary reactions.
|
|
@@ -123,9 +123,12 @@ cross-cutting behavior belongs in the generated HTTP abstraction.
|
|
|
123
123
|
|
|
124
124
|
#### Example Flow
|
|
125
125
|
|
|
126
|
-
```
|
|
127
|
-
|
|
126
|
+
```mermaid
|
|
127
|
+
flowchart LR
|
|
128
|
+
request[Request] --> middleware[Middleware]
|
|
129
|
+
middleware --> handler[Handler]
|
|
130
|
+
handler --> response["Response body"]
|
|
128
131
|
```
|
|
129
132
|
|
|
130
133
|
This flow keeps the transport boundary explicit while leaving framework choices
|
|
131
|
-
to the adapter layer.
|
|
134
|
+
to the adapter layer.
|
|
@@ -120,8 +120,11 @@ an external platform. It only depends on the application-level contract.
|
|
|
120
120
|
|
|
121
121
|
#### Example Flow
|
|
122
122
|
|
|
123
|
-
```
|
|
124
|
-
|
|
123
|
+
```mermaid
|
|
124
|
+
flowchart LR
|
|
125
|
+
service[Service] --> contract["Logger contract"]
|
|
126
|
+
contract --> adapter[Adapter]
|
|
127
|
+
adapter --> backend["Logging backend"]
|
|
125
128
|
```
|
|
126
129
|
|
|
127
|
-
This flow keeps observability concerns outside the core process.
|
|
130
|
+
This flow keeps observability concerns outside the core process.
|
|
@@ -112,9 +112,13 @@ result, depending on the requirements of the use case.
|
|
|
112
112
|
|
|
113
113
|
#### Example Flow
|
|
114
114
|
|
|
115
|
-
```
|
|
116
|
-
|
|
115
|
+
```mermaid
|
|
116
|
+
flowchart LR
|
|
117
|
+
input[Input] --> service[Service]
|
|
118
|
+
service --> domain["Domain capabilities"]
|
|
119
|
+
domain --> collaborators[Collaborators]
|
|
120
|
+
collaborators --> result[Result]
|
|
117
121
|
```
|
|
118
122
|
|
|
119
123
|
This flow keeps orchestration in the application layer and domain meaning in
|
|
120
|
-
the domain layer.
|
|
124
|
+
the domain layer.
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
### Aggregates
|
|
2
2
|
|
|
3
|
-
An aggregate
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
several entities instead of a single entity in isolation.
|
|
3
|
+
An aggregate groups multiple entities into one logical domain unit.
|
|
4
|
+
It is useful when the behavior of a concept depends on the collaboration of
|
|
5
|
+
several entities working together.
|
|
7
6
|
|
|
8
7
|
The generated template provides `Aggregate` as a semantic base class.
|
|
9
8
|
|
|
@@ -15,45 +14,145 @@ The generated template provides `Aggregate` as a semantic base class.
|
|
|
15
14
|
export abstract class Aggregate {}
|
|
16
15
|
```
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
The class is intentionally empty. Its role is to provide the semantic base for
|
|
18
|
+
an aggregate whose concrete implementation gathers the involved entities as a
|
|
19
|
+
key-value structure through its own properties, such as `buyer`, `seller`,
|
|
20
|
+
`order`, or `invoice`. Persistence rules, event publication, and identity
|
|
21
|
+
behavior live in the concrete domain model of the project.
|
|
21
22
|
|
|
22
23
|
#### First Aggregate
|
|
23
24
|
|
|
24
|
-
In the following example we model a
|
|
25
|
+
In the following example we model a sale as one domain unit composed of a
|
|
26
|
+
buyer, a seller, an order, and an invoice generated by the aggregate itself.
|
|
25
27
|
|
|
26
|
-
```ts title="
|
|
28
|
+
```ts title="sales/domain/sale.ts"
|
|
27
29
|
import { Aggregate } from '../../shared/domain/aggregates.js'
|
|
28
30
|
import { Entity } from '../../shared/domain/entities.js'
|
|
29
31
|
|
|
30
|
-
class
|
|
32
|
+
class Person extends Entity {
|
|
33
|
+
public readonly id: string
|
|
34
|
+
public readonly name: string
|
|
35
|
+
|
|
36
|
+
public constructor(id: string, name: string) {
|
|
37
|
+
super()
|
|
38
|
+
this.id = id
|
|
39
|
+
this.name = name
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public equals(other: Entity): boolean {
|
|
43
|
+
return other instanceof Person && this.id === other.id
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class Product extends Entity {
|
|
48
|
+
public readonly id: string
|
|
49
|
+
public readonly name: string
|
|
50
|
+
public readonly price: number
|
|
51
|
+
|
|
52
|
+
public constructor(id: string, name: string, price: number) {
|
|
53
|
+
super()
|
|
54
|
+
this.id = id
|
|
55
|
+
this.name = name
|
|
56
|
+
this.price = price
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public equals(other: Entity): boolean {
|
|
60
|
+
return other instanceof Product && this.id === other.id
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
class Order extends Entity {
|
|
65
|
+
public readonly id: string
|
|
66
|
+
public readonly products: Product[]
|
|
67
|
+
|
|
68
|
+
public constructor(id: string, products: Product[]) {
|
|
69
|
+
super()
|
|
70
|
+
this.id = id
|
|
71
|
+
this.products = products
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public equals(other: Entity): boolean {
|
|
75
|
+
return other instanceof Order && this.id === other.id
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
class Invoice extends Entity {
|
|
80
|
+
public readonly id: string
|
|
81
|
+
public readonly order: Order
|
|
82
|
+
public readonly buyer: Person
|
|
83
|
+
public readonly seller: Person
|
|
84
|
+
public readonly total: number
|
|
85
|
+
|
|
31
86
|
public constructor(
|
|
32
|
-
|
|
33
|
-
|
|
87
|
+
id: string,
|
|
88
|
+
order: Order,
|
|
89
|
+
buyer: Person,
|
|
90
|
+
seller: Person,
|
|
34
91
|
) {
|
|
35
92
|
super()
|
|
93
|
+
this.id = id
|
|
94
|
+
this.order = order
|
|
95
|
+
this.buyer = buyer
|
|
96
|
+
this.seller = seller
|
|
97
|
+
this.total = this.order.products.reduce(
|
|
98
|
+
(sum, product) => sum + product.price,
|
|
99
|
+
0,
|
|
100
|
+
)
|
|
36
101
|
}
|
|
37
102
|
|
|
38
103
|
public equals(other: Entity): boolean {
|
|
39
|
-
return other instanceof
|
|
104
|
+
return other instanceof Invoice && this.id === other.id
|
|
40
105
|
}
|
|
41
106
|
}
|
|
42
107
|
|
|
43
|
-
export class
|
|
44
|
-
public
|
|
108
|
+
export class Sale extends Aggregate {
|
|
109
|
+
public readonly buyer: Person
|
|
110
|
+
public readonly seller: Person
|
|
111
|
+
public readonly order: Order
|
|
112
|
+
protected invoice: Invoice | null
|
|
113
|
+
|
|
114
|
+
public constructor(
|
|
115
|
+
buyer: Person,
|
|
116
|
+
seller: Person,
|
|
117
|
+
order: Order,
|
|
118
|
+
) {
|
|
45
119
|
super()
|
|
120
|
+
this.buyer = buyer
|
|
121
|
+
this.seller = seller
|
|
122
|
+
this.order = order
|
|
123
|
+
this.invoice = null
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
public generateInvoice(): { invoice: Invoice } {
|
|
127
|
+
const id = Math.random().toString()
|
|
128
|
+
const invoice = new Invoice(
|
|
129
|
+
id,
|
|
130
|
+
this.order,
|
|
131
|
+
this.buyer,
|
|
132
|
+
this.seller,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
this.invoice = invoice
|
|
136
|
+
|
|
137
|
+
return { invoice }
|
|
46
138
|
}
|
|
47
139
|
|
|
48
|
-
public
|
|
49
|
-
return this.
|
|
140
|
+
public getInvoice(): Invoice | null {
|
|
141
|
+
return this.invoice
|
|
50
142
|
}
|
|
51
143
|
}
|
|
52
144
|
```
|
|
53
145
|
|
|
54
|
-
`
|
|
55
|
-
|
|
56
|
-
|
|
146
|
+
`Sale` is an aggregate because the rule for generating the invoice depends on
|
|
147
|
+
the collaboration between buyer, seller, and order. In this example, both
|
|
148
|
+
buyer and seller are modeled with the same `Person` entity, while `Order`
|
|
149
|
+
stores the list of purchased products and `Invoice` stores the computed
|
|
150
|
+
`total`. `Invoice` is created inside the aggregate, and its constructor
|
|
151
|
+
calculates the total from the order products when the invoice is generated. The
|
|
152
|
+
`invoice` field stays inside the aggregate lifecycle and `getInvoice()` offers
|
|
153
|
+
controlled access to the generated invoice. When a domain object already
|
|
154
|
+
exists, the example uses that object directly and keeps the collaboration
|
|
155
|
+
between domain parts explicit.
|
|
57
156
|
|
|
58
157
|
#### Responsibility Boundary
|
|
59
158
|
|
|
@@ -66,14 +165,15 @@ Examples include:
|
|
|
66
165
|
2. keeping related entities in a consistent state;
|
|
67
166
|
3. exposing operations that depend on the collaboration of those entities.
|
|
68
167
|
|
|
69
|
-
The generated `Aggregate` base class
|
|
70
|
-
|
|
168
|
+
The generated `Aggregate` base class provides the semantic place where those
|
|
169
|
+
rules belong. The concrete aggregate defines and coordinates the rules that
|
|
170
|
+
keep the domain unit consistent.
|
|
71
171
|
|
|
72
172
|
> **Warning**
|
|
73
|
-
>
|
|
74
|
-
>
|
|
173
|
+
> Extend `Aggregate` when the concept represents one domain unit composed of
|
|
174
|
+
> several related parts and shared rules.
|
|
75
175
|
|
|
76
176
|
#### Next Step
|
|
77
177
|
|
|
78
178
|
Aggregates usually collaborate with entities and value objects. The base entity
|
|
79
|
-
behavior is documented in `entities.md`.
|
|
179
|
+
behavior is documented in `entities.md`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tshex-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.25",
|
|
4
4
|
"author": "https://github.com/virtualitems/",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Typescript Hexagonal Architecture CLI",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"commander": "^12.1.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
+
"@fission-ai/openspec": "^1.6.0",
|
|
48
49
|
"@types/node": "^22.5.4",
|
|
49
50
|
"typescript": "^5.4.5"
|
|
50
51
|
},
|
package/source/main.ts
CHANGED
|
@@ -55,7 +55,13 @@ function executeCreateReactContext(templatesDir: string, contextDir: string) {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
function executeCreateTests(
|
|
58
|
+
function executeCreateTests(
|
|
59
|
+
sourceDir: string,
|
|
60
|
+
destinationDir: string,
|
|
61
|
+
fileContents: string,
|
|
62
|
+
ignoredSourceDir?: string,
|
|
63
|
+
rootSourceDir: string = sourceDir
|
|
64
|
+
) {
|
|
59
65
|
const entries = fs.readdirSync(sourceDir, { withFileTypes: true })
|
|
60
66
|
|
|
61
67
|
if (fs.existsSync(destinationDir) === false) {
|
|
@@ -67,7 +73,7 @@ function executeCreateTests(sourceDir: string, destinationDir: string, fileConte
|
|
|
67
73
|
const destinationPath = path.join(destinationDir, entry.name)
|
|
68
74
|
|
|
69
75
|
if (entry.isDirectory()) {
|
|
70
|
-
if (entry.name === 'shared') {
|
|
76
|
+
if (entry.name === 'shared' && sourceDir === rootSourceDir) {
|
|
71
77
|
continue
|
|
72
78
|
}
|
|
73
79
|
|
|
@@ -79,7 +85,7 @@ function executeCreateTests(sourceDir: string, destinationDir: string, fileConte
|
|
|
79
85
|
continue
|
|
80
86
|
}
|
|
81
87
|
|
|
82
|
-
executeCreateTests(sourcePath, destinationPath, fileContents, ignoredSourceDir)
|
|
88
|
+
executeCreateTests(sourcePath, destinationPath, fileContents, ignoredSourceDir, rootSourceDir)
|
|
83
89
|
continue
|
|
84
90
|
}
|
|
85
91
|
|