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.
- package/docs/context-ports.md +66 -28
- package/docs/generated-file-reference.md +2 -1
- package/docs/library-structure.md +22 -6
- package/docs/shared/application/data.md +187 -117
- package/docs/shared/application/services.md +90 -87
- package/docs/shared/domain/aggregates.md +22 -131
- package/docs/shared/domain/entities.md +96 -51
- package/package.json +1 -1
- package/templates/lib/shared/application/data/capabilities.ts +18 -14
- package/templates/lib/shared/application/data/drivers.ts +2 -2
- package/templates/lib/shared/application/data/managers.ts +5 -5
- package/templates/lib/shared/application/data/repositories.ts +3 -4
- package/templates/lib/shared/application/events.ts +1 -1
- package/templates/lib/shared/application/http/errors.ts +2 -0
package/docs/context-ports.md
CHANGED
|
@@ -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="
|
|
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
|
-
|
|
42
|
+
managing course enrollments.
|
|
43
43
|
|
|
44
|
-
```ts title="
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
73
|
+
public listInscriptions() {
|
|
74
|
+
const result = this.driver.connect('inscriptions').all()
|
|
75
|
+
this.driver.disconnect()
|
|
76
|
+
return result
|
|
77
|
+
}
|
|
61
78
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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="
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
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 -->
|
|
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"] -->
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
8
|
+
The generated structure splits this concern into four files:
|
|
10
9
|
|
|
11
|
-
1. `
|
|
12
|
-
2. `
|
|
13
|
-
3. `
|
|
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
|
-
####
|
|
15
|
+
#### Capabilities
|
|
16
16
|
|
|
17
|
-
`
|
|
18
|
-
|
|
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/
|
|
21
|
-
|
|
20
|
+
```ts title="shared/application/data/capabilities.ts"
|
|
21
|
+
export interface Listable<DataShape extends Generic = Generic> {
|
|
22
|
+
all(): DataShape[]
|
|
23
|
+
}
|
|
22
24
|
|
|
23
|
-
export
|
|
24
|
-
|
|
25
|
+
export interface Creatable<DataShape extends Generic = Generic, Feedback = unknown> {
|
|
26
|
+
create(data: DataShape): Feedback
|
|
27
|
+
}
|
|
25
28
|
|
|
26
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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
|
|
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
|
-
|
|
40
|
-
|
|
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
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
####
|
|
111
|
+
#### Driver Adapter
|
|
112
|
+
|
|
113
|
+
`DriverAdapter` is responsible for connecting to a data source and returning an
|
|
114
|
+
enabled `DataManager`.
|
|
53
115
|
|
|
54
|
-
|
|
116
|
+
```ts title="shared/application/data/drivers.ts"
|
|
117
|
+
export abstract class DriverAdapter<M extends DataManager = DataManager> {
|
|
118
|
+
[property: string]: unknown
|
|
55
119
|
|
|
56
|
-
|
|
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
|
-
|
|
61
|
-
id: string
|
|
62
|
-
email: string
|
|
63
|
-
active: boolean | null
|
|
122
|
+
public abstract disconnect(): unknown
|
|
64
123
|
}
|
|
124
|
+
```
|
|
65
125
|
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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
|
|
77
|
-
|
|
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
|
|
82
|
-
|
|
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
|
|
86
|
-
|
|
155
|
+
public disconnect(): void {
|
|
156
|
+
this.manager = null
|
|
87
157
|
}
|
|
88
158
|
}
|
|
89
159
|
```
|
|
90
160
|
|
|
91
|
-
`
|
|
92
|
-
raw records. The application layer can use both without knowing whether
|
|
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
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
import { type DriverAdapter } from './drivers.js'
|
|
197
|
+
export class CoursesRepository extends Repository<CourseData, Course, InMemoryDatabaseManager> {
|
|
198
|
+
[property: string]: unknown
|
|
115
199
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
> {
|
|
120
|
-
public constructor(public readonly driver: DriverAdapter<DataManager<DataShape>>) {}
|
|
200
|
+
constructor(manager: InMemoryDatabaseManager) {
|
|
201
|
+
super(manager)
|
|
202
|
+
}
|
|
121
203
|
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
131
|
-
return
|
|
208
|
+
public create(course: Course): boolean {
|
|
209
|
+
return this.manager.create(course.toJSON())
|
|
132
210
|
}
|
|
133
211
|
|
|
134
|
-
|
|
212
|
+
public delete(course: Course): boolean {
|
|
213
|
+
return this.manager.delete(course.toJSON())
|
|
214
|
+
}
|
|
135
215
|
}
|
|
136
|
-
```
|
|
137
216
|
|
|
138
|
-
|
|
139
|
-
|
|
217
|
+
export class StudentsRepository extends Repository<StudentData, Student, InMemoryDatabaseManager> {
|
|
218
|
+
[property: string]: unknown
|
|
140
219
|
|
|
141
|
-
|
|
220
|
+
constructor(manager: InMemoryDatabaseManager) {
|
|
221
|
+
super(manager)
|
|
222
|
+
}
|
|
142
223
|
|
|
143
|
-
|
|
144
|
-
|
|
224
|
+
protected transform(data: StudentData): Student {
|
|
225
|
+
return new Student(data.name, data.email)
|
|
226
|
+
}
|
|
145
227
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
active: boolean | null
|
|
232
|
+
public delete(student: Student): boolean {
|
|
233
|
+
return this.manager.delete(student.toJSON())
|
|
234
|
+
}
|
|
155
235
|
}
|
|
156
236
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
email: string
|
|
160
|
-
active: boolean | null
|
|
161
|
-
}
|
|
237
|
+
export class InscriptionsRepository extends Repository<InscriptionData, Inscription, InMemoryDatabaseManager> {
|
|
238
|
+
[property: string]: unknown
|
|
162
239
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
super(driver)
|
|
240
|
+
constructor(manager: InMemoryDatabaseManager) {
|
|
241
|
+
super(manager)
|
|
166
242
|
}
|
|
167
243
|
|
|
168
|
-
protected transform(data:
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
248
|
+
public create(inscription: Inscription): boolean {
|
|
249
|
+
return this.manager.create(inscription.toJSON())
|
|
250
|
+
}
|
|
181
251
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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 -->
|
|
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
|
-
|
|
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
|
|
21
|
-
|
|
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
|
-
|
|
60
|
+
export class StudentsService extends Service {
|
|
61
|
+
[property: string]: unknown
|
|
24
62
|
|
|
25
|
-
|
|
63
|
+
constructor(
|
|
64
|
+
private readonly manager: InMemoryDatabaseManager,
|
|
65
|
+
private readonly repository: StudentsRepository
|
|
66
|
+
) {
|
|
67
|
+
super()
|
|
68
|
+
}
|
|
26
69
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
70
|
+
public all() {
|
|
71
|
+
return this.manager.all()
|
|
72
|
+
}
|
|
30
73
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
active: boolean | null
|
|
35
|
-
}
|
|
74
|
+
public create(student: Student) {
|
|
75
|
+
return this.repository.create(student)
|
|
76
|
+
}
|
|
36
77
|
|
|
37
|
-
|
|
38
|
-
|
|
78
|
+
public delete(student: Student) {
|
|
79
|
+
return this.repository.delete(student)
|
|
80
|
+
}
|
|
39
81
|
}
|
|
40
82
|
|
|
41
|
-
export class
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
110
|
-
> needs those conventions, establish them explicitly in project code
|
|
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
|
-
|
|
118
|
-
service -->
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
75
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
`
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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
|
-
|
|
15
|
+
[property: string]: unknown
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
return this
|
|
20
|
-
}
|
|
17
|
+
public abstract equals(other: Entity): boolean
|
|
21
18
|
|
|
22
|
-
|
|
23
|
-
|
|
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()`.
|
|
29
|
-
|
|
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
|
-
|
|
36
|
-
import { Entity } from '../../shared/domain/entities.js'
|
|
37
|
-
import { Email } from '../../shared/domain/value-objects.js'
|
|
32
|
+
#### Usage
|
|
38
33
|
|
|
39
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
```
|
|
36
|
+
```ts title="enrollment/domain/students.ts"
|
|
37
|
+
import { Entity } from '../shared/domain/entities.ts'
|
|
52
38
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
the same user.
|
|
39
|
+
export class Student extends Entity {
|
|
40
|
+
[property: string]: unknown
|
|
56
41
|
|
|
57
|
-
|
|
42
|
+
constructor(
|
|
43
|
+
public name: string,
|
|
44
|
+
public email: string
|
|
45
|
+
) {
|
|
46
|
+
super()
|
|
47
|
+
}
|
|
58
48
|
|
|
59
|
-
|
|
49
|
+
public equals(other: Student): boolean {
|
|
50
|
+
return this.email === other.email
|
|
51
|
+
}
|
|
60
52
|
|
|
61
|
-
|
|
62
|
-
|
|
53
|
+
public override toJSON() {
|
|
54
|
+
return {
|
|
55
|
+
name: this.name,
|
|
56
|
+
email: this.email
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
63
61
|
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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,49 +1,53 @@
|
|
|
1
1
|
type Generic = Record<string, unknown>
|
|
2
2
|
|
|
3
|
-
export interface Listable {
|
|
4
|
-
all():
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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[]):
|
|
12
|
+
public abstract connect(...args: unknown[]): M
|
|
13
13
|
|
|
14
|
-
public abstract disconnect():
|
|
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>):
|
|
15
|
+
public abstract union(other: Array<T>): Array<T>
|
|
16
16
|
|
|
17
|
-
public abstract intersection(other: Array<T>):
|
|
17
|
+
public abstract intersection(other: Array<T>): Array<T>
|
|
18
18
|
|
|
19
|
-
public abstract difference(other: Array<T>):
|
|
19
|
+
public abstract difference(other: Array<T>): Array<T>
|
|
20
20
|
|
|
21
|
-
public abstract symmetricDifference(other: Array<T>):
|
|
21
|
+
public abstract symmetricDifference(other: Array<T>): Array<T>
|
|
22
22
|
|
|
23
|
-
public abstract complement(other: 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
|
|
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
|
|
@@ -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',
|