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.
@@ -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.