tshex-cli 1.0.19 → 1.0.21
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/README.md +19 -1090
- package/docs/context-ports.md +165 -0
- package/docs/generated-file-reference.md +88 -0
- package/docs/library-structure.md +163 -0
- package/docs/library-types.md +70 -0
- package/docs/shared/application/data.md +185 -0
- package/docs/shared/application/events.md +144 -0
- package/docs/shared/application/http.md +131 -0
- package/docs/shared/application/loggers.md +127 -0
- package/docs/shared/application/services.md +120 -0
- package/docs/shared/application/validations.md +94 -0
- package/docs/shared/domain/aggregates.md +79 -0
- package/docs/shared/domain/entities.md +91 -0
- package/docs/shared/domain/errors.md +103 -0
- package/docs/shared/domain/value-objects.md +148 -0
- package/package.json +9 -3
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
### Events
|
|
2
|
+
|
|
3
|
+
Events represent something that occurred in the application.
|
|
4
|
+
They are used to make side effects explicit and to separate the main process
|
|
5
|
+
from the reactions that can happen after that process completes.
|
|
6
|
+
|
|
7
|
+
The generated template defines three contracts: `Event`, `EventHandler`, and
|
|
8
|
+
`EventDispatcher`.
|
|
9
|
+
|
|
10
|
+
#### Event
|
|
11
|
+
|
|
12
|
+
`Event` is responsible for carrying the event time and its plain details.
|
|
13
|
+
|
|
14
|
+
```ts title="shared/application/events.ts"
|
|
15
|
+
export abstract class Event {
|
|
16
|
+
public constructor(
|
|
17
|
+
public readonly timestamp: number = Date.now(),
|
|
18
|
+
public readonly details: Record<string, unknown> = {},
|
|
19
|
+
) {}
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Every event instance includes a timestamp and a `details` object. Concrete
|
|
24
|
+
events can extend this base class directly when the generic `details` payload is
|
|
25
|
+
enough for the process.
|
|
26
|
+
|
|
27
|
+
#### First Event
|
|
28
|
+
|
|
29
|
+
In the following example we define an event for user registration.
|
|
30
|
+
|
|
31
|
+
```ts title="users/application/user-registered.ts"
|
|
32
|
+
import { Event } from '../../shared/application/events.js'
|
|
33
|
+
|
|
34
|
+
export class UserRegisteredEvent extends Event {}
|
|
35
|
+
|
|
36
|
+
const event = new UserRegisteredEvent(Date.now(), {
|
|
37
|
+
userId: 'usr_1',
|
|
38
|
+
email: 'ada@example.com',
|
|
39
|
+
})
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`UserRegisteredEvent` does not need additional code because the generated base
|
|
43
|
+
class already stores the time and the details.
|
|
44
|
+
|
|
45
|
+
#### Event Handler
|
|
46
|
+
|
|
47
|
+
`EventHandler` is responsible for reacting to an event.
|
|
48
|
+
|
|
49
|
+
```ts title="shared/application/events.ts"
|
|
50
|
+
export abstract class EventHandler {
|
|
51
|
+
public abstract handle(event: Event): Promise<void>
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Now that the event exists, a handler can implement the required `handle()`
|
|
56
|
+
method.
|
|
57
|
+
|
|
58
|
+
```ts title="users/adapters/send-welcome-email.ts"
|
|
59
|
+
import { Event, EventHandler } from '../../shared/application/events.js'
|
|
60
|
+
|
|
61
|
+
export class SendWelcomeEmailHandler extends EventHandler {
|
|
62
|
+
public async handle(event: Event): Promise<void> {
|
|
63
|
+
const email = String(event.details.email)
|
|
64
|
+
void email
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
This example keeps the reaction minimal. In a real adapter, the handler would
|
|
70
|
+
call an email provider or another external system.
|
|
71
|
+
|
|
72
|
+
#### Event Dispatcher
|
|
73
|
+
|
|
74
|
+
`EventDispatcher` is responsible for subscription management and event
|
|
75
|
+
publication.
|
|
76
|
+
|
|
77
|
+
```ts title="shared/application/events.ts"
|
|
78
|
+
export abstract class EventDispatcher {
|
|
79
|
+
public abstract subscribe(key: unknown, handler: EventHandler): void
|
|
80
|
+
|
|
81
|
+
public abstract unsubscribe(key: unknown, handler: EventHandler): void
|
|
82
|
+
|
|
83
|
+
public abstract dispatch(event: Event): void
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
In the following example we use an in-memory dispatcher.
|
|
88
|
+
|
|
89
|
+
```ts title="users/adapters/in-memory-dispatcher.ts"
|
|
90
|
+
import {
|
|
91
|
+
Event,
|
|
92
|
+
EventDispatcher,
|
|
93
|
+
EventHandler,
|
|
94
|
+
} from '../../shared/application/events.js'
|
|
95
|
+
|
|
96
|
+
export class InMemoryDispatcher extends EventDispatcher {
|
|
97
|
+
private readonly handlers = new Map<string, Array<EventHandler>>()
|
|
98
|
+
|
|
99
|
+
public subscribe(key: unknown, handler: EventHandler): void {
|
|
100
|
+
const normalizedKey = String(key)
|
|
101
|
+
const existing = this.handlers.get(normalizedKey) ?? []
|
|
102
|
+
this.handlers.set(normalizedKey, [...existing, handler])
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public unsubscribe(key: unknown, handler: EventHandler): void {
|
|
106
|
+
const normalizedKey = String(key)
|
|
107
|
+
const existing = this.handlers.get(normalizedKey) ?? []
|
|
108
|
+
this.handlers.set(
|
|
109
|
+
normalizedKey,
|
|
110
|
+
existing.filter((current) => current !== handler),
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
public dispatch(event: Event): void {
|
|
115
|
+
const key = event.constructor.name
|
|
116
|
+
const handlers = this.handlers.get(key) ?? []
|
|
117
|
+
|
|
118
|
+
for (const handler of handlers) {
|
|
119
|
+
void handler.handle(event)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The dispatcher keeps the subscription mechanics outside the application service.
|
|
126
|
+
This allows the service to publish an event without knowing how reactions are
|
|
127
|
+
registered or executed.
|
|
128
|
+
|
|
129
|
+
> **Warning**
|
|
130
|
+
> The generated dispatcher contract does not define durability, retries, or
|
|
131
|
+
> ordering guarantees. If the application needs those guarantees, document and
|
|
132
|
+
> implement them in the concrete adapter.
|
|
133
|
+
|
|
134
|
+
#### Example Flow
|
|
135
|
+
|
|
136
|
+
The normal flow is the following:
|
|
137
|
+
|
|
138
|
+
```text
|
|
139
|
+
service -> dispatch(event)
|
|
140
|
+
dispatcher -> matching handlers
|
|
141
|
+
handlers -> side effects
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
This keeps the main process separate from secondary reactions.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
### HTTP
|
|
2
|
+
|
|
3
|
+
The HTTP contracts define a transport-facing boundary without coupling the
|
|
4
|
+
generated structure to a specific framework.
|
|
5
|
+
They are used when an adapter needs to describe requests, responses, handlers,
|
|
6
|
+
or middleware in a consistent way.
|
|
7
|
+
|
|
8
|
+
The generated template keeps `HttpRequest` and `HttpResponse` empty on purpose.
|
|
9
|
+
Each project can extend them with the fields required by its own transport.
|
|
10
|
+
|
|
11
|
+
#### Response Body
|
|
12
|
+
|
|
13
|
+
`HttpResponseBody` is responsible for standardizing the shape of the response
|
|
14
|
+
payload.
|
|
15
|
+
|
|
16
|
+
```ts title="shared/application/http.ts"
|
|
17
|
+
export interface HttpResponseBody {
|
|
18
|
+
readonly data: Record<string, unknown> | null
|
|
19
|
+
readonly errors: string[] | null
|
|
20
|
+
readonly links: Record<string, URL> | null
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
This structure makes successful data, error messages, and related links
|
|
25
|
+
explicit without forcing a specific router or server implementation.
|
|
26
|
+
|
|
27
|
+
#### Request Handler
|
|
28
|
+
|
|
29
|
+
`HttpRequestHandler` is responsible for processing a request and returning a
|
|
30
|
+
response.
|
|
31
|
+
|
|
32
|
+
```ts title="shared/application/http.ts"
|
|
33
|
+
export interface HttpRequestHandler {
|
|
34
|
+
handle(request: HttpRequest): HttpResponse | Promise<HttpResponse>
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
In the following example we define adapter-specific request and response types,
|
|
39
|
+
then implement a handler.
|
|
40
|
+
|
|
41
|
+
```ts title="users/adapters/get-user-handler.ts"
|
|
42
|
+
import {
|
|
43
|
+
HttpRequest,
|
|
44
|
+
HttpRequestHandler,
|
|
45
|
+
HttpResponse,
|
|
46
|
+
HttpResponseBody,
|
|
47
|
+
} from '../../shared/application/http.js'
|
|
48
|
+
|
|
49
|
+
interface UserHttpRequest extends HttpRequest {
|
|
50
|
+
readonly params: {
|
|
51
|
+
id: string
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface UserHttpResponse extends HttpResponse {
|
|
56
|
+
readonly status: number
|
|
57
|
+
readonly body: HttpResponseBody
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class GetUserHandler implements HttpRequestHandler {
|
|
61
|
+
public handle(request: HttpRequest): HttpResponse {
|
|
62
|
+
const typedRequest = request as UserHttpRequest
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
status: 200,
|
|
66
|
+
body: {
|
|
67
|
+
data: {
|
|
68
|
+
id: typedRequest.params.id,
|
|
69
|
+
},
|
|
70
|
+
errors: null,
|
|
71
|
+
links: null,
|
|
72
|
+
},
|
|
73
|
+
} as UserHttpResponse
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The generated `HttpRequest` and `HttpResponse` interfaces stay empty, so the
|
|
79
|
+
adapter declares the transport-specific fields locally. This keeps the shared
|
|
80
|
+
contract small and portable.
|
|
81
|
+
|
|
82
|
+
#### Middleware
|
|
83
|
+
|
|
84
|
+
`HttpMiddleware` is responsible for running logic before or around the handler.
|
|
85
|
+
|
|
86
|
+
```ts title="shared/application/http.ts"
|
|
87
|
+
export interface HttpMiddleware {
|
|
88
|
+
process(
|
|
89
|
+
request: HttpRequest,
|
|
90
|
+
handler: HttpRequestHandler,
|
|
91
|
+
): HttpResponse | Promise<HttpResponse>
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Now that the handler exists, middleware can wrap it.
|
|
96
|
+
|
|
97
|
+
```ts title="users/adapters/request-logger.ts"
|
|
98
|
+
import {
|
|
99
|
+
HttpMiddleware,
|
|
100
|
+
HttpRequest,
|
|
101
|
+
HttpRequestHandler,
|
|
102
|
+
HttpResponse,
|
|
103
|
+
} from '../../shared/application/http.js'
|
|
104
|
+
|
|
105
|
+
export class RequestLoggerMiddleware implements HttpMiddleware {
|
|
106
|
+
public async process(
|
|
107
|
+
request: HttpRequest,
|
|
108
|
+
handler: HttpRequestHandler,
|
|
109
|
+
): Promise<HttpResponse> {
|
|
110
|
+
void request
|
|
111
|
+
return handler.handle(request)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
This middleware does not mutate the request or response. It only shows where
|
|
117
|
+
cross-cutting behavior belongs in the generated HTTP abstraction.
|
|
118
|
+
|
|
119
|
+
> **Warning**
|
|
120
|
+
> Do not treat the shared HTTP contracts as a full framework abstraction. They
|
|
121
|
+
> only define the minimum boundary for adapters. Routing, serialization, and
|
|
122
|
+
> status code policies remain the responsibility of the concrete transport.
|
|
123
|
+
|
|
124
|
+
#### Example Flow
|
|
125
|
+
|
|
126
|
+
```text
|
|
127
|
+
request -> middleware -> handler -> response body
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
This flow keeps the transport boundary explicit while leaving framework choices
|
|
131
|
+
to the adapter layer.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
### Loggers
|
|
2
|
+
|
|
3
|
+
The logging contracts define how the application layer emits operational
|
|
4
|
+
information.
|
|
5
|
+
They are used to keep services independent from a concrete logging library or
|
|
6
|
+
transport.
|
|
7
|
+
|
|
8
|
+
The generated template provides shared level constants and the abstract
|
|
9
|
+
`Logger` contract.
|
|
10
|
+
|
|
11
|
+
#### Log Levels
|
|
12
|
+
|
|
13
|
+
The following constants describe the generated severity scale.
|
|
14
|
+
|
|
15
|
+
| Constant | Value |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| `DEBUG` | `10` |
|
|
18
|
+
| `INFO` | `20` |
|
|
19
|
+
| `WARNING` | `30` |
|
|
20
|
+
| `ERROR` | `40` |
|
|
21
|
+
| `CRITICAL` | `50` |
|
|
22
|
+
|
|
23
|
+
These values give the project a shared vocabulary for severity without forcing
|
|
24
|
+
any adapter to use a particular logger implementation.
|
|
25
|
+
|
|
26
|
+
#### Logger
|
|
27
|
+
|
|
28
|
+
`Logger` is responsible for receiving log data from the application layer.
|
|
29
|
+
|
|
30
|
+
```ts title="shared/application/loggers.ts"
|
|
31
|
+
export abstract class Logger {
|
|
32
|
+
public abstract debug(data: unknown): void
|
|
33
|
+
|
|
34
|
+
public abstract info(data: unknown): void
|
|
35
|
+
|
|
36
|
+
public abstract warning(data: unknown): void
|
|
37
|
+
|
|
38
|
+
public abstract error(data: unknown): void
|
|
39
|
+
|
|
40
|
+
public abstract critical(data: unknown): void
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The contract is intentionally small. It defines the actions the application can
|
|
45
|
+
request, while the adapter decides how those actions are persisted or displayed.
|
|
46
|
+
|
|
47
|
+
#### First Adapter
|
|
48
|
+
|
|
49
|
+
In the following example we implement a console-based logger.
|
|
50
|
+
|
|
51
|
+
```ts title="users/adapters/console-logger.ts"
|
|
52
|
+
import { Logger } from '../../shared/application/loggers.js'
|
|
53
|
+
|
|
54
|
+
type ExternalService = {
|
|
55
|
+
debug(data: unknown): void
|
|
56
|
+
info(data: unknown): void
|
|
57
|
+
warning(data: unknown): void
|
|
58
|
+
error(data: unknown): void
|
|
59
|
+
critical(data: unknown): void
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export class ConsoleLogger extends Logger {
|
|
63
|
+
constructor(protected readonly externalService: ExternalService) {
|
|
64
|
+
super()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public debug(data: unknown): void {
|
|
68
|
+
this.externalService.debug(data)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public info(data: unknown): void {
|
|
72
|
+
this.externalService.info(data)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public warning(data: unknown): void {
|
|
76
|
+
this.externalService.warning(data)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public error(data: unknown): void {
|
|
80
|
+
this.externalService.error(data)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public critical(data: unknown): void {
|
|
84
|
+
this.externalService.critical(data)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
This adapter satisfies the generated contract without changing the application
|
|
90
|
+
layer.
|
|
91
|
+
|
|
92
|
+
#### Service Integration
|
|
93
|
+
|
|
94
|
+
Now that the logger exists, an application service can depend on the contract.
|
|
95
|
+
|
|
96
|
+
```ts title="users/application/register-user.ts"
|
|
97
|
+
import { Service } from '../../shared/application/services.js'
|
|
98
|
+
import { Logger } from '../../shared/application/loggers.js'
|
|
99
|
+
|
|
100
|
+
export class RegisterUser extends Service {
|
|
101
|
+
public constructor(private readonly logger: Logger) {
|
|
102
|
+
super()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public async execute(email: string): Promise<void> {
|
|
106
|
+
this.logger.info({
|
|
107
|
+
message: 'Registering user',
|
|
108
|
+
email,
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The service does not know whether the logger writes to the console, a file, or
|
|
115
|
+
an external platform. It only depends on the application-level contract.
|
|
116
|
+
|
|
117
|
+
> **Hint**
|
|
118
|
+
> Pass structured objects when the project needs machine-readable logs. The
|
|
119
|
+
> generated contract accepts `unknown`, so the adapter can enforce its own shape.
|
|
120
|
+
|
|
121
|
+
#### Example Flow
|
|
122
|
+
|
|
123
|
+
```text
|
|
124
|
+
service -> Logger contract -> adapter -> logging backend
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
This flow keeps observability concerns outside the core process.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
### Services
|
|
2
|
+
|
|
3
|
+
An application service is responsible for coordinating domain capabilities and
|
|
4
|
+
collaborators to fulfill a system purpose.
|
|
5
|
+
It defines the process of a use case, not the business meaning of the domain
|
|
6
|
+
objects involved in that use case.
|
|
7
|
+
|
|
8
|
+
The generated template provides `Service` as a semantic base class for these
|
|
9
|
+
processes.
|
|
10
|
+
|
|
11
|
+
#### Base Class
|
|
12
|
+
|
|
13
|
+
`Service` is an abstract class with no concrete behavior.
|
|
14
|
+
|
|
15
|
+
```ts title="shared/application/services.ts"
|
|
16
|
+
export abstract class Service {}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
This design is intentional. The generated class marks the role of the object
|
|
20
|
+
without imposing an `execute()` method, a result shape, or a framework-specific
|
|
21
|
+
lifecycle.
|
|
22
|
+
|
|
23
|
+
#### First Service
|
|
24
|
+
|
|
25
|
+
In the following example we build a small registration process.
|
|
26
|
+
|
|
27
|
+
```ts title="users/application/register-user.ts"
|
|
28
|
+
import { Service } from '../../shared/application/services.js'
|
|
29
|
+
import { Email, NullableBoolean } from '../../shared/domain/value-objects.js'
|
|
30
|
+
|
|
31
|
+
type UserRecord = {
|
|
32
|
+
id: string
|
|
33
|
+
email: string
|
|
34
|
+
active: boolean | null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type SaveUser = {
|
|
38
|
+
save(user: UserRecord): Promise<UserRecord>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class RegisterUser extends Service {
|
|
42
|
+
public constructor(private readonly repository: SaveUser) {
|
|
43
|
+
super()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public async execute(data: {
|
|
47
|
+
id: string
|
|
48
|
+
email: string
|
|
49
|
+
active: boolean | null
|
|
50
|
+
}): Promise<UserRecord> {
|
|
51
|
+
const email = Email.from(data.email)
|
|
52
|
+
const active = NullableBoolean.from(data.active)
|
|
53
|
+
|
|
54
|
+
return this.repository.save({
|
|
55
|
+
id: data.id,
|
|
56
|
+
email: email.value,
|
|
57
|
+
active: active.value,
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The service coordinates the process. It validates the email, normalizes the
|
|
64
|
+
tri-state boolean, and delegates persistence to a collaborator.
|
|
65
|
+
|
|
66
|
+
The service does not own the email validation rule or the repository transport.
|
|
67
|
+
Those concerns remain in the value object and the adapter-facing collaborator.
|
|
68
|
+
|
|
69
|
+
#### Validation Failures
|
|
70
|
+
|
|
71
|
+
Now consider a process that wants to return an explicit result when the input
|
|
72
|
+
cannot be converted into domain objects.
|
|
73
|
+
|
|
74
|
+
```ts title="users/application/register-user.ts"
|
|
75
|
+
import { Service } from '../../shared/application/services.js'
|
|
76
|
+
import { Email } from '../../shared/domain/value-objects.js'
|
|
77
|
+
import { ValueError } from '../../shared/domain/errors.js'
|
|
78
|
+
|
|
79
|
+
type RegistrationResult =
|
|
80
|
+
| { ok: true; email: string }
|
|
81
|
+
| { ok: false; error: string }
|
|
82
|
+
|
|
83
|
+
export class RegisterUserSafely extends Service {
|
|
84
|
+
public execute(email: string): RegistrationResult {
|
|
85
|
+
try {
|
|
86
|
+
return {
|
|
87
|
+
ok: true,
|
|
88
|
+
email: Email.from(email).value,
|
|
89
|
+
}
|
|
90
|
+
} catch (error: unknown) {
|
|
91
|
+
if (error instanceof ValueError) {
|
|
92
|
+
return {
|
|
93
|
+
ok: false,
|
|
94
|
+
error: error.message,
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
throw error
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`Email.from()` throws `ValueError` when the input is invalid. The service can
|
|
105
|
+
either allow that error to propagate or translate it into an application-level
|
|
106
|
+
result, depending on the requirements of the use case.
|
|
107
|
+
|
|
108
|
+
> **Warning**
|
|
109
|
+
> `Service` does not guarantee a method name or a result contract. If the codebase
|
|
110
|
+
> needs those conventions, establish them explicitly in project code instead of
|
|
111
|
+
> assuming the generated base class already provides them.
|
|
112
|
+
|
|
113
|
+
#### Example Flow
|
|
114
|
+
|
|
115
|
+
```text
|
|
116
|
+
input -> service -> domain capabilities -> collaborators -> result
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
This flow keeps orchestration in the application layer and domain meaning in
|
|
120
|
+
the domain layer.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
### Validations
|
|
2
|
+
|
|
3
|
+
Validation objects are responsible for checking whether application input is
|
|
4
|
+
ready for the next step of a process.
|
|
5
|
+
They are used to keep validation explicit before the application constructs
|
|
6
|
+
domain objects or invokes external collaborators.
|
|
7
|
+
|
|
8
|
+
The generated template provides a single contract for this purpose:
|
|
9
|
+
`Validatable`.
|
|
10
|
+
|
|
11
|
+
#### Contract
|
|
12
|
+
|
|
13
|
+
`Validatable` is responsible for exposing a Boolean validation result.
|
|
14
|
+
|
|
15
|
+
```ts title="shared/application/validations.ts"
|
|
16
|
+
export interface Validatable {
|
|
17
|
+
isValid(): boolean
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
This contract stays deliberately small. It does not prescribe how errors are
|
|
22
|
+
stored, how messages are formatted, or whether validation is synchronous or
|
|
23
|
+
composed from several objects.
|
|
24
|
+
|
|
25
|
+
#### First Validation Object
|
|
26
|
+
|
|
27
|
+
In the following example we validate a registration payload before the service
|
|
28
|
+
turns it into domain values.
|
|
29
|
+
|
|
30
|
+
```ts title="users/application/create-user-input.ts"
|
|
31
|
+
import { Validatable } from '../../shared/application/validations.js'
|
|
32
|
+
import { Email } from '../../shared/domain/value-objects.js'
|
|
33
|
+
|
|
34
|
+
export class CreateUserInput implements Validatable {
|
|
35
|
+
public constructor(
|
|
36
|
+
public readonly id: string,
|
|
37
|
+
public readonly email: string,
|
|
38
|
+
) {}
|
|
39
|
+
|
|
40
|
+
public isValid(): boolean {
|
|
41
|
+
return this.id.length > 0 && Email.isValid(this.email)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`CreateUserInput` performs application-level checks without constructing an
|
|
47
|
+
`Email` instance yet. This is useful when the process wants to reject invalid
|
|
48
|
+
input before attempting a full domain conversion.
|
|
49
|
+
|
|
50
|
+
#### Integration With A Service
|
|
51
|
+
|
|
52
|
+
Now that the validation object exists, a service can decide what to do when the
|
|
53
|
+
input does not satisfy the contract.
|
|
54
|
+
|
|
55
|
+
```ts title="users/application/register-user.ts"
|
|
56
|
+
import { Service } from '../../shared/application/services.js'
|
|
57
|
+
import { Validatable } from '../../shared/application/validations.js'
|
|
58
|
+
|
|
59
|
+
type CreateUserInput = Validatable & {
|
|
60
|
+
readonly id: string
|
|
61
|
+
readonly email: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type RegistrationResult =
|
|
65
|
+
| { ok: true }
|
|
66
|
+
| { ok: false; errors: string[] }
|
|
67
|
+
|
|
68
|
+
export class RegisterUser extends Service {
|
|
69
|
+
public execute(input: CreateUserInput): RegistrationResult {
|
|
70
|
+
if (input.isValid() === false) {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
errors: ['The registration input is invalid.'],
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
ok: true,
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The service decides the process outcome, while the validation object owns the
|
|
85
|
+
question of whether the input is acceptable.
|
|
86
|
+
|
|
87
|
+
> **Hint**
|
|
88
|
+
> `Validatable` does not replace domain rules. Use it for application-level
|
|
89
|
+
> checks. Keep domain invariants inside value objects, entities, or aggregates.
|
|
90
|
+
|
|
91
|
+
#### Next Step
|
|
92
|
+
|
|
93
|
+
After validation succeeds, the next step is usually to construct domain objects
|
|
94
|
+
or call a repository through an application service.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
### Aggregates
|
|
2
|
+
|
|
3
|
+
An aggregate is responsible for grouping multiple entities into one logical
|
|
4
|
+
domain unit.
|
|
5
|
+
It is used when the behavior of the concept depends on the collaboration of
|
|
6
|
+
several entities instead of a single entity in isolation.
|
|
7
|
+
|
|
8
|
+
The generated template provides `Aggregate` as a semantic base class.
|
|
9
|
+
|
|
10
|
+
#### Base Class
|
|
11
|
+
|
|
12
|
+
`Aggregate` is an abstract class with no concrete methods.
|
|
13
|
+
|
|
14
|
+
```ts title="shared/domain/aggregates.ts"
|
|
15
|
+
export abstract class Aggregate {}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
This means the generated base class does not impose persistence rules, event
|
|
19
|
+
publication, or identity behavior. Those concerns remain in the concrete domain
|
|
20
|
+
model of the project.
|
|
21
|
+
|
|
22
|
+
#### First Aggregate
|
|
23
|
+
|
|
24
|
+
In the following example we model a shopping cart as a group of line items.
|
|
25
|
+
|
|
26
|
+
```ts title="users/domain/cart.ts"
|
|
27
|
+
import { Aggregate } from '../../shared/domain/aggregates.js'
|
|
28
|
+
import { Entity } from '../../shared/domain/entities.js'
|
|
29
|
+
|
|
30
|
+
class LineItem extends Entity {
|
|
31
|
+
public constructor(
|
|
32
|
+
public readonly id: string,
|
|
33
|
+
public readonly quantity: number,
|
|
34
|
+
) {
|
|
35
|
+
super()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public equals(other: Entity): boolean {
|
|
39
|
+
return other instanceof LineItem && this.id === other.id
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class Cart extends Aggregate {
|
|
44
|
+
public constructor(public readonly items: Array<LineItem>) {
|
|
45
|
+
super()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public totalItems(): number {
|
|
49
|
+
return this.items.reduce((sum, item) => sum + item.quantity, 0)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`Cart` is an aggregate because its behavior depends on the collection of
|
|
55
|
+
entities that compose it. `LineItem` remains an entity because its identity is
|
|
56
|
+
independent inside the aggregate.
|
|
57
|
+
|
|
58
|
+
#### Responsibility Boundary
|
|
59
|
+
|
|
60
|
+
An aggregate should own rules that require several internal parts to work
|
|
61
|
+
together.
|
|
62
|
+
|
|
63
|
+
Examples include:
|
|
64
|
+
|
|
65
|
+
1. checking whether the aggregate can change status;
|
|
66
|
+
2. keeping related entities in a consistent state;
|
|
67
|
+
3. exposing operations that depend on the collaboration of those entities.
|
|
68
|
+
|
|
69
|
+
The generated `Aggregate` base class does not implement these rules for you. It
|
|
70
|
+
only provides the semantic place where those rules belong.
|
|
71
|
+
|
|
72
|
+
> **Warning**
|
|
73
|
+
> Do not extend `Aggregate` only to create a container for unrelated values. Use
|
|
74
|
+
> it when the concept represents one domain unit composed of several parts.
|
|
75
|
+
|
|
76
|
+
#### Next Step
|
|
77
|
+
|
|
78
|
+
Aggregates usually collaborate with entities and value objects. The base entity
|
|
79
|
+
behavior is documented in `entities.md`.
|