tshex-cli 1.0.20 → 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 +14 -14
- 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 +8 -3
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
### Data
|
|
2
|
+
|
|
3
|
+
The data contracts define how the application layer interacts with plain source
|
|
4
|
+
records.
|
|
5
|
+
They separate connection management, raw record access, and domain
|
|
6
|
+
transformation so that a context can change drivers without rewriting its use
|
|
7
|
+
cases.
|
|
8
|
+
|
|
9
|
+
The generated structure splits this concern into three files:
|
|
10
|
+
|
|
11
|
+
1. `drivers.ts` for connection adapters;
|
|
12
|
+
2. `managers.ts` for plain-record operations;
|
|
13
|
+
3. `repositories.ts` for record-to-domain transformation.
|
|
14
|
+
|
|
15
|
+
#### Driver Adapter
|
|
16
|
+
|
|
17
|
+
`DriverAdapter` is responsible for connecting to a data source and returning an
|
|
18
|
+
enabled `DataManager`.
|
|
19
|
+
|
|
20
|
+
```ts title="shared/application/data/drivers.ts"
|
|
21
|
+
import { DataManager } from './managers.js'
|
|
22
|
+
|
|
23
|
+
export abstract class DriverAdapter<M extends DataManager = DataManager> {
|
|
24
|
+
public abstract connect(...args: unknown[]): Promise<M>
|
|
25
|
+
|
|
26
|
+
public abstract disconnect(): Promise<unknown>
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The `connect()` method returns a manager that can read or manipulate raw data.
|
|
31
|
+
The `disconnect()` method closes the interaction when the work is finished.
|
|
32
|
+
|
|
33
|
+
#### Data Manager
|
|
34
|
+
|
|
35
|
+
`DataManager` is responsible for exposing plain source data.
|
|
36
|
+
|
|
37
|
+
```ts title="shared/application/data/managers.ts"
|
|
38
|
+
export abstract class DataManager<T = Record<string, unknown>> {
|
|
39
|
+
public none(): Array<T> {
|
|
40
|
+
return []
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public abstract all(): Promise<Array<T>>
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The base class provides `none()` as an explicit empty result and requires
|
|
48
|
+
`all()` for retrieving records. The generated template also includes operation
|
|
49
|
+
contracts such as `Filterable`, `Creatable`, and `Updatable`, plus the
|
|
50
|
+
`DatasetManager` extension for set operations.
|
|
51
|
+
|
|
52
|
+
#### First Implementation
|
|
53
|
+
|
|
54
|
+
In the following example we implement an in-memory manager and its driver.
|
|
55
|
+
|
|
56
|
+
```ts title="users/adapters/memory-users-driver.ts"
|
|
57
|
+
import { DriverAdapter } from '../../shared/application/data/drivers.js'
|
|
58
|
+
import { DataManager } from '../../shared/application/data/managers.js'
|
|
59
|
+
|
|
60
|
+
type UserRecord = {
|
|
61
|
+
id: string
|
|
62
|
+
email: string
|
|
63
|
+
active: boolean | null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
class MemoryUsersManager extends DataManager<UserRecord> {
|
|
67
|
+
public constructor(private readonly rows: Array<UserRecord>) {
|
|
68
|
+
super()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public async all(): Promise<Array<UserRecord>> {
|
|
72
|
+
return this.rows
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class MemoryUsersDriver extends DriverAdapter<MemoryUsersManager> {
|
|
77
|
+
public constructor(private readonly rows: Array<UserRecord>) {
|
|
78
|
+
super()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
public async connect(): Promise<MemoryUsersManager> {
|
|
82
|
+
return new MemoryUsersManager(this.rows)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
public async disconnect(): Promise<void> {
|
|
86
|
+
return undefined
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`MemoryUsersDriver` owns the connection contract. `MemoryUsersManager` owns the
|
|
92
|
+
raw records. The application layer can use both without knowing whether the
|
|
93
|
+
source is memory, SQL, or an HTTP-backed adapter.
|
|
94
|
+
|
|
95
|
+
#### Repository
|
|
96
|
+
|
|
97
|
+
`Repository` is responsible for transforming raw records into domain-oriented
|
|
98
|
+
representations.
|
|
99
|
+
|
|
100
|
+
```ts title="shared/application/data/repositories.ts"
|
|
101
|
+
import { type DataManager } from './managers.js'
|
|
102
|
+
import { type DriverAdapter } from './drivers.js'
|
|
103
|
+
|
|
104
|
+
export abstract class Repository<
|
|
105
|
+
DataShape extends Record<string, unknown> = Record<string, unknown>,
|
|
106
|
+
EntityShape extends Record<string, unknown> = Record<string, unknown>
|
|
107
|
+
> {
|
|
108
|
+
public constructor(public readonly driver: DriverAdapter<DataManager<DataShape>>) {}
|
|
109
|
+
|
|
110
|
+
public async all(): Promise<Array<EntityShape>> {
|
|
111
|
+
const connection = await this.driver.connect()
|
|
112
|
+
const raw = await connection.all()
|
|
113
|
+
const entities = this.transformList(raw)
|
|
114
|
+
await this.driver.disconnect()
|
|
115
|
+
return entities
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
protected transformList(data: Array<DataShape>): Array<EntityShape> {
|
|
119
|
+
return data.map(this.transform)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
protected abstract transform(data: DataShape): EntityShape
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The base repository already defines the `all()` flow. A concrete repository only
|
|
127
|
+
needs to implement `transform()`.
|
|
128
|
+
|
|
129
|
+
#### Repository Implementation
|
|
130
|
+
|
|
131
|
+
Now that the driver exists, a repository can translate raw records into a shape
|
|
132
|
+
that the rest of the context can use.
|
|
133
|
+
|
|
134
|
+
```ts title="users/adapters/users-repository.ts"
|
|
135
|
+
import { Repository } from '../../shared/application/data/repositories.js'
|
|
136
|
+
import { DriverAdapter } from '../../shared/application/data/drivers.js'
|
|
137
|
+
import { DataManager } from '../../shared/application/data/managers.js'
|
|
138
|
+
|
|
139
|
+
type UserRecord = {
|
|
140
|
+
id: string
|
|
141
|
+
email: string
|
|
142
|
+
active: boolean | null
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
type UserView = {
|
|
146
|
+
id: string
|
|
147
|
+
email: string
|
|
148
|
+
active: boolean | null
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export class UsersRepository extends Repository<UserRecord, UserView> {
|
|
152
|
+
public constructor(driver: DriverAdapter<DataManager<UserRecord>>) {
|
|
153
|
+
super(driver)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
protected transform(data: UserRecord): UserView {
|
|
157
|
+
return {
|
|
158
|
+
id: data.id,
|
|
159
|
+
email: data.email,
|
|
160
|
+
active: data.active,
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
This repository does not own the connection lifecycle because `Repository`
|
|
167
|
+
already handles it. Its responsibility is the mapping between raw source data
|
|
168
|
+
and the representation used by the context.
|
|
169
|
+
|
|
170
|
+
> **Warning**
|
|
171
|
+
> The generated `Repository` only implements `all()`. If the project needs
|
|
172
|
+
> filtering, creation, or updates, add those operations explicitly instead of
|
|
173
|
+
> assuming they already exist in the base class.
|
|
174
|
+
|
|
175
|
+
#### Example Flow
|
|
176
|
+
|
|
177
|
+
The normal flow of the data abstractions is the following:
|
|
178
|
+
|
|
179
|
+
```text
|
|
180
|
+
service -> repository -> driver -> data manager -> raw records
|
|
181
|
+
service <- repository <- transformed records
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
This separation keeps the application service focused on orchestration while
|
|
185
|
+
the repository focuses on transformation.
|
|
@@ -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.
|