tshex-cli 1.0.28 → 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.
@@ -1,176 +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
+ }
42
95
 
43
- public abstract all(): Promise<Array<T>>
96
+ export abstract class DatasetManager<T = Record<string, unknown>> extends DataManager<T> {
97
+ [property: string]: unknown
98
+
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
- #### First Implementation
111
+ #### Driver Adapter
53
112
 
54
- In the following example we implement an in-memory manager and its driver.
113
+ `DriverAdapter` is responsible for connecting to a data source and returning an
114
+ enabled `DataManager`.
55
115
 
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'
116
+ ```ts title="shared/application/data/drivers.ts"
117
+ export abstract class DriverAdapter<M extends DataManager = DataManager> {
118
+ [property: string]: unknown
59
119
 
60
- type UserRecord = {
61
- id: string
62
- email: string
63
- active: boolean | null
120
+ public abstract connect(...args: unknown[]): M
121
+
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>
134
+
135
+ export type Database = Record<string, Generic[]>
136
+
137
+ export class InMemoryDatabaseDriver extends DriverAdapter<InMemoryDatabaseManager> {
138
+ [property: string]: unknown
75
139
 
76
- export class MemoryUsersDriver extends DriverAdapter<MemoryUsersManager> {
77
- public constructor(private readonly rows: Array<UserRecord>) {
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
165
  #### Repository
96
166
 
97
167
  `Repository` is responsible for transforming raw records into domain-oriented
98
- representations.
168
+ representations. It holds a reference to the manager and requires `transform()`
169
+ to map a raw record into a domain entity.
99
170
 
100
171
  ```ts title="shared/application/data/repositories.ts"
101
- import { type DataManager } from './managers.js'
102
- import { type DriverAdapter } from './drivers.js'
103
-
104
172
  export abstract class Repository<
105
- DataShape extends Record<string, unknown> = Record<string, unknown>,
106
- EntityShape extends Record<string, unknown> = Record<string, unknown>
173
+ RawDataShape = Generic,
174
+ EntityShape = Generic,
175
+ M extends DataManager<RawDataShape> = DataManager<RawDataShape>
107
176
  > {
108
- public constructor(public readonly driver: DriverAdapter<DataManager<DataShape>>) {}
177
+ [property: string]: unknown
109
178
 
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
179
+ public constructor(public readonly manager: M) {}
180
+
181
+ protected abstract transform(data: RawDataShape, ...args: unknown[]): EntityShape
182
+ }
183
+ ```
184
+
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`.
188
+
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'
196
+
197
+ export class CoursesRepository extends Repository<CourseData, Course, InMemoryDatabaseManager> {
198
+ [property: string]: unknown
199
+
200
+ constructor(manager: InMemoryDatabaseManager) {
201
+ super(manager)
116
202
  }
117
203
 
118
- protected transformList(data: Array<DataShape>): Array<EntityShape> {
119
- return data.map(this.transform)
204
+ protected transform(data: CourseData): Course {
205
+ return new Course(data.name, data.description, data.duration_hours)
120
206
  }
121
207
 
122
- protected abstract transform(data: DataShape): EntityShape
208
+ public create(course: Course): boolean {
209
+ return this.manager.create(course.toJSON())
210
+ }
211
+
212
+ public delete(course: Course): boolean {
213
+ return this.manager.delete(course.toJSON())
214
+ }
123
215
  }
124
- ```
125
216
 
126
- The base repository already defines the `all()` flow. A concrete repository only
127
- needs to implement `transform()`.
217
+ export class StudentsRepository extends Repository<StudentData, Student, InMemoryDatabaseManager> {
218
+ [property: string]: unknown
128
219
 
129
- #### Repository Implementation
220
+ constructor(manager: InMemoryDatabaseManager) {
221
+ super(manager)
222
+ }
130
223
 
131
- Now that the driver exists, a repository can translate raw records into a shape
132
- that the rest of the context can use.
224
+ protected transform(data: StudentData): Student {
225
+ return new Student(data.name, data.email)
226
+ }
133
227
 
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'
228
+ public create(student: Student): boolean {
229
+ return this.manager.create(student.toJSON())
230
+ }
138
231
 
139
- type UserRecord = {
140
- id: string
141
- email: string
142
- active: boolean | null
232
+ public delete(student: Student): boolean {
233
+ return this.manager.delete(student.toJSON())
234
+ }
143
235
  }
144
236
 
145
- type UserView = {
146
- id: string
147
- email: string
148
- active: boolean | null
149
- }
237
+ export class InscriptionsRepository extends Repository<InscriptionData, Inscription, InMemoryDatabaseManager> {
238
+ [property: string]: unknown
150
239
 
151
- export class UsersRepository extends Repository<UserRecord, UserView> {
152
- public constructor(driver: DriverAdapter<DataManager<UserRecord>>) {
153
- super(driver)
240
+ constructor(manager: InMemoryDatabaseManager) {
241
+ super(manager)
154
242
  }
155
243
 
156
- protected transform(data: UserRecord): UserView {
157
- return {
158
- id: data.id,
159
- email: data.email,
160
- active: data.active,
161
- }
244
+ protected transform(data: InscriptionData, student: Student, course: Course): Inscription {
245
+ return new Inscription(student, course, data.enrolled_at)
162
246
  }
163
- }
164
- ```
165
247
 
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.
248
+ public create(inscription: Inscription): boolean {
249
+ return this.manager.create(inscription.toJSON())
250
+ }
169
251
 
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.
252
+ public delete(inscription: Inscription): boolean {
253
+ return this.manager.delete(inscription.toJSON())
254
+ }
255
+ }
256
+ ```
174
257
 
175
258
  #### Example Flow
176
259
 
@@ -179,8 +262,7 @@ The normal flow of the data abstractions is the following:
179
262
  ```mermaid
180
263
  flowchart LR
181
264
  service[Service] --> repository[Repository]
182
- repository --> driver[Driver]
183
- driver --> manager["Data manager"]
265
+ repository --> manager["Data manager"]
184
266
  manager --> raw["Raw records"]
185
267
  repository --> transformed["Transformed records"]
186
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