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.
- package/docs/context-ports.md +66 -28
- package/docs/generated-file-reference.md +4 -11
- package/docs/library-structure.md +23 -6
- package/docs/shared/application/data.md +188 -117
- package/docs/shared/application/services.md +90 -87
- package/docs/shared/domain/aggregates.md +22 -131
- package/docs/shared/domain/entities.md +98 -51
- package/package.json +1 -1
- package/readme.md +2 -9
- 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/shared/application/http/handlers.md +0 -102
- package/docs/shared/application/http/json-api.md +0 -235
- package/docs/shared/application/http/json-web-token.md +0 -209
- package/docs/shared/application/http/opengraph.md +0 -161
|
@@ -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,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
|
-
|
|
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'
|
|
38
|
+
import { Email } from '../shared/domain/value-objects.ts'
|
|
52
39
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
the same user.
|
|
40
|
+
export class Student extends Entity {
|
|
41
|
+
[property: string]: unknown
|
|
56
42
|
|
|
57
|
-
|
|
43
|
+
constructor(
|
|
44
|
+
public name: string,
|
|
45
|
+
public email: Email
|
|
46
|
+
) {
|
|
47
|
+
super()
|
|
48
|
+
}
|
|
58
49
|
|
|
59
|
-
|
|
50
|
+
public equals(other: Student): boolean {
|
|
51
|
+
return this.email.equals(other.email)
|
|
52
|
+
}
|
|
60
53
|
|
|
61
|
-
|
|
62
|
-
|
|
54
|
+
public override toJSON() {
|
|
55
|
+
return {
|
|
56
|
+
name: this.name,
|
|
57
|
+
email: this.email.value
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
63
62
|
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
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
|
-
| | |
|
|
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():
|
|
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
|
/**
|