tshex-cli 1.0.29 → 1.0.31

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,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,121 @@ 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'
38
+ import { Email } from '../shared/domain/value-objects.ts'
52
39
 
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.
40
+ export class Student extends Entity {
41
+ [property: string]: unknown
56
42
 
57
- #### Included Behavior
43
+ constructor(
44
+ public name: string,
45
+ public email: Email
46
+ ) {
47
+ super()
48
+ }
58
49
 
59
- Now that the entity exists, the inherited helpers become useful.
50
+ public equals(other: Student): boolean {
51
+ return this.email.equals(other.email)
52
+ }
60
53
 
61
- ```ts
62
- const user = new User('usr_1', Email.from('ada@example.com'))
54
+ public override toJSON() {
55
+ return {
56
+ name: this.name,
57
+ email: this.email.value
58
+ }
59
+ }
60
+ }
61
+ ```
63
62
 
64
- user.toString()
65
- user.toJSON()
63
+ ```ts title="enrollment/domain/courses.ts"
64
+ import { Entity } from '../shared/domain/entities.ts'
65
+
66
+ export class Course extends Entity {
67
+ [property: string]: unknown
68
+
69
+ constructor(
70
+ public name: string,
71
+ public description: string,
72
+ public duration_hours: number
73
+ ) {
74
+ super()
75
+ }
76
+
77
+ public equals(other: Course): boolean {
78
+ return this.name === other.name
79
+ }
80
+
81
+ public override toJSON() {
82
+ return {
83
+ name: this.name,
84
+ description: this.description,
85
+ duration_hours: this.duration_hours
86
+ }
87
+ }
88
+ }
66
89
  ```
67
90
 
68
- `toString()` returns the constructor name. `toJSON()` returns the instance as a
69
- plain record, which is convenient for simple serialization or inspection.
91
+ ```ts title="enrollment/domain/inscriptions.ts"
92
+ import { Entity } from '../shared/domain/entities.ts'
93
+ import { Course } from './courses.ts'
94
+ import { Student } from './students.ts'
95
+
96
+ export class Inscription extends Entity {
97
+ [property: string]: unknown
98
+
99
+ constructor(
100
+ public readonly student: Student,
101
+ public readonly course: Course,
102
+ public readonly enrolled_at: Date
103
+ ) {
104
+ super()
105
+ }
106
+
107
+ public equals(other: Inscription): boolean {
108
+ return this.student.equals(other.student) && this.course.equals(other.course)
109
+ }
110
+
111
+ public override toJSON() {
112
+ return {
113
+ student: this.student.toJSON(),
114
+ course: this.course.toJSON(),
115
+ enrolled_at: this.enrolled_at
116
+ }
117
+ }
118
+ }
119
+ ```
70
120
 
71
121
  #### Equality Rules
72
122
 
73
123
  The most important design decision in an entity is the identity comparison.
74
124
 
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.
125
+ `Student` uses `Email` as the identity because two students with the same
126
+ address represent the same person. `equals()` delegates to `Email.equals()` so
127
+ the comparison rule lives in the value object. `Course` uses `name`.
128
+ `Inscription` combines the student and course identities two inscriptions are
129
+ the same when both the student and the course match.
83
130
 
84
131
  > **Hint**
85
132
  > Keep `equals()` explicit and small. If the comparison starts depending on many
@@ -88,4 +135,4 @@ return `false` for the same conceptual entity.
88
135
  #### Next Step
89
136
 
90
137
  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`.
138
+ 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.31",
4
4
  "author": "https://github.com/virtualitems/",
5
5
  "license": "MIT",
6
6
  "description": "Typescript Hexagonal Architecture CLI",
package/readme.md CHANGED
@@ -86,16 +86,13 @@ core/
86
86
  |-- shared/
87
87
  | |-- application/
88
88
  | | |-- data/
89
+ | | | |-- capabilities.ts
89
90
  | | | |-- drivers.ts
90
91
  | | | |-- managers.ts
91
92
  | | | `-- repositories.ts
92
93
  | | |-- events.ts
93
94
  | | |-- http/
94
- | | | |-- errors.ts
95
- | | | |-- handlers.ts
96
- | | | |-- json-api.ts
97
- | | | |-- json-web-token.ts
98
- | | | `-- opengraph.ts
95
+ | | | `-- errors.ts
99
96
  | | |-- loggers.ts
100
97
  | | |-- services.ts
101
98
  | | `-- validations.ts
@@ -248,10 +245,6 @@ From this point on, the guide is split into dedicated documents under `docs/`.
248
245
  - [shared/application/data](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/application/data.md)
249
246
  - [shared/application/events.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/application/events.md)
250
247
  - [shared/application/http/errors.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/application/http/errors.md)
251
- - [shared/application/http/handlers.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/application/http/handlers.md)
252
- - [shared/application/http/json-api.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/application/http/json-api.md)
253
- - [shared/application/http/json-web-token.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/application/http/json-web-token.md)
254
- - [shared/application/http/opengraph.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/application/http/opengraph.md)
255
248
  - [shared/application/loggers.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/application/loggers.md)
256
249
  - [shared/application/services.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/application/services.md)
257
250
  - [shared/application/validations.ts](https://github.com/virtualitems/tshex-cli/blob/main/docs/shared/application/validations.md)
@@ -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
  /**