tshex-cli 1.0.29 → 1.0.30

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.
@@ -21,7 +21,7 @@ surface that another actor can call or observe.
21
21
 
22
22
  The generated context starts with a single root file for ports.
23
23
 
24
- ```ts title="users/example-ports.ts"
24
+ ```ts title="enrollment/example-ports.ts"
25
25
  export class ExamplePort {
26
26
  public doSomething(): void {
27
27
  // ...
@@ -39,32 +39,56 @@ expected to be a module that defines one or more context ports.
39
39
  #### First Port
40
40
 
41
41
  In the following example we replace the placeholder with a concrete port for
42
- creating a user.
42
+ managing course enrollments.
43
43
 
44
- ```ts title="users/example-ports.ts"
45
- export interface User {
46
- id: string
47
- email: string
48
- active: boolean | null
49
- }
44
+ ```ts title="enrollment/example-ports.ts"
45
+ import { InMemoryDatabaseDriver } from './application/database.ts'
46
+ import { Course } from './domain/courses.ts'
47
+ import { Student } from './domain/students.ts'
48
+ import { InscriptionAggregate } from './domain/inscriptions.ts'
50
49
 
51
- export class UsersRegistry {
52
- public async createUser(user: User): Promise<User> {
53
- // ...
50
+ const database: Record<string, Record<string, unknown>[]> = {}
51
+
52
+ export class Example {
53
+ [property: string]: unknown
54
+
55
+ private readonly driver: InMemoryDatabaseDriver
56
+
57
+ constructor() {
58
+ this.driver = new InMemoryDatabaseDriver(database)
59
+ }
60
+
61
+ public createStudent(student: Student) {
62
+ const result = this.driver.connect('students').create(student.toJSON())
63
+ this.driver.disconnect()
64
+ return result
65
+ }
66
+
67
+ public createCourse(course: Course) {
68
+ const result = this.driver.connect('courses').create(course.toJSON())
69
+ this.driver.disconnect()
70
+ return result
54
71
  }
55
- }
56
- ```
57
72
 
58
- `User` expresses the user data that crosses the boundary. `UsersRegistry` is a
59
- boundary object of the context and `createUser()` is one concrete capability
60
- that it exposes.
73
+ public listInscriptions() {
74
+ const result = this.driver.connect('inscriptions').all()
75
+ this.driver.disconnect()
76
+ return result
77
+ }
61
78
 
62
- This definition focuses on the interaction the context makes available. The
63
- port keeps its identity as one boundary capability while making its data and
64
- action explicit.
79
+ public createInscription(student: Student, course: Course) {
80
+ const inscription = InscriptionAggregate.enroll(student, course)
81
+ const result = this.driver.connect('inscriptions').create(inscription.toJSON())
82
+ this.driver.disconnect()
83
+ return result
84
+ }
85
+ }
86
+ ```
65
87
 
66
- The port is the executable object that this context exposes. Types help
67
- describe it and make its boundary explicit.
88
+ `Example` is a boundary object of the context. Each static method is one
89
+ concrete capability it exposes. The port connects the boundary to a driver,
90
+ uses domain concepts internally, and keeps infrastructure details out of the
91
+ caller.
68
92
 
69
93
  This is the normal flow inside the context boundary:
70
94
 
@@ -83,10 +107,23 @@ capability and uses domain capabilities.
83
107
 
84
108
  As the context grows, you can keep several port modules at the context root.
85
109
 
86
- ```ts title="users/registry.ts"
87
- export class UsersRegistry {
88
- public async listUsers(): Promise<User[]> {
89
- // ...
110
+ ```ts title="enrollment/courses.ts"
111
+ import { CoursesService } from './application/services.ts'
112
+ import { Course } from './domain/courses.ts'
113
+
114
+ export class CoursesPort {
115
+ constructor(private readonly service: CoursesService) {}
116
+
117
+ public all(): Record<string, unknown>[] {
118
+ return this.service.all()
119
+ }
120
+
121
+ public create(name: string, description: string, hours: number): boolean {
122
+ return this.service.create(new Course(name, description, hours))
123
+ }
124
+
125
+ public delete(course: Course): boolean {
126
+ return this.service.delete(course)
90
127
  }
91
128
  }
92
129
  ```
@@ -110,9 +147,10 @@ in the generated folders.
110
147
 
111
148
  ```mermaid
112
149
  flowchart TD
113
- users["users/"] --> registry["registry.ts"]
114
- users --> application["application/"]
115
- users --> domain["domain/"]
150
+ enrollment["enrollment/"] --> examplePort["example-ports.ts"]
151
+ enrollment --> courses["courses.ts"]
152
+ enrollment --> application["application/"]
153
+ enrollment --> domain["domain/"]
116
154
  ```
117
155
 
118
156
  This layout keeps the context boundary visible from the top level. It also
@@ -85,8 +85,9 @@ The generated template also includes a small set of data-access abstractions.
85
85
 
86
86
  | File | Responsibility |
87
87
  | --- | --- |
88
+ | `shared/application/data/capabilities.ts` | Declares the operation capability interfaces: `Listable`, `Filterable`, `Sortable`, `Creatable`, `Updatable`, `Deletable`, `Aggregatable`, and `Relatable`. |
88
89
  | `shared/application/data/drivers.ts` | Declares `DriverAdapter`, the connection contract with a data source. |
89
- | `shared/application/data/managers.ts` | Declares `DataManager`, `DatasetManager`, and plain-record operations. |
90
+ | `shared/application/data/managers.ts` | Declares `DataManager` and `DatasetManager`. |
90
91
  | `shared/application/data/repositories.ts` | Declares `Repository`, which transforms raw records into domain representations. |
91
92
 
92
93
  These contracts belong to the application layer because they define how the
@@ -18,7 +18,7 @@ flowchart TD
18
18
  root["Library root"] --> types["types/"]
19
19
  root --> main["main.ts"]
20
20
  root --> shared["shared/"]
21
- root --> users["users/"]
21
+ root --> enrollment["enrollment/"]
22
22
  ```
23
23
 
24
24
  `types/` groups the root-level type declarations. `main.ts` starts as a
@@ -69,7 +69,7 @@ capability.
69
69
 
70
70
  ```mermaid
71
71
  flowchart TD
72
- contexts["Contexts"] --> users["users/"]
72
+ contexts["Contexts"] --> enrollment["enrollment/"]
73
73
  contexts --> billing["billing/"]
74
74
  contexts --> inventory["inventory/"]
75
75
  contexts --> sales["sales/"]
@@ -85,10 +85,10 @@ Every generated context starts with the same internal structure.
85
85
 
86
86
  ```mermaid
87
87
  flowchart TD
88
- users["users/"] --> ports["example-ports.ts"]
89
- users --> adapters["adapters/"]
90
- users --> application["application/"]
91
- users --> domain["domain/"]
88
+ enrollment["enrollment/"] --> ports["example-ports.ts"]
89
+ enrollment --> adapters["adapters/"]
90
+ enrollment --> application["application/"]
91
+ enrollment --> domain["domain/"]
92
92
  ```
93
93
 
94
94
  `example-ports.ts` is an example module in the root communication surface of
@@ -189,6 +189,22 @@ application services use domain capabilities, while adapters can depend on
189
189
  ports and third-party libraries. The port branch stops at the boundary because
190
190
  what exists beyond that port depends on the system that implements it.
191
191
 
192
+ ```ts title="main.ts"
193
+ import { Example } from './enrollment/example-ports.ts'
194
+ import { Student } from './enrollment/domain/students.ts'
195
+ import { Course } from './enrollment/domain/courses.ts'
196
+
197
+ const example = new Example()
198
+ const student = new Student('Ada Lovelace', 'ada@example.com')
199
+ const course = new Course('Mathematics', 'Fundamentals of algebra and calculus', 40)
200
+
201
+ example.createStudent(student)
202
+ example.createCourse(course)
203
+ example.createInscription(student, course)
204
+
205
+ console.log(example.listInscriptions())
206
+ ```
207
+
192
208
  #### Next Step
193
209
 
194
210
  Use this structure as the default layout for new code. When you need to inspect
@@ -1,188 +1,259 @@
1
1
  ### Data
2
2
 
3
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
4
+ records. They separate connection management, raw record access, and domain
6
5
  transformation so that a context can change drivers without rewriting its use
7
6
  cases.
8
7
 
9
- The generated structure splits this concern into three files:
8
+ The generated structure splits this concern into four files:
10
9
 
11
- 1. `drivers.ts` for connection adapters;
12
- 2. `managers.ts` for plain-record operations;
13
- 3. `repositories.ts` for record-to-domain transformation.
10
+ 1. `capabilities.ts` for operation capability interfaces;
11
+ 2. `drivers.ts` for connection adapters;
12
+ 3. `managers.ts` for plain-record operations;
13
+ 4. `repositories.ts` for record-to-domain transformation.
14
14
 
15
- #### Driver Adapter
15
+ #### Capabilities
16
16
 
17
- `DriverAdapter` is responsible for connecting to a data source and returning an
18
- enabled `DataManager`.
17
+ `capabilities.ts` declares the operation interfaces that a manager implements
18
+ to advertise what it supports.
19
19
 
20
- ```ts title="shared/application/data/drivers.ts"
21
- import { DataManager } from './managers.js'
20
+ ```ts title="shared/application/data/capabilities.ts"
21
+ export interface Listable<DataShape extends Generic = Generic> {
22
+ all(): DataShape[]
23
+ }
22
24
 
23
- export abstract class DriverAdapter<M extends DataManager = DataManager> {
24
- public abstract connect(...args: unknown[]): Promise<M>
25
+ export interface Creatable<DataShape extends Generic = Generic, Feedback = unknown> {
26
+ create(data: DataShape): Feedback
27
+ }
25
28
 
26
- public abstract disconnect(): Promise<unknown>
29
+ export interface Deletable<Selector = unknown, Feedback = unknown> {
30
+ delete(selector: Selector): Feedback
27
31
  }
32
+
33
+ // Also available: Filterable, Sortable, Updatable, Aggregatable, Relatable
28
34
  ```
29
35
 
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.
36
+ Implement only the interfaces that the manager actually supports. Adding
37
+ `Creatable` to a class makes the creation capability explicit and discoverable
38
+ without adding it to the base class.
39
+
40
+ ```ts title="enrollment/application/managers.ts"
41
+ import { DataManager } from '../shared/application/data/managers.ts'
42
+ import type {
43
+ Listable,
44
+ Creatable,
45
+ Deletable
46
+ } from '../shared/application/data/capabilities.ts'
47
+ import type { Course } from '../domain/courses.ts'
48
+ import type { Student } from '../domain/students.ts'
49
+ import type { Inscription } from '../domain/inscriptions.ts'
50
+
51
+ type Generic = Record<string, unknown>
52
+
53
+ export type CourseData = ReturnType<Course['toJSON']>
54
+ export type StudentData = ReturnType<Student['toJSON']>
55
+ export type InscriptionData = ReturnType<Inscription['toJSON']>
56
+
57
+ export class InMemoryDatabaseManager
58
+ extends DataManager
59
+ implements Listable, Creatable, Deletable
60
+ {
61
+ [property: string]: unknown
62
+
63
+ constructor(private records: Generic[]) {
64
+ super()
65
+ }
66
+
67
+ public all(): Generic[] {
68
+ return this.records
69
+ }
70
+
71
+ public create(data: Generic): boolean {
72
+ this.records.push(data)
73
+ return true
74
+ }
75
+
76
+ public delete(selector: Partial<Generic>): boolean {
77
+ const before = this.records.length
78
+ this.records = this.records.filter((record) =>
79
+ Object.entries(selector).every(([key, value]) => record[key] !== value)
80
+ )
81
+ return this.records.length < before
82
+ }
83
+ }
84
+ ```
32
85
 
33
86
  #### Data Manager
34
87
 
35
- `DataManager` is responsible for exposing plain source data.
88
+ `DataManager` is the base contract for any data source. `DatasetManager`
89
+ extends it with set operations for contexts that need to combine collections.
36
90
 
37
91
  ```ts title="shared/application/data/managers.ts"
38
92
  export abstract class DataManager<T = Record<string, unknown>> {
39
- public none(): Array<T> {
40
- return []
41
- }
93
+ [property: string]: unknown
94
+ }
95
+
96
+ export abstract class DatasetManager<T = Record<string, unknown>> extends DataManager<T> {
97
+ [property: string]: unknown
42
98
 
43
- public abstract all(): Promise<Array<T>>
99
+ public abstract union(other: Array<T>): Array<T>
100
+ public abstract intersection(other: Array<T>): Array<T>
101
+ public abstract difference(other: Array<T>): Array<T>
102
+ public abstract symmetricDifference(other: Array<T>): Array<T>
103
+ public abstract complement(other: Array<T>): Array<T>
44
104
  }
45
105
  ```
46
106
 
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.
107
+ Extend `DataManager` to implement a concrete data source. The manager exposes
108
+ raw data without domain transformation. The `InMemoryDatabaseManager` above
109
+ extends `DataManager` and implements `Listable`, `Creatable`, and `Deletable`.
51
110
 
52
- #### Implementation
111
+ #### Driver Adapter
112
+
113
+ `DriverAdapter` is responsible for connecting to a data source and returning an
114
+ enabled `DataManager`.
53
115
 
54
- In the following example we implement an in-memory manager and its driver.
116
+ ```ts title="shared/application/data/drivers.ts"
117
+ export abstract class DriverAdapter<M extends DataManager = DataManager> {
118
+ [property: string]: unknown
55
119
 
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'
120
+ public abstract connect(...args: unknown[]): M
59
121
 
60
- type UserRecord = {
61
- id: string
62
- email: string
63
- active: boolean | null
122
+ public abstract disconnect(): unknown
64
123
  }
124
+ ```
65
125
 
66
- class MemoryUsersManager extends DataManager<UserRecord> {
67
- public constructor(private readonly rows: Array<UserRecord>) {
68
- super()
69
- }
126
+ Extend `DriverAdapter` to wrap a concrete data source. The driver connects to
127
+ the source, returns an enabled manager, and disconnects when the work is done.
70
128
 
71
- public async all(): Promise<Array<UserRecord>> {
72
- return this.rows
73
- }
74
- }
129
+ ```ts title="enrollment/application/database.ts"
130
+ import { DriverAdapter } from '../shared/application/data/drivers.ts'
131
+ import { InMemoryDatabaseManager } from './managers.ts'
132
+
133
+ type Generic = Record<string, unknown>
75
134
 
76
- export class MemoryUsersDriver extends DriverAdapter<MemoryUsersManager> {
77
- public constructor(private readonly rows: Array<UserRecord>) {
135
+ export type Database = Record<string, Generic[]>
136
+
137
+ export class InMemoryDatabaseDriver extends DriverAdapter<InMemoryDatabaseManager> {
138
+ [property: string]: unknown
139
+
140
+ private manager: InMemoryDatabaseManager | null = null
141
+
142
+ constructor(private readonly database: Database) {
78
143
  super()
79
144
  }
80
145
 
81
- public async connect(): Promise<MemoryUsersManager> {
82
- return new MemoryUsersManager(this.rows)
146
+ public connect(collectionKey: string): InMemoryDatabaseManager {
147
+ if (this.database[collectionKey] === undefined) {
148
+ this.database[collectionKey] = []
149
+ }
150
+
151
+ this.manager = new InMemoryDatabaseManager(this.database[collectionKey])
152
+ return this.manager
83
153
  }
84
154
 
85
- public async disconnect(): Promise<void> {
86
- return undefined
155
+ public disconnect(): void {
156
+ this.manager = null
87
157
  }
88
158
  }
89
159
  ```
90
160
 
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.
161
+ `InMemoryDatabaseDriver` owns the connection contract. `InMemoryDatabaseManager`
162
+ owns the raw records. The application layer can use both without knowing whether
163
+ the source is memory, SQL, or an HTTP-backed adapter.
94
164
 
95
- Put all your complex data operations in `DataManager`. `Repository` should only handle the transformation of raw records into domain representations. For example, if you need to relate users to their posts, implement that in a manager:
165
+ #### Repository
96
166
 
97
- ```ts
98
- class ComplexUsersManager extends DataManager<EnrichedUserRecord> {
99
- ...
167
+ `Repository` is responsible for transforming raw records into domain-oriented
168
+ representations. It holds a reference to the manager and requires `transform()`
169
+ to map a raw record into a domain entity.
100
170
 
101
- public async findAllAndRelate(): Promise<Array<EnrichedUserRecord>> {
102
- ...
103
- }
171
+ ```ts title="shared/application/data/repositories.ts"
172
+ export abstract class Repository<
173
+ RawDataShape = Generic,
174
+ EntityShape = Generic,
175
+ M extends DataManager<RawDataShape> = DataManager<RawDataShape>
176
+ > {
177
+ [property: string]: unknown
178
+
179
+ public constructor(public readonly manager: M) {}
180
+
181
+ protected abstract transform(data: RawDataShape, ...args: unknown[]): EntityShape
104
182
  }
105
183
  ```
106
184
 
107
- #### Repository
185
+ Data retrieval operations are not defined in the base class — add them
186
+ explicitly in the concrete class using the capability interfaces from
187
+ `capabilities.ts`.
108
188
 
109
- `Repository` is responsible for transforming raw records into domain-oriented
110
- representations.
189
+ ```ts title="enrollment/application/repositories.ts"
190
+ import { Repository } from '../shared/application/data/repositories.ts'
191
+ import { InMemoryDatabaseManager } from './managers.ts'
192
+ import type { CourseData, StudentData, InscriptionData } from './managers.ts'
193
+ import { Course } from '../domain/courses.ts'
194
+ import { Student } from '../domain/students.ts'
195
+ import { Inscription } from '../domain/inscriptions.ts'
111
196
 
112
- ```ts title="shared/application/data/repositories.ts"
113
- import { type DataManager } from './managers.js'
114
- import { type DriverAdapter } from './drivers.js'
197
+ export class CoursesRepository extends Repository<CourseData, Course, InMemoryDatabaseManager> {
198
+ [property: string]: unknown
115
199
 
116
- export abstract class Repository<
117
- DataShape extends Record<string, unknown> = Record<string, unknown>,
118
- EntityShape extends Record<string, unknown> = Record<string, unknown>
119
- > {
120
- public constructor(public readonly driver: DriverAdapter<DataManager<DataShape>>) {}
200
+ constructor(manager: InMemoryDatabaseManager) {
201
+ super(manager)
202
+ }
121
203
 
122
- public async all(): Promise<Array<EntityShape>> {
123
- const connection = await this.driver.connect()
124
- const raw = await connection.all()
125
- const entities = this.transformList(raw)
126
- await this.driver.disconnect()
127
- return entities
204
+ protected transform(data: CourseData): Course {
205
+ return new Course(data.name, data.description, data.duration_hours)
128
206
  }
129
207
 
130
- protected transformList(data: Array<DataShape>): Array<EntityShape> {
131
- return data.map(this.transform)
208
+ public create(course: Course): boolean {
209
+ return this.manager.create(course.toJSON())
132
210
  }
133
211
 
134
- protected abstract transform(data: DataShape): EntityShape
212
+ public delete(course: Course): boolean {
213
+ return this.manager.delete(course.toJSON())
214
+ }
135
215
  }
136
- ```
137
216
 
138
- The base repository already defines the `all()` flow. A concrete repository only
139
- needs to implement `transform()`.
217
+ export class StudentsRepository extends Repository<StudentData, Student, InMemoryDatabaseManager> {
218
+ [property: string]: unknown
140
219
 
141
- #### Repository Implementation
220
+ constructor(manager: InMemoryDatabaseManager) {
221
+ super(manager)
222
+ }
142
223
 
143
- Now that the driver exists, a repository can translate raw records into a shape
144
- that the rest of the context can use.
224
+ protected transform(data: StudentData): Student {
225
+ return new Student(data.name, data.email)
226
+ }
145
227
 
146
- ```ts title="users/adapters/users-repository.ts"
147
- import { Repository } from '../../shared/application/data/repositories.js'
148
- import { DriverAdapter } from '../../shared/application/data/drivers.js'
149
- import { DataManager } from '../../shared/application/data/managers.js'
228
+ public create(student: Student): boolean {
229
+ return this.manager.create(student.toJSON())
230
+ }
150
231
 
151
- type UserRecord = {
152
- id: string
153
- email: string
154
- active: boolean | null
232
+ public delete(student: Student): boolean {
233
+ return this.manager.delete(student.toJSON())
234
+ }
155
235
  }
156
236
 
157
- type UserView = {
158
- id: string
159
- email: string
160
- active: boolean | null
161
- }
237
+ export class InscriptionsRepository extends Repository<InscriptionData, Inscription, InMemoryDatabaseManager> {
238
+ [property: string]: unknown
162
239
 
163
- export class UsersRepository extends Repository<UserRecord, UserView> {
164
- public constructor(driver: DriverAdapter<DataManager<UserRecord>>) {
165
- super(driver)
240
+ constructor(manager: InMemoryDatabaseManager) {
241
+ super(manager)
166
242
  }
167
243
 
168
- protected transform(data: UserRecord): UserView {
169
- return {
170
- id: data.id,
171
- email: data.email,
172
- active: data.active,
173
- }
244
+ protected transform(data: InscriptionData, student: Student, course: Course): Inscription {
245
+ return new Inscription(student, course, data.enrolled_at)
174
246
  }
175
- }
176
- ```
177
247
 
178
- This repository does not own the connection lifecycle because `Repository`
179
- already handles it. Its responsibility is the mapping between raw source data
180
- and the representation used by the context.
248
+ public create(inscription: Inscription): boolean {
249
+ return this.manager.create(inscription.toJSON())
250
+ }
181
251
 
182
- > **Warning**
183
- > The generated `Repository` only implements `all()`. If the project needs
184
- > filtering, creation, or updates, add those operations explicitly instead of
185
- > assuming they already exist in the base class.
252
+ public delete(inscription: Inscription): boolean {
253
+ return this.manager.delete(inscription.toJSON())
254
+ }
255
+ }
256
+ ```
186
257
 
187
258
  #### Example Flow
188
259
 
@@ -191,8 +262,7 @@ The normal flow of the data abstractions is the following:
191
262
  ```mermaid
192
263
  flowchart LR
193
264
  service[Service] --> repository[Repository]
194
- repository --> driver[Driver]
195
- driver --> manager["Data manager"]
265
+ repository --> manager["Data manager"]
196
266
  manager --> raw["Raw records"]
197
267
  repository --> transformed["Transformed records"]
198
268
  transformed --> service
@@ -1,9 +1,8 @@
1
1
  ### Services
2
2
 
3
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.
4
+ collaborators to fulfill a system purpose. It defines the process of a use
5
+ case, not the business meaning of the domain objects involved in that use case.
7
6
 
8
7
  The generated template provides `Service` as a semantic base class for these
9
8
  processes.
@@ -13,111 +12,115 @@ processes.
13
12
  `Service` is an abstract class with no concrete behavior.
14
13
 
15
14
  ```ts title="shared/application/services.ts"
16
- export abstract class Service {}
15
+ export abstract class Service {
16
+ [property: string]: unknown
17
+ }
17
18
  ```
18
19
 
19
20
  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.
21
+ without imposing a method name, result shape, or framework-specific lifecycle.
22
+
23
+ #### Usage
24
+
25
+ In the following example we build services for a course enrollment context.
26
+ Each service receives its dependencies through the constructor and exposes
27
+ operations that the context ports can call.
28
+
29
+ ```ts title="enrollment/application/services.ts"
30
+ import { Service } from '../shared/application/services.ts'
31
+ import { InMemoryDatabaseManager } from './managers.ts'
32
+ import { CoursesRepository, StudentsRepository, InscriptionsRepository } from './repositories.ts'
33
+ import type { Course } from '../domain/courses.ts'
34
+ import type { Student } from '../domain/students.ts'
35
+ import type { Inscription } from '../domain/inscriptions.ts'
36
+
37
+ export class CoursesService extends Service {
38
+ [property: string]: unknown
39
+
40
+ constructor(
41
+ private readonly manager: InMemoryDatabaseManager,
42
+ private readonly repository: CoursesRepository
43
+ ) {
44
+ super()
45
+ }
46
+
47
+ public all() {
48
+ return this.manager.all()
49
+ }
50
+
51
+ public create(course: Course) {
52
+ return this.repository.create(course)
53
+ }
54
+
55
+ public delete(course: Course) {
56
+ return this.repository.delete(course)
57
+ }
58
+ }
22
59
 
23
- #### First Service
60
+ export class StudentsService extends Service {
61
+ [property: string]: unknown
24
62
 
25
- In the following example we build a small registration process.
63
+ constructor(
64
+ private readonly manager: InMemoryDatabaseManager,
65
+ private readonly repository: StudentsRepository
66
+ ) {
67
+ super()
68
+ }
26
69
 
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'
70
+ public all() {
71
+ return this.manager.all()
72
+ }
30
73
 
31
- type UserRecord = {
32
- id: string
33
- email: string
34
- active: boolean | null
35
- }
74
+ public create(student: Student) {
75
+ return this.repository.create(student)
76
+ }
36
77
 
37
- type SaveUser = {
38
- save(user: UserRecord): Promise<UserRecord>
78
+ public delete(student: Student) {
79
+ return this.repository.delete(student)
80
+ }
39
81
  }
40
82
 
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
- ```
83
+ export class InscriptionsService extends Service {
84
+ [property: string]: unknown
85
+
86
+ constructor(
87
+ private readonly manager: InMemoryDatabaseManager,
88
+ private readonly repository: InscriptionsRepository
89
+ ) {
90
+ super()
91
+ }
92
+
93
+ public all() {
94
+ return this.manager.all()
95
+ }
96
+
97
+ public create(inscription: Inscription) {
98
+ return this.repository.create(inscription)
99
+ }
62
100
 
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
+ public delete(inscription: Inscription) {
102
+ return this.repository.delete(inscription)
103
+ }
101
104
  }
102
105
  ```
103
106
 
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
+ Each service holds a reference to its manager and repository. The manager
108
+ provides raw list access; the repository delegates creation and deletion
109
+ through the domain entities.
107
110
 
108
111
  > **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
+ > `Service` does not guarantee a method name or a result contract. If the
113
+ > codebase needs those conventions, establish them explicitly in project code
114
+ > instead of assuming the generated base class already provides them.
112
115
 
113
116
  #### Example Flow
114
117
 
115
118
  ```mermaid
116
119
  flowchart LR
117
- input[Input] --> service[Service]
118
- service --> domain["Domain capabilities"]
119
- domain --> collaborators[Collaborators]
120
- collaborators --> result[Result]
120
+ port[Port] --> service[Service]
121
+ service --> repository[Repository]
122
+ service --> manager["Data manager"]
123
+ repository --> domain["Domain entities"]
121
124
  ```
122
125
 
123
126
  This flow keeps orchestration in the application layer and domain meaning in
@@ -11,148 +11,39 @@ The generated template provides `Aggregate` as a semantic base class.
11
11
  `Aggregate` is an abstract class with no concrete methods.
12
12
 
13
13
  ```ts title="shared/domain/aggregates.ts"
14
- export abstract class Aggregate {}
14
+ export abstract class Aggregate {
15
+ [property: string]: unknown
16
+ }
15
17
  ```
16
18
 
17
19
  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.
22
-
23
- #### First Aggregate
24
-
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.
27
-
28
- ```ts title="sales/domain/sale.ts"
29
- import { Aggregate } from '../../shared/domain/aggregates.js'
30
- import { Entity } from '../../shared/domain/entities.js'
31
-
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
- }
20
+ an aggregate whose concrete implementation gathers the involved entities and
21
+ exposes operations that span more than one of them.
41
22
 
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
- }
23
+ #### Usage
63
24
 
64
- class Order extends Entity {
65
- public readonly id: string
66
- public readonly products: Product[]
25
+ In the following example we model an enrollment aggregate that groups a
26
+ student and a course to produce an inscription with the current date.
67
27
 
68
- public constructor(id: string, products: Product[]) {
69
- super()
70
- this.id = id
71
- this.products = products
72
- }
28
+ ```ts title="enrollment/domain/inscriptions.ts"
29
+ import { Aggregate } from '../shared/domain/aggregates.ts'
30
+ import { Course } from './courses.ts'
31
+ import { Student } from './students.ts'
32
+ import { Inscription } from './inscriptions.ts'
73
33
 
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
-
86
- public constructor(
87
- id: string,
88
- order: Order,
89
- buyer: Person,
90
- seller: Person,
91
- ) {
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
- )
101
- }
102
-
103
- public equals(other: Entity): boolean {
104
- return other instanceof Invoice && this.id === other.id
105
- }
106
- }
34
+ export class InscriptionAggregate extends Aggregate {
35
+ [property: string]: unknown
107
36
 
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
- ) {
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 }
138
- }
139
-
140
- public getInvoice(): Invoice | null {
141
- return this.invoice
142
- }
37
+ public static enroll(student: Student, course: Course): Inscription {
38
+ const currentDate = new Date()
39
+ return new Inscription(student, course, currentDate)
40
+ }
143
41
  }
144
42
  ```
145
43
 
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.
44
+ `InscriptionAggregate` centralizes the rule for creating an inscription. The
45
+ enrollment date is set automatically, so callers do not pass it directly and
46
+ the rule stays in one place.
156
47
 
157
48
  #### Responsibility Boundary
158
49
 
@@ -1,9 +1,8 @@
1
1
  ### Entities
2
2
 
3
3
  An entity is responsible for representing a domain concept with its own
4
- identity.
5
- Two entity instances refer to the same conceptual element when they share that
6
- identity, even if other attributes change over time.
4
+ identity. Two entity instances refer to the same conceptual element when they
5
+ share that identity, even if other attributes change over time.
7
6
 
8
7
  The generated template provides `Entity` as a base class for this pattern.
9
8
 
@@ -13,73 +12,119 @@ The generated template provides `Entity` as a base class for this pattern.
13
12
 
14
13
  ```ts title="shared/domain/entities.ts"
15
14
  export abstract class Entity {
16
- public abstract equals(other: Entity): boolean
15
+ [property: string]: unknown
17
16
 
18
- public toJSON(): Record<string, unknown> {
19
- return this
20
- }
17
+ public abstract equals(other: Entity): boolean
21
18
 
22
- public toString(): string {
23
- return String(this.constructor.name)
24
- }
19
+ public toJSON(): Record<string, unknown> {
20
+ return this
21
+ }
22
+
23
+ public toString(): string {
24
+ return this.constructor.name
25
+ }
25
26
  }
26
27
  ```
27
28
 
28
- Every concrete entity must implement `equals()`. The generated base class also
29
- provides `toJSON()` and `toString()`.
30
-
31
- #### First Entity
32
-
33
- In the following example we model a user entity.
29
+ Every concrete entity must implement `equals()`. Override `toJSON()` to control
30
+ the plain representation returned when the entity is serialized.
34
31
 
35
- ```ts title="users/domain/user.ts"
36
- import { Entity } from '../../shared/domain/entities.js'
37
- import { Email } from '../../shared/domain/value-objects.js'
32
+ #### Usage
38
33
 
39
- export class User extends Entity {
40
- public constructor(
41
- public readonly id: string,
42
- public readonly email: Email,
43
- ) {
44
- super()
45
- }
34
+ In the following example we model the entities of a course enrollment context.
46
35
 
47
- public equals(other: Entity): boolean {
48
- return other instanceof User && this.id === other.id
49
- }
50
- }
51
- ```
36
+ ```ts title="enrollment/domain/students.ts"
37
+ import { Entity } from '../shared/domain/entities.ts'
52
38
 
53
- The identity rule lives in `equals()`. The entity compares the `id`, not the
54
- email address, because the email can change while the entity still represents
55
- the same user.
39
+ export class Student extends Entity {
40
+ [property: string]: unknown
56
41
 
57
- #### Included Behavior
42
+ constructor(
43
+ public name: string,
44
+ public email: string
45
+ ) {
46
+ super()
47
+ }
58
48
 
59
- Now that the entity exists, the inherited helpers become useful.
49
+ public equals(other: Student): boolean {
50
+ return this.email === other.email
51
+ }
60
52
 
61
- ```ts
62
- const user = new User('usr_1', Email.from('ada@example.com'))
53
+ public override toJSON() {
54
+ return {
55
+ name: this.name,
56
+ email: this.email
57
+ }
58
+ }
59
+ }
60
+ ```
63
61
 
64
- user.toString()
65
- user.toJSON()
62
+ ```ts title="enrollment/domain/courses.ts"
63
+ import { Entity } from '../shared/domain/entities.ts'
64
+
65
+ export class Course extends Entity {
66
+ [property: string]: unknown
67
+
68
+ constructor(
69
+ public name: string,
70
+ public description: string,
71
+ public duration_hours: number
72
+ ) {
73
+ super()
74
+ }
75
+
76
+ public equals(other: Course): boolean {
77
+ return this.name === other.name
78
+ }
79
+
80
+ public override toJSON() {
81
+ return {
82
+ name: this.name,
83
+ description: this.description,
84
+ duration_hours: this.duration_hours
85
+ }
86
+ }
87
+ }
66
88
  ```
67
89
 
68
- `toString()` returns the constructor name. `toJSON()` returns the instance as a
69
- plain record, which is convenient for simple serialization or inspection.
90
+ ```ts title="enrollment/domain/inscriptions.ts"
91
+ import { Entity } from '../shared/domain/entities.ts'
92
+ import { Course } from './courses.ts'
93
+ import { Student } from './students.ts'
94
+
95
+ export class Inscription extends Entity {
96
+ [property: string]: unknown
97
+
98
+ constructor(
99
+ public readonly student: Student,
100
+ public readonly course: Course,
101
+ public readonly enrolled_at: Date
102
+ ) {
103
+ super()
104
+ }
105
+
106
+ public equals(other: Inscription): boolean {
107
+ return this.student.equals(other.student) && this.course.equals(other.course)
108
+ }
109
+
110
+ public override toJSON() {
111
+ return {
112
+ student: this.student.toJSON(),
113
+ course: this.course.toJSON(),
114
+ enrolled_at: this.enrolled_at
115
+ }
116
+ }
117
+ }
118
+ ```
70
119
 
71
120
  #### Equality Rules
72
121
 
73
122
  The most important design decision in an entity is the identity comparison.
74
123
 
75
- Correct identity candidates usually include:
76
-
77
- 1. a stable identifier;
78
- 2. a natural domain key that does not drift over time;
79
- 3. a combination of fields that the domain treats as unique.
80
-
81
- Changing fields that are not part of the identity should not make `equals()`
82
- return `false` for the same conceptual entity.
124
+ `Student` uses `email` as the identity because two students with the same email
125
+ represent the same person. `Course` uses `name`. `Inscription` combines the
126
+ student and course identities — two inscriptions are the same when both the
127
+ student and the course match.
83
128
 
84
129
  > **Hint**
85
130
  > Keep `equals()` explicit and small. If the comparison starts depending on many
@@ -88,4 +133,4 @@ return `false` for the same conceptual entity.
88
133
  #### Next Step
89
134
 
90
135
  When the identity of a concept is determined entirely by its value, use a value
91
- object instead. The generated abstraction is documented in `value-objects.md`.
136
+ object instead. The generated abstraction is documented in `value-objects.md`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tshex-cli",
3
- "version": "1.0.29",
3
+ "version": "1.0.30",
4
4
  "author": "https://github.com/virtualitems/",
5
5
  "license": "MIT",
6
6
  "description": "Typescript Hexagonal Architecture CLI",
@@ -1,49 +1,53 @@
1
1
  type Generic = Record<string, unknown>
2
2
 
3
- export interface Listable {
4
- all(): Generic | Promise<Generic[]>
3
+ export interface Listable<DataShape extends Generic = Generic> {
4
+ all(): DataShape[]
5
5
  }
6
6
 
7
7
  /**
8
8
  * @description Declares a filtering operation over plain source records.
9
9
  */
10
- export interface Filterable {
11
- filter(selector: unknown): Generic | Promise<Generic[]>
10
+ export interface Filterable<DataShape extends Generic = Generic, Selector = unknown> {
11
+ filter(selector: Selector): DataShape[]
12
12
  }
13
13
 
14
14
  /**
15
15
  * @description Declares a sorting operation over plain source records.
16
16
  */
17
- export interface Sortable {
18
- sort(selector: unknown): Generic | Promise<Generic[]>
17
+ export interface Sortable<DataShape extends Generic = Generic, Selector = unknown> {
18
+ sort(selector: Selector): DataShape[]
19
19
  }
20
20
 
21
21
  /**
22
22
  * @description Declares a creation operation for plain source records.
23
23
  */
24
- export interface Creatable {
25
- create(data: unknown): unknown
24
+ export interface Creatable<DataShape extends Generic = Generic, Feedback = unknown> {
25
+ create(data: DataShape): Feedback
26
26
  }
27
27
 
28
28
  /**
29
29
  * @description Declares an update operation that selects source records and applies new plain data.
30
30
  */
31
- export interface Updatable {
32
- update(selector: unknown, data: unknown): unknown
31
+ export interface Updatable<
32
+ DataShape extends Generic = Generic,
33
+ Selector = unknown,
34
+ Feedback = unknown
35
+ > {
36
+ update(selector: Selector, data: Partial<DataShape>): Feedback
33
37
  }
34
38
 
35
39
  /**
36
40
  * @description Declares a deletion operation over source records selected by plain criteria.
37
41
  */
38
- export interface Deletable {
39
- delete(selector: unknown): unknown
42
+ export interface Deletable<Selector = unknown, Feedback = unknown> {
43
+ delete(selector: Selector): Feedback
40
44
  }
41
45
 
42
46
  /**
43
47
  * @description Declares an aggregation operation over source records.
44
48
  */
45
- export interface Aggregatable {
46
- aggregate(selector: unknown): unknown
49
+ export interface Aggregatable<Selector = unknown, Feedback = unknown> {
50
+ aggregate(selector: Selector): Feedback
47
51
  }
48
52
 
49
53
  /**
@@ -9,7 +9,7 @@ import { DataManager } from './managers.js'
9
9
  export abstract class DriverAdapter<M extends DataManager = DataManager> {
10
10
  [property: string]: unknown
11
11
 
12
- public abstract connect(...args: unknown[]): Promise<M>
12
+ public abstract connect(...args: unknown[]): M
13
13
 
14
- public abstract disconnect(): Promise<unknown>
14
+ public abstract disconnect(): unknown
15
15
  } //:: class
@@ -12,13 +12,13 @@ export abstract class DataManager<T = Record<string, unknown>> {
12
12
  export abstract class DatasetManager<T = Record<string, unknown>> extends DataManager<T> {
13
13
  [property: string]: unknown
14
14
 
15
- public abstract union(other: Array<T>): Promise<Array<T>>
15
+ public abstract union(other: Array<T>): Array<T>
16
16
 
17
- public abstract intersection(other: Array<T>): Promise<Array<T>>
17
+ public abstract intersection(other: Array<T>): Array<T>
18
18
 
19
- public abstract difference(other: Array<T>): Promise<Array<T>>
19
+ public abstract difference(other: Array<T>): Array<T>
20
20
 
21
- public abstract symmetricDifference(other: Array<T>): Promise<Array<T>>
21
+ public abstract symmetricDifference(other: Array<T>): Array<T>
22
22
 
23
- public abstract complement(other: Array<T>): Promise<Array<T>>
23
+ public abstract complement(other: Array<T>): Array<T>
24
24
  } //:: class
@@ -1,5 +1,4 @@
1
1
  import { type DataManager } from './managers.js'
2
- import { type DriverAdapter } from './drivers.js'
3
2
 
4
3
  type Generic = Record<string, unknown>
5
4
 
@@ -7,12 +6,12 @@ type Generic = Record<string, unknown>
7
6
  * @description Acts as an intermediary between plain source data and domain objects.
8
7
  * It transforms records into domain representations and can translate them back when needed.
9
8
  */
10
- export abstract class Repository<RawDataShape = Generic, EntityShape = Generic> {
9
+ export abstract class Repository<RawDataShape = Generic, EntityShape = Generic, M extends DataManager<RawDataShape> = DataManager<RawDataShape>> {
11
10
  [property: string]: unknown
12
11
 
13
12
  public constructor(
14
- public readonly driver: DriverAdapter<DataManager<RawDataShape>>
13
+ public readonly manager: M
15
14
  ) {}
16
15
 
17
- protected abstract transform(data: RawDataShape): EntityShape
16
+ protected abstract transform(data: RawDataShape, ...args: unknown[]): EntityShape
18
17
  } //:: class
@@ -17,7 +17,7 @@ export abstract class Event {
17
17
  export abstract class EventHandler {
18
18
  [property: string]: unknown
19
19
 
20
- public abstract handle(event: Event): Promise<void>
20
+ public abstract handle(event: Event): void
21
21
  } //:: class
22
22
 
23
23
  /**
@@ -2,6 +2,8 @@
2
2
  * @description HTTP error with a specific status code and message.
3
3
  */
4
4
  export class HttpError extends Error {
5
+ [property: string]: unknown
6
+
5
7
  public static readonly messages: { [code: number]: string } = Object.freeze({
6
8
  400: 'Bad Request',
7
9
  401: 'Unauthorized',