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,165 @@
|
|
|
1
|
+
### Context Ports
|
|
2
|
+
|
|
3
|
+
Context ports define the communication available at a context boundary.
|
|
4
|
+
They describe what data enters the context, what data leaves it, and which
|
|
5
|
+
operation another system can invoke.
|
|
6
|
+
|
|
7
|
+
Ports are used to keep communication contracts explicit. An adapter imports a
|
|
8
|
+
port, implements that contract, and translates the external interaction into an
|
|
9
|
+
application process.
|
|
10
|
+
|
|
11
|
+
> **Hint**
|
|
12
|
+
> The generated `example-ports.ts` file is only a placeholder. Replace it when
|
|
13
|
+
> the first real interaction of the context becomes clear.
|
|
14
|
+
|
|
15
|
+
#### Root Port File
|
|
16
|
+
|
|
17
|
+
The generated context starts with a single root file for ports.
|
|
18
|
+
|
|
19
|
+
```ts title="users/example-ports.ts"
|
|
20
|
+
export function example(): void {
|
|
21
|
+
// ...
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
This placeholder does not define a real contract yet. Its purpose is to mark
|
|
26
|
+
the context root as the place where boundary-facing types and interfaces live.
|
|
27
|
+
|
|
28
|
+
#### First Contract
|
|
29
|
+
|
|
30
|
+
In the following example we replace the placeholder with a minimal contract for
|
|
31
|
+
creating a user.
|
|
32
|
+
|
|
33
|
+
```ts title="users/example-ports.ts"
|
|
34
|
+
export type CreateUserRequest = {
|
|
35
|
+
user: {
|
|
36
|
+
id: string
|
|
37
|
+
email: string
|
|
38
|
+
active: boolean | null
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type CreateUserResponse = {
|
|
43
|
+
user: {
|
|
44
|
+
id: string
|
|
45
|
+
email: string
|
|
46
|
+
active: boolean | null
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface CreateUserPort {
|
|
51
|
+
create(request: CreateUserRequest): Promise<CreateUserResponse>
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`CreateUserRequest` defines the incoming payload. `CreateUserResponse` defines
|
|
56
|
+
the outgoing payload. `CreateUserPort` exposes the operation available at the
|
|
57
|
+
boundary.
|
|
58
|
+
|
|
59
|
+
This contract says nothing about HTTP, queues, or databases. It only declares
|
|
60
|
+
the shape of the communication.
|
|
61
|
+
|
|
62
|
+
#### Adapter Implementation
|
|
63
|
+
|
|
64
|
+
Now that the port exists, an adapter can implement it and delegate the work to
|
|
65
|
+
an application service.
|
|
66
|
+
|
|
67
|
+
```ts title="users/adapters/create-user.ts"
|
|
68
|
+
import type {
|
|
69
|
+
CreateUserPort,
|
|
70
|
+
CreateUserRequest,
|
|
71
|
+
CreateUserResponse,
|
|
72
|
+
} from '../example-ports.js'
|
|
73
|
+
|
|
74
|
+
type CreateUserService = {
|
|
75
|
+
execute(data: {
|
|
76
|
+
id: string
|
|
77
|
+
email: string
|
|
78
|
+
active: boolean | null
|
|
79
|
+
}): Promise<CreateUserResponse>
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export class CreateUserAdapter implements CreateUserPort {
|
|
83
|
+
public constructor(
|
|
84
|
+
private readonly service: CreateUserService,
|
|
85
|
+
) {}
|
|
86
|
+
|
|
87
|
+
public async create(
|
|
88
|
+
request: CreateUserRequest,
|
|
89
|
+
): Promise<CreateUserResponse> {
|
|
90
|
+
return this.service.execute({
|
|
91
|
+
id: request.user.id,
|
|
92
|
+
email: request.user.email,
|
|
93
|
+
active: request.user.active,
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The adapter implements `CreateUserPort`, so it must provide `create()`. Inside
|
|
100
|
+
that method it translates the root-level request into the input expected by the
|
|
101
|
+
application service.
|
|
102
|
+
|
|
103
|
+
This is the normal flow of the generated structure:
|
|
104
|
+
|
|
105
|
+
```text
|
|
106
|
+
external system -> adapter -> application -> domain
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The port belongs to the boundary. The adapter materializes the boundary. The
|
|
110
|
+
application process executes the use case.
|
|
111
|
+
|
|
112
|
+
#### Multiple Port Files
|
|
113
|
+
|
|
114
|
+
As the context grows, you can keep several contracts at the context root.
|
|
115
|
+
|
|
116
|
+
```ts title="users/list-users.ts"
|
|
117
|
+
export type ListUsersRequest = {
|
|
118
|
+
active: boolean | null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export type ListUsersResponse = {
|
|
122
|
+
users: Array<{
|
|
123
|
+
id: string
|
|
124
|
+
email: string
|
|
125
|
+
active: boolean | null
|
|
126
|
+
}>
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface ListUsersPort {
|
|
130
|
+
list(request: ListUsersRequest): Promise<ListUsersResponse>
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
An adapter can then import the contract from the file that owns it.
|
|
135
|
+
|
|
136
|
+
This arrangement is useful when one context exposes several independent forms
|
|
137
|
+
of communication. A single file works well for a small context. Separate files
|
|
138
|
+
become easier to maintain when responsibilities start to diverge.
|
|
139
|
+
|
|
140
|
+
> **Warning**
|
|
141
|
+
> A port should define communication, not domain behavior. Avoid moving entity
|
|
142
|
+
> rules, repository logic, or infrastructure details into the port file.
|
|
143
|
+
|
|
144
|
+
#### Example Layout
|
|
145
|
+
|
|
146
|
+
The following structure keeps ports at the root while the implementation lives
|
|
147
|
+
in the generated folders.
|
|
148
|
+
|
|
149
|
+
```text
|
|
150
|
+
users/
|
|
151
|
+
├── example-ports.ts
|
|
152
|
+
├── list-users.ts
|
|
153
|
+
├── adapters/
|
|
154
|
+
├── application/
|
|
155
|
+
└── domain/
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
This layout keeps the context boundary visible from the top level. It also
|
|
159
|
+
reduces coupling between adapters because they all import the same contracts.
|
|
160
|
+
|
|
161
|
+
#### Next Step
|
|
162
|
+
|
|
163
|
+
After defining a port, implement the corresponding adapter and connect it to an
|
|
164
|
+
application service. The surrounding structure is described in
|
|
165
|
+
`library-structure.md`.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
### Generated File Reference
|
|
2
|
+
|
|
3
|
+
This reference describes the files generated by the library template.
|
|
4
|
+
Use it to identify where a concern belongs before adding new code.
|
|
5
|
+
|
|
6
|
+
#### Root Files
|
|
7
|
+
|
|
8
|
+
The root contains the main entry points of the generated library.
|
|
9
|
+
|
|
10
|
+
| File | Responsibility |
|
|
11
|
+
| --- | --- |
|
|
12
|
+
| `index.d.ts` | Declares root-level shared types such as `Generic<T>`. |
|
|
13
|
+
| `main.ts` | Starts as a placeholder for the main implementation and root exports. |
|
|
14
|
+
|
|
15
|
+
`index.d.ts` is the place for root-level type declarations. `main.ts` is the
|
|
16
|
+
place for the main runtime entry point when the library starts exposing shared
|
|
17
|
+
runtime components.
|
|
18
|
+
|
|
19
|
+
#### Shared Domain Files
|
|
20
|
+
|
|
21
|
+
The `shared/domain` directory contains foundations for domain modeling.
|
|
22
|
+
|
|
23
|
+
| File | Responsibility |
|
|
24
|
+
| --- | --- |
|
|
25
|
+
| `shared/domain/value-objects.ts` | Declares `ValueObject<T>` and provides `Email` and `NullableBoolean`. |
|
|
26
|
+
| `shared/domain/entities.ts` | Declares the `Entity` base class. |
|
|
27
|
+
| `shared/domain/aggregates.ts` | Declares the `Aggregate` base class. |
|
|
28
|
+
| `shared/domain/errors.ts` | Declares `ValueError`. |
|
|
29
|
+
|
|
30
|
+
These files contain reusable domain concepts whose meaning can be shared across
|
|
31
|
+
contexts. They are discussed in more detail in `shared/domain/*.md`.
|
|
32
|
+
|
|
33
|
+
#### Shared Application Files
|
|
34
|
+
|
|
35
|
+
The `shared/application` directory contains contracts used to coordinate use
|
|
36
|
+
cases and integrations.
|
|
37
|
+
|
|
38
|
+
| File | Responsibility |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| `shared/application/validations.ts` | Declares the `Validatable` contract. |
|
|
41
|
+
| `shared/application/services.ts` | Declares the `Service` base class for use cases. |
|
|
42
|
+
| `shared/application/http.ts` | Declares framework-agnostic HTTP contracts. |
|
|
43
|
+
| `shared/application/loggers.ts` | Declares shared log levels and the `Logger` contract. |
|
|
44
|
+
| `shared/application/events.ts` | Declares `Event`, `EventHandler`, and `EventDispatcher`. |
|
|
45
|
+
|
|
46
|
+
These files do not implement frameworks or transports. They define the stable
|
|
47
|
+
contracts that adapters and services can share.
|
|
48
|
+
|
|
49
|
+
#### Shared Data Files
|
|
50
|
+
|
|
51
|
+
The generated template also includes a small set of data-access abstractions.
|
|
52
|
+
|
|
53
|
+
| File | Responsibility |
|
|
54
|
+
| --- | --- |
|
|
55
|
+
| `shared/application/data/drivers.ts` | Declares `DriverAdapter`, the connection contract with a data source. |
|
|
56
|
+
| `shared/application/data/managers.ts` | Declares `DataManager`, `DatasetManager`, and plain-record operations. |
|
|
57
|
+
| `shared/application/data/repositories.ts` | Declares `Repository`, which transforms raw records into domain representations. |
|
|
58
|
+
|
|
59
|
+
These contracts belong to the application layer because they define how the
|
|
60
|
+
application coordinates data access without forcing a specific driver.
|
|
61
|
+
|
|
62
|
+
#### Context Files
|
|
63
|
+
|
|
64
|
+
Each generated context starts with a root port file and three directories.
|
|
65
|
+
|
|
66
|
+
| Path | Responsibility |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| `users/example-ports.ts` | Placeholder root file for context ports. |
|
|
69
|
+
| `users/domain/` | Domain capabilities and rules for the context. |
|
|
70
|
+
| `users/application/` | Processes that use domain capabilities to fulfill system purposes. |
|
|
71
|
+
| `users/adapters/` | Integrations that implement ports and connect the context to external systems. |
|
|
72
|
+
|
|
73
|
+
The `users/` path is an example context name. Your project can generate one or
|
|
74
|
+
more contexts with the same internal layout.
|
|
75
|
+
|
|
76
|
+
#### How To Use This Reference
|
|
77
|
+
|
|
78
|
+
Use the following sequence when deciding where new code belongs.
|
|
79
|
+
|
|
80
|
+
1. Put reusable domain concepts in `shared/domain`.
|
|
81
|
+
2. Put reusable application contracts in `shared/application`.
|
|
82
|
+
3. Put context-specific rules in `<context>/domain`.
|
|
83
|
+
4. Put use cases in `<context>/application`.
|
|
84
|
+
5. Put boundary implementations in `<context>/adapters`.
|
|
85
|
+
6. Put boundary contracts at the context root.
|
|
86
|
+
|
|
87
|
+
This reference explains placement. The architectural rationale is described in
|
|
88
|
+
`library-structure.md`.
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
### Library Structure
|
|
2
|
+
|
|
3
|
+
The library structure organizes generated code into root files, shared
|
|
4
|
+
capabilities, and one or more contexts.
|
|
5
|
+
Each level has a distinct responsibility so that domain rules, use cases, and
|
|
6
|
+
integrations do not collapse into the same place.
|
|
7
|
+
|
|
8
|
+
This structure is used to keep the implementation centered on capabilities.
|
|
9
|
+
Shared code holds common abstractions. Contexts hold application-specific
|
|
10
|
+
language, rules, and operations.
|
|
11
|
+
|
|
12
|
+
#### Root
|
|
13
|
+
|
|
14
|
+
The root contains the entry points of the generated library.
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
index.d.ts
|
|
18
|
+
main.ts
|
|
19
|
+
shared/
|
|
20
|
+
users/
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`index.d.ts` defines root-level types. `main.ts` starts as a placeholder for
|
|
24
|
+
main runtime exports. The rest of the structure lives under `shared/` and one
|
|
25
|
+
or more context directories.
|
|
26
|
+
|
|
27
|
+
#### Shared
|
|
28
|
+
|
|
29
|
+
The `shared` directory contains concepts that can be reused by multiple
|
|
30
|
+
contexts.
|
|
31
|
+
|
|
32
|
+
```text
|
|
33
|
+
shared/
|
|
34
|
+
├── application/
|
|
35
|
+
└── domain/
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`shared/domain` contains modeling foundations such as value objects, entities,
|
|
39
|
+
aggregates, and errors. `shared/application` contains contracts for services,
|
|
40
|
+
validations, events, logging, HTTP boundaries, and data access.
|
|
41
|
+
|
|
42
|
+
Move code to `shared` only when its meaning belongs to more than one context.
|
|
43
|
+
Until then, keep it close to the context that owns the rule.
|
|
44
|
+
|
|
45
|
+
#### Contexts
|
|
46
|
+
|
|
47
|
+
A context groups the vocabulary, rules, and operations of one application
|
|
48
|
+
capability.
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
users/
|
|
52
|
+
billing/
|
|
53
|
+
inventory/
|
|
54
|
+
sales/
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Each context can evolve independently while still reusing the abstractions from
|
|
58
|
+
`shared`. This separation reduces accidental coupling between unrelated parts of
|
|
59
|
+
the system.
|
|
60
|
+
|
|
61
|
+
#### Context Layout
|
|
62
|
+
|
|
63
|
+
Every generated context starts with the same internal structure.
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
users/
|
|
67
|
+
├── example-ports.ts
|
|
68
|
+
├── adapters/
|
|
69
|
+
├── application/
|
|
70
|
+
└── domain/
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`example-ports.ts` is the root communication surface of the context.
|
|
74
|
+
`domain/` contains capabilities and rules. `application/` contains processes.
|
|
75
|
+
`adapters/` contains boundary implementations.
|
|
76
|
+
|
|
77
|
+
#### Domain
|
|
78
|
+
|
|
79
|
+
The domain layer contains the context's capabilities.
|
|
80
|
+
|
|
81
|
+
This layer models concepts that carry business meaning: value objects, entities,
|
|
82
|
+
aggregates, and the rules that make them valid. The domain should not depend on
|
|
83
|
+
transport concerns or infrastructure details.
|
|
84
|
+
|
|
85
|
+
Typical domain responsibilities include validating an email address, identifying
|
|
86
|
+
an entity, changing the state of an order, or grouping related entities into a
|
|
87
|
+
single unit.
|
|
88
|
+
|
|
89
|
+
#### Application
|
|
90
|
+
|
|
91
|
+
The application layer contains processes that use domain capabilities to
|
|
92
|
+
fulfill a system purpose.
|
|
93
|
+
|
|
94
|
+
A service in this layer coordinates collaborators. It can validate input,
|
|
95
|
+
construct domain objects, read or write data through abstractions, publish an
|
|
96
|
+
event, and log the result.
|
|
97
|
+
|
|
98
|
+
The application layer is responsible for orchestration, not for owning the
|
|
99
|
+
business rules themselves.
|
|
100
|
+
|
|
101
|
+
#### Adapters
|
|
102
|
+
|
|
103
|
+
The adapters layer contains the integrations that connect a context to other
|
|
104
|
+
systems.
|
|
105
|
+
|
|
106
|
+
An adapter can expose an HTTP handler, consume a message, call a remote API,
|
|
107
|
+
implement a data driver, or connect to an event bus. Its role is to translate
|
|
108
|
+
external input or output into the contracts expected by the application layer.
|
|
109
|
+
|
|
110
|
+
#### Ports
|
|
111
|
+
|
|
112
|
+
Ports define the communication available at the context boundary.
|
|
113
|
+
|
|
114
|
+
They live at the context root because they describe how the context is used
|
|
115
|
+
from the outside. An adapter imports a port, implements it, and delegates the
|
|
116
|
+
work to an application process.
|
|
117
|
+
|
|
118
|
+
The generated template starts with `example-ports.ts`, but a larger context may
|
|
119
|
+
split ports across several files. The detailed guidance for that layout lives in
|
|
120
|
+
`context-ports.md`.
|
|
121
|
+
|
|
122
|
+
#### Dependency Direction
|
|
123
|
+
|
|
124
|
+
The normal dependency direction is the following:
|
|
125
|
+
|
|
126
|
+
```text
|
|
127
|
+
adapter -> port
|
|
128
|
+
adapter -> application
|
|
129
|
+
application -> domain
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
This direction keeps the core model isolated from transport and infrastructure
|
|
133
|
+
details. The deeper a layer is, the less it should know about the outside.
|
|
134
|
+
|
|
135
|
+
> **Warning**
|
|
136
|
+
> Avoid importing adapter-specific concerns into the domain layer. Once a domain
|
|
137
|
+
> object depends on HTTP, database, or framework details, the context boundary
|
|
138
|
+
> becomes harder to change.
|
|
139
|
+
|
|
140
|
+
#### Example Flow
|
|
141
|
+
|
|
142
|
+
The following diagram shows the runtime flow of a typical operation.
|
|
143
|
+
|
|
144
|
+
```text
|
|
145
|
+
External system
|
|
146
|
+
|
|
|
147
|
+
v
|
|
148
|
+
Port + adapter
|
|
149
|
+
|
|
|
150
|
+
v
|
|
151
|
+
Application service
|
|
152
|
+
|
|
|
153
|
+
v
|
|
154
|
+
Domain capability
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The adapter receives the input, the application service coordinates the use
|
|
158
|
+
case, and the domain provides the rules and behavior required by that use case.
|
|
159
|
+
|
|
160
|
+
#### Next Step
|
|
161
|
+
|
|
162
|
+
Use this structure as the default layout for new code. When you need to inspect
|
|
163
|
+
the purpose of a generated file, consult `generated-file-reference.md`.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
### Library Types
|
|
2
|
+
|
|
3
|
+
The generated root type `Generic<T>` represents a plain object whose keys are
|
|
4
|
+
strings and whose values share the same type.
|
|
5
|
+
It provides a small common building block for code that works with object-like
|
|
6
|
+
data but does not need a more specific shape yet.
|
|
7
|
+
|
|
8
|
+
#### Root Declaration
|
|
9
|
+
|
|
10
|
+
The root declaration lives in `index.d.ts`.
|
|
11
|
+
|
|
12
|
+
```ts title="index.d.ts"
|
|
13
|
+
type Generic<T = unknown> = Record<string, T>
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
This alias expands to `Record<string, T>`. When no type argument is provided,
|
|
17
|
+
the values use `unknown`.
|
|
18
|
+
|
|
19
|
+
#### Basic Usage
|
|
20
|
+
|
|
21
|
+
In the following example we use `Generic<string>` for a set of plain filters.
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
const filters: Generic<string> = {
|
|
25
|
+
status: 'active',
|
|
26
|
+
sort: 'email',
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`filters` can only store string values because the type argument fixes the
|
|
31
|
+
value shape for the whole object.
|
|
32
|
+
|
|
33
|
+
#### Default Type Argument
|
|
34
|
+
|
|
35
|
+
Now consider the same pattern without providing a type argument.
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
const metadata: Generic = {
|
|
39
|
+
retries: 2,
|
|
40
|
+
cached: true,
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
In this case the values use `unknown`. This is useful when the object is plain
|
|
45
|
+
and open-ended, but the caller must narrow each value before using it in a
|
|
46
|
+
specific way.
|
|
47
|
+
|
|
48
|
+
#### When To Use It
|
|
49
|
+
|
|
50
|
+
Use `Generic<T>` when the code needs a simple object contract and the exact set
|
|
51
|
+
of keys is not the main concern.
|
|
52
|
+
|
|
53
|
+
Typical uses include:
|
|
54
|
+
|
|
55
|
+
1. filter objects;
|
|
56
|
+
2. metadata objects;
|
|
57
|
+
3. plain configuration maps;
|
|
58
|
+
4. transport-neutral dictionaries.
|
|
59
|
+
|
|
60
|
+
When the object has a stable business meaning, prefer a named type instead of a
|
|
61
|
+
generic record.
|
|
62
|
+
|
|
63
|
+
> **Hint**
|
|
64
|
+
> `Generic<T>` is intentionally small. It should support loose object contracts,
|
|
65
|
+
> not replace explicit domain or application types.
|
|
66
|
+
|
|
67
|
+
#### Next Step
|
|
68
|
+
|
|
69
|
+
For the rest of the generated shared abstractions, continue with the pages in
|
|
70
|
+
`shared/application` and `shared/domain`.
|
|
@@ -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.
|