tshex-cli 1.0.24 → 1.0.26
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 +47 -100
- package/docs/generated-file-reference.md +41 -11
- package/docs/library-structure.md +42 -18
- package/docs/library-types.md +45 -3
- package/docs/shared/application/http.md +216 -67
- package/docs/shared/application/loggers.md +48 -24
- package/docs/shared/domain/aggregates.md +125 -25
- package/package.json +2 -1
- package/source/main.ts +9 -3
- package/templates/lib/shared/application/http/http.ts +76 -0
- package/templates/lib/shared/application/http/json-api.ts +611 -0
- package/templates/lib/shared/application/http/json-web-token.ts +980 -0
- package/templates/lib/shared/application/http/opengraph.ts +533 -0
- package/templates/lib/shared/application/loggers.ts +25 -0
- package/templates/lib/types/cldr.d.ts +770 -0
- package/templates/lib/types/iana.d.ts +423 -0
- package/templates/lib/types/json.d.ts +9 -0
- package/templates/lib/types/objects.d.ts +1 -0
- package/templates/lib/index.d.ts +0 -1
- package/templates/lib/shared/application/http.ts +0 -35
|
@@ -1,94 +1,64 @@
|
|
|
1
1
|
### HTTP
|
|
2
2
|
|
|
3
|
-
The HTTP contracts define a transport-facing boundary
|
|
4
|
-
generated structure to a specific framework.
|
|
3
|
+
The HTTP contracts define a framework-agnostic, transport-facing boundary.
|
|
5
4
|
They are used when an adapter needs to describe requests, responses, handlers,
|
|
6
5
|
or middleware in a consistent way.
|
|
7
6
|
|
|
8
|
-
The generated template
|
|
9
|
-
|
|
7
|
+
The generated template relies on the standard `Request` and `Response` types
|
|
8
|
+
from the Fetch API, so adapters work directly with the platform's own APIs.
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
The concern is split across four files under `shared/application/http/`:
|
|
12
11
|
|
|
13
|
-
`
|
|
14
|
-
|
|
12
|
+
1. `http.ts` for the request/response boundary contracts and `HttpError`;
|
|
13
|
+
2. `json-api.ts` for a type-only JSON:API v1.1 specification;
|
|
14
|
+
3. `json-web-token.ts` for a type-only JOSE/JWT specification;
|
|
15
|
+
4. `opengraph.ts` for a type-only Open Graph and social metadata specification.
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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.
|
|
17
|
+
Only `http.ts` contains runtime code. The other three files describe
|
|
18
|
+
compile-time structure for widely used formats so adapters do not have to
|
|
19
|
+
redefine them; they do not implement parsing, validation, or serialization.
|
|
26
20
|
|
|
27
21
|
#### Request Handler
|
|
28
22
|
|
|
29
23
|
`HttpRequestHandler` is responsible for processing a request and returning a
|
|
30
24
|
response.
|
|
31
25
|
|
|
32
|
-
```ts title="shared/application/http.ts"
|
|
26
|
+
```ts title="shared/application/http/http.ts"
|
|
33
27
|
export interface HttpRequestHandler {
|
|
34
|
-
handle(request:
|
|
28
|
+
handle(request: Request): Response | Promise<Response>
|
|
35
29
|
}
|
|
36
30
|
```
|
|
37
31
|
|
|
38
|
-
In the following example we
|
|
39
|
-
|
|
32
|
+
In the following example we implement a handler using the standard `Request`
|
|
33
|
+
and `Response` objects.
|
|
40
34
|
|
|
41
35
|
```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
|
-
}
|
|
36
|
+
import { HttpRequestHandler } from '../../shared/application/http/http.js'
|
|
59
37
|
|
|
60
38
|
export class GetUserHandler implements HttpRequestHandler {
|
|
61
|
-
public handle(request:
|
|
62
|
-
const
|
|
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
|
|
39
|
+
public handle(request: Request): Response {
|
|
40
|
+
const id = new URL(request.url).pathname.split('/').at(-1)
|
|
41
|
+
|
|
42
|
+
return Response.json({ data: { id } }, { status: 200 })
|
|
74
43
|
}
|
|
75
44
|
}
|
|
76
45
|
```
|
|
77
46
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
47
|
+
Using the standard `Request` and `Response` types means the adapter relies
|
|
48
|
+
on the platform's own APIs, such as `request.url`, `request.headers`, and
|
|
49
|
+
`Response.json`. The shape of the JSON body itself, when the adapter follows
|
|
50
|
+
JSON:API, is described by the document types in `json-api.ts`.
|
|
81
51
|
|
|
82
52
|
#### Middleware
|
|
83
53
|
|
|
84
54
|
`HttpMiddleware` is responsible for running logic before or around the handler.
|
|
85
55
|
|
|
86
|
-
```ts title="shared/application/http.ts"
|
|
56
|
+
```ts title="shared/application/http/http.ts"
|
|
87
57
|
export interface HttpMiddleware {
|
|
88
58
|
process(
|
|
89
|
-
request:
|
|
59
|
+
request: Request,
|
|
90
60
|
handler: HttpRequestHandler,
|
|
91
|
-
):
|
|
61
|
+
): Response | Promise<Response>
|
|
92
62
|
}
|
|
93
63
|
```
|
|
94
64
|
|
|
@@ -97,29 +67,69 @@ Now that the handler exists, middleware can wrap it.
|
|
|
97
67
|
```ts title="users/adapters/request-logger.ts"
|
|
98
68
|
import {
|
|
99
69
|
HttpMiddleware,
|
|
100
|
-
HttpRequest,
|
|
101
70
|
HttpRequestHandler,
|
|
102
|
-
|
|
103
|
-
} from '../../shared/application/http.js'
|
|
71
|
+
} from '../../shared/application/http/http.js'
|
|
104
72
|
|
|
105
73
|
export class RequestLoggerMiddleware implements HttpMiddleware {
|
|
106
74
|
public async process(
|
|
107
|
-
request:
|
|
75
|
+
request: Request,
|
|
108
76
|
handler: HttpRequestHandler,
|
|
109
|
-
): Promise<
|
|
77
|
+
): Promise<Response> {
|
|
110
78
|
void request
|
|
111
79
|
return handler.handle(request)
|
|
112
80
|
}
|
|
113
81
|
}
|
|
114
82
|
```
|
|
115
83
|
|
|
116
|
-
This middleware
|
|
84
|
+
This middleware passes the request through unchanged, showing where
|
|
117
85
|
cross-cutting behavior belongs in the generated HTTP abstraction.
|
|
118
86
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
87
|
+
#### HTTP Errors
|
|
88
|
+
|
|
89
|
+
`HttpError` is responsible for carrying an HTTP status code alongside a
|
|
90
|
+
matching message.
|
|
91
|
+
|
|
92
|
+
```ts title="shared/application/http/http.ts"
|
|
93
|
+
export class HttpError extends Error {
|
|
94
|
+
public static readonly messages: { [code: number]: string } = Object.freeze({
|
|
95
|
+
400: 'Bad Request',
|
|
96
|
+
404: 'Not Found',
|
|
97
|
+
409: 'Conflict',
|
|
98
|
+
// ...remaining standard 4xx/5xx status codes
|
|
99
|
+
500: 'Internal Server Error',
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
public readonly code: number
|
|
103
|
+
|
|
104
|
+
constructor(code: number, message?: string) {
|
|
105
|
+
super(message ?? HttpError.messages[code] ?? 'Unknown Error')
|
|
106
|
+
this.code = code
|
|
107
|
+
this.name = 'HttpError'
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`HttpError.messages` maps every standard 4xx/5xx status code to its reason
|
|
113
|
+
phrase. A caller can throw `new HttpError(404)` to get the standard message for
|
|
114
|
+
free, or pass an explicit `message` to override it. Codes outside the map fall
|
|
115
|
+
back to `'Unknown Error'`.
|
|
116
|
+
|
|
117
|
+
```ts title="users/adapters/get-user-handler.ts"
|
|
118
|
+
import { HttpError } from '../../shared/application/http/http.js'
|
|
119
|
+
|
|
120
|
+
function assertFound<T>(value: T | null): T {
|
|
121
|
+
if (value === null) {
|
|
122
|
+
throw new HttpError(404)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return value
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
> **Note**
|
|
130
|
+
> The shared HTTP contracts define the minimum boundary for adapters. Routing,
|
|
131
|
+
> serialization, and status code policies beyond `HttpError` are the
|
|
132
|
+
> responsibility of the concrete transport.
|
|
123
133
|
|
|
124
134
|
#### Example Flow
|
|
125
135
|
|
|
@@ -132,3 +142,142 @@ flowchart LR
|
|
|
132
142
|
|
|
133
143
|
This flow keeps the transport boundary explicit while leaving framework choices
|
|
134
144
|
to the adapter layer.
|
|
145
|
+
|
|
146
|
+
#### JSON:API
|
|
147
|
+
|
|
148
|
+
`json-api.ts` declares a type-only implementation of the
|
|
149
|
+
[JSON:API v1.1](https://jsonapi.org/format/) specification, including the
|
|
150
|
+
[Atomic Operations extension](https://jsonapi.org/ext/atomic/). It gives an
|
|
151
|
+
adapter a shared vocabulary for request and response bodies without forcing a
|
|
152
|
+
particular server framework.
|
|
153
|
+
|
|
154
|
+
The main building blocks are:
|
|
155
|
+
|
|
156
|
+
- `JsonApiResourceObject` / `JsonApiResourceIdentifier` for resources and
|
|
157
|
+
resource linkage, generic over the resource `type`, `attributes`, and
|
|
158
|
+
`relationships`;
|
|
159
|
+
- `JsonApiRelationship`, `JsonApiToOneRelationship`, and
|
|
160
|
+
`JsonApiToManyRelationship` for relationship objects;
|
|
161
|
+
- `JsonApiError` for the top-level error object;
|
|
162
|
+
- `JsonApiDocument` (and its narrower aliases such as
|
|
163
|
+
`JsonApiSingleResourceDocument` and `JsonApiResourceCollectionDocument`) for
|
|
164
|
+
the top-level document, discriminated between a data document, an error
|
|
165
|
+
document, and a meta-only document;
|
|
166
|
+
- `JsonApiAtomicOperationsDocument` / `JsonApiAtomicResultsDocument` for the
|
|
167
|
+
Atomic Operations extension request and response bodies.
|
|
168
|
+
|
|
169
|
+
```ts title="users/adapters/get-user-handler.ts"
|
|
170
|
+
import { HttpRequestHandler } from '../../shared/application/http/http.js'
|
|
171
|
+
import {
|
|
172
|
+
JsonApiSingleResourceDocument,
|
|
173
|
+
JsonApiResourceObject,
|
|
174
|
+
} from '../../shared/application/http/json-api.js'
|
|
175
|
+
|
|
176
|
+
type UserAttributes = { email: string }
|
|
177
|
+
type UserResource = JsonApiResourceObject<'users', UserAttributes>
|
|
178
|
+
|
|
179
|
+
export class GetUserHandler implements HttpRequestHandler {
|
|
180
|
+
public handle(request: Request): Response {
|
|
181
|
+
const id = new URL(request.url).pathname.split('/').at(-1) ?? ''
|
|
182
|
+
|
|
183
|
+
const body: JsonApiSingleResourceDocument<UserResource> = {
|
|
184
|
+
data: {
|
|
185
|
+
type: 'users',
|
|
186
|
+
id,
|
|
187
|
+
attributes: { email: 'ada@example.com' },
|
|
188
|
+
},
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return Response.json(body, { status: 200 })
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
> **Hint**
|
|
197
|
+
> These declarations only provide compile-time structure. Rules that depend on
|
|
198
|
+
> runtime values, URI validity, document-wide uniqueness, or member-name
|
|
199
|
+
> character validation still require explicit checks in the adapter.
|
|
200
|
+
|
|
201
|
+
#### JSON Web Tokens
|
|
202
|
+
|
|
203
|
+
`json-web-token.ts` declares a type-only implementation of the JOSE and JWT
|
|
204
|
+
family of RFCs (JWS, JWE, JWK, JWT, and related extensions such as DPoP and
|
|
205
|
+
selective disclosure). It lets an adapter describe tokens and keys precisely
|
|
206
|
+
without depending on a specific JOSE library's own types.
|
|
207
|
+
|
|
208
|
+
The main building blocks are:
|
|
209
|
+
|
|
210
|
+
- branded wire-format primitives such as `Base64Url`, `NumericDate`, and
|
|
211
|
+
`CompactJwt`;
|
|
212
|
+
- `JsonWebKey` / `JsonWebKeySet` for keys, covering EC, RSA, `oct`, OKP, and
|
|
213
|
+
ML-DSA (`AKP`) key types;
|
|
214
|
+
- `JwsHeader` / `JweHeader` for protected header parameters, and
|
|
215
|
+
`JwsJsonSerialization` / `JweJsonSerialization` for the JSON serializations;
|
|
216
|
+
- `JwtClaims` (built on `IanaRegisteredJwtClaims`) for decoded payloads, plus
|
|
217
|
+
ready-made profiles such as `OpenIdConnectIdTokenClaims`,
|
|
218
|
+
`OAuth2JwtAccessTokenClaims`, `DpopProofClaims`, and `SdJwtClaims`;
|
|
219
|
+
- service contracts an adapter can implement against a concrete JOSE
|
|
220
|
+
library: `JwtDecoder`, `JwsSigner`, `JwsVerifier`, `JweEncrypter`,
|
|
221
|
+
`JweDecrypter`, `JwkThumbprinter`, and `JwksResolver`, plus
|
|
222
|
+
`JwtValidationResult` for the outcome of validating a token against a
|
|
223
|
+
`JwtValidationPolicy`.
|
|
224
|
+
|
|
225
|
+
```ts title="users/adapters/verify-access-token.ts"
|
|
226
|
+
import {
|
|
227
|
+
JwsVerifier,
|
|
228
|
+
OAuth2JwtAccessTokenClaims,
|
|
229
|
+
CompactJws,
|
|
230
|
+
JsonWebKey,
|
|
231
|
+
} from '../../shared/application/http/json-web-token.js'
|
|
232
|
+
|
|
233
|
+
export function verifyAccessToken(
|
|
234
|
+
verifier: JwsVerifier,
|
|
235
|
+
token: CompactJws,
|
|
236
|
+
key: JsonWebKey,
|
|
237
|
+
) {
|
|
238
|
+
return verifier.verify<OAuth2JwtAccessTokenClaims>(token, key)
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
> **Hint**
|
|
243
|
+
> This module has no runtime implementation. Pair it with a concrete JOSE
|
|
244
|
+
> library (for signing, encryption, or verification) and use these types to
|
|
245
|
+
> annotate its inputs and outputs.
|
|
246
|
+
|
|
247
|
+
#### Open Graph
|
|
248
|
+
|
|
249
|
+
`opengraph.ts` declares a type-only implementation of the
|
|
250
|
+
[Open Graph protocol](https://ogp.me/), the Twitter Card meta tags, and
|
|
251
|
+
Facebook's compatibility extensions. It is used when an adapter needs to build
|
|
252
|
+
or read the social-sharing metadata of a page.
|
|
253
|
+
|
|
254
|
+
The main building blocks are:
|
|
255
|
+
|
|
256
|
+
- `OpenGraphMetadata`, a union of every standard Open Graph object type
|
|
257
|
+
(`OpenGraphWebsite`, `OpenGraphArticle`, `OpenGraphBook`, `OpenGraphProfile`,
|
|
258
|
+
the `music.*` and `video.*` types, `OpenGraphPaymentLink`, and
|
|
259
|
+
`OpenGraphCustomObject` for CURIE-style custom types);
|
|
260
|
+
- `OpenGraphMetaTag` and `TwitterMetaTag`, the flat `property`/`content` and
|
|
261
|
+
`name`/`content` tag representations closer to the actual `<meta>` markup;
|
|
262
|
+
- `SocialMetadataDocument`, an aggregate of Open Graph, Twitter, Facebook, and
|
|
263
|
+
standard head metadata for a single page, and `RawSocialMetadataDocument`
|
|
264
|
+
for its rendered, tag-list form.
|
|
265
|
+
|
|
266
|
+
```ts title="users/adapters/user-profile-metadata.ts"
|
|
267
|
+
import { OpenGraphProfile } from '../../shared/application/http/opengraph.js'
|
|
268
|
+
|
|
269
|
+
export function buildProfileMetadata(username: string): OpenGraphProfile {
|
|
270
|
+
return {
|
|
271
|
+
type: 'profile',
|
|
272
|
+
title: username,
|
|
273
|
+
url: `https://example.com/users/${username}`,
|
|
274
|
+
images: [{ url: `https://example.com/users/${username}/avatar.png` }],
|
|
275
|
+
username,
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
> **Hint**
|
|
281
|
+
> This module has no runtime implementation, including no HTML rendering. Use
|
|
282
|
+
> `OpenGraphMetadata` to build the data and a separate template or renderer to
|
|
283
|
+
> emit the `<meta>` tags described by `OpenGraphMetaTag`/`TwitterMetaTag`.
|
|
@@ -28,22 +28,57 @@ any adapter to use a particular logger implementation.
|
|
|
28
28
|
`Logger` is responsible for receiving log data from the application layer.
|
|
29
29
|
|
|
30
30
|
```ts title="shared/application/loggers.ts"
|
|
31
|
+
import { type TimeZone } from '../../types/iana.js'
|
|
32
|
+
import { type Locale } from '../../types/cldr.js'
|
|
33
|
+
|
|
31
34
|
export abstract class Logger {
|
|
32
|
-
|
|
35
|
+
[property: string]: unknown
|
|
33
36
|
|
|
34
|
-
|
|
37
|
+
public name: string = 'main'
|
|
35
38
|
|
|
36
|
-
|
|
39
|
+
public level: number = 0
|
|
37
40
|
|
|
38
|
-
|
|
41
|
+
public datetimeLocales: Locale[] = ['en-GB']
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
public datetimeFormatOptions: Intl.DateTimeFormatOptions & { timeZone: TimeZone } = {
|
|
44
|
+
timeZone: 'UTC',
|
|
45
|
+
year: 'numeric',
|
|
46
|
+
month: '2-digit',
|
|
47
|
+
day: '2-digit',
|
|
48
|
+
hour: '2-digit',
|
|
49
|
+
minute: '2-digit',
|
|
50
|
+
second: '2-digit',
|
|
51
|
+
fractionalSecondDigits: 3,
|
|
52
|
+
hourCycle: 'h23'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public abstract debug(data: unknown): void
|
|
56
|
+
|
|
57
|
+
public abstract info(data: unknown): void
|
|
58
|
+
|
|
59
|
+
public abstract warning(data: unknown): void
|
|
60
|
+
|
|
61
|
+
public abstract error(data: unknown): void
|
|
62
|
+
|
|
63
|
+
public abstract critical(data: unknown): void
|
|
64
|
+
|
|
65
|
+
protected getCurrentDatetime(): string {
|
|
66
|
+
return new Date().toLocaleString(this.datetimeLocales, this.datetimeFormatOptions)
|
|
67
|
+
}
|
|
68
|
+
} //:: class
|
|
42
69
|
```
|
|
43
70
|
|
|
44
71
|
The contract is intentionally small. It defines the actions the application can
|
|
45
72
|
request, while the adapter decides how those actions are persisted or displayed.
|
|
46
73
|
|
|
74
|
+
`name` and `level` identify the logger instance and its minimum severity, so an
|
|
75
|
+
adapter can decide which logs to emit or route. `datetimeLocales` and
|
|
76
|
+
`datetimeFormatOptions` control how `getCurrentDatetime()` formats the current
|
|
77
|
+
moment, using the `Locale` type from `types/cldr.d.ts` and the `TimeZone` type
|
|
78
|
+
from `types/iana.d.ts`. Adapters can use `getCurrentDatetime()` to timestamp
|
|
79
|
+
log entries consistently, regardless of the runtime environment's own locale
|
|
80
|
+
or timezone.
|
|
81
|
+
|
|
47
82
|
#### First Adapter
|
|
48
83
|
|
|
49
84
|
In the following example we implement a console-based logger.
|
|
@@ -51,43 +86,32 @@ In the following example we implement a console-based logger.
|
|
|
51
86
|
```ts title="users/adapters/console-logger.ts"
|
|
52
87
|
import { Logger } from '../../shared/application/loggers.js'
|
|
53
88
|
|
|
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
89
|
export class ConsoleLogger extends Logger {
|
|
63
|
-
constructor(protected readonly externalService: ExternalService) {
|
|
64
|
-
super()
|
|
65
|
-
}
|
|
66
|
-
|
|
67
90
|
public debug(data: unknown): void {
|
|
68
|
-
this.
|
|
91
|
+
console.debug(this.getCurrentDatetime(), this.name, data)
|
|
69
92
|
}
|
|
70
93
|
|
|
71
94
|
public info(data: unknown): void {
|
|
72
|
-
this.
|
|
95
|
+
console.info(this.getCurrentDatetime(), this.name, data)
|
|
73
96
|
}
|
|
74
97
|
|
|
75
98
|
public warning(data: unknown): void {
|
|
76
|
-
this.
|
|
99
|
+
console.warn(this.getCurrentDatetime(), this.name, data)
|
|
77
100
|
}
|
|
78
101
|
|
|
79
102
|
public error(data: unknown): void {
|
|
80
|
-
this.
|
|
103
|
+
console.error(this.getCurrentDatetime(), this.name, data)
|
|
81
104
|
}
|
|
82
105
|
|
|
83
106
|
public critical(data: unknown): void {
|
|
84
|
-
this.
|
|
107
|
+
console.error(this.getCurrentDatetime(), this.name, data)
|
|
85
108
|
}
|
|
86
109
|
}
|
|
87
110
|
```
|
|
88
111
|
|
|
89
112
|
This adapter satisfies the generated contract without changing the application
|
|
90
|
-
layer.
|
|
113
|
+
layer. It reuses `getCurrentDatetime()` to prefix every entry with a
|
|
114
|
+
consistently formatted timestamp.
|
|
91
115
|
|
|
92
116
|
#### Service Integration
|
|
93
117
|
|
|
@@ -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.26",
|
|
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
|
},
|