tshex-cli 1.0.19 → 1.0.21

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.
@@ -0,0 +1,91 @@
1
+ ### Entities
2
+
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.
7
+
8
+ The generated template provides `Entity` as a base class for this pattern.
9
+
10
+ #### Base Class
11
+
12
+ `Entity` requires one operation and already provides two helper methods.
13
+
14
+ ```ts title="shared/domain/entities.ts"
15
+ export abstract class Entity {
16
+ public abstract equals(other: Entity): boolean
17
+
18
+ public toJSON(): Record<string, unknown> {
19
+ return this
20
+ }
21
+
22
+ public toString(): string {
23
+ return String(this.constructor.name)
24
+ }
25
+ }
26
+ ```
27
+
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.
34
+
35
+ ```ts title="users/domain/user.ts"
36
+ import { Entity } from '../../shared/domain/entities.js'
37
+ import { Email } from '../../shared/domain/value-objects.js'
38
+
39
+ export class User extends Entity {
40
+ public constructor(
41
+ public readonly id: string,
42
+ public readonly email: Email,
43
+ ) {
44
+ super()
45
+ }
46
+
47
+ public equals(other: Entity): boolean {
48
+ return other instanceof User && this.id === other.id
49
+ }
50
+ }
51
+ ```
52
+
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.
56
+
57
+ #### Included Behavior
58
+
59
+ Now that the entity exists, the inherited helpers become useful.
60
+
61
+ ```ts
62
+ const user = new User('usr_1', Email.from('ada@example.com'))
63
+
64
+ user.toString()
65
+ user.toJSON()
66
+ ```
67
+
68
+ `toString()` returns the constructor name. `toJSON()` returns the instance as a
69
+ plain record, which is convenient for simple serialization or inspection.
70
+
71
+ #### Equality Rules
72
+
73
+ The most important design decision in an entity is the identity comparison.
74
+
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.
83
+
84
+ > **Hint**
85
+ > Keep `equals()` explicit and small. If the comparison starts depending on many
86
+ > mutable fields, the model may be closer to a value object than to an entity.
87
+
88
+ #### Next Step
89
+
90
+ 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`.
@@ -0,0 +1,103 @@
1
+ ### Errors
2
+
3
+ Domain errors are responsible for making invalid domain values explicit.
4
+ They are used when a domain concept rejects an input because that input does not
5
+ satisfy the rule required by the concept.
6
+
7
+ The generated template provides `ValueError` for this purpose.
8
+
9
+ #### ValueError
10
+
11
+ `ValueError` is responsible for describing that a received value does not match
12
+ the expected domain concept.
13
+
14
+ ```ts title="shared/domain/errors.ts"
15
+ export class ValueError extends Error {
16
+ public constructor(received: string, expected: string) {
17
+ super(`Invalid value ${received} for ${expected}.`)
18
+ }
19
+ }
20
+ ```
21
+
22
+ The constructor receives the invalid value and the expected concept name. The
23
+ message format is generated automatically.
24
+
25
+ #### First Usage
26
+
27
+ In the following example we define a local value object that rejects numbers
28
+ outside the accepted range.
29
+
30
+ ```ts title="users/domain/percentage.ts"
31
+ import { ValueError } from '../../shared/domain/errors.js'
32
+
33
+ export class Percentage {
34
+ public constructor(public readonly value: number) {}
35
+
36
+ public static from(value: number): Percentage {
37
+ if (value < 0 || value > 100) {
38
+ throw new ValueError(String(value), Percentage.name)
39
+ }
40
+
41
+ return new Percentage(value)
42
+ }
43
+ }
44
+ ```
45
+
46
+ `Percentage.from()` throws `ValueError` when the received value does not satisfy
47
+ the rule of the concept.
48
+
49
+ #### Handling The Error
50
+
51
+ Now consider a process that wants to translate a domain error into an
52
+ application-level result.
53
+
54
+ ```ts title="users/application/apply-discount.ts"
55
+ import { ValueError } from '../../shared/domain/errors.js'
56
+
57
+ type DiscountResult =
58
+ | { ok: true; value: number }
59
+ | { ok: false; error: string }
60
+
61
+ class Percentage {
62
+ public constructor(public readonly value: number) {}
63
+
64
+ public static from(value: number): Percentage {
65
+ if (value < 0 || value > 100) {
66
+ throw new ValueError(String(value), Percentage.name)
67
+ }
68
+
69
+ return new Percentage(value)
70
+ }
71
+ }
72
+
73
+ export function applyDiscount(value: number): DiscountResult {
74
+ try {
75
+ return {
76
+ ok: true,
77
+ value: Percentage.from(value).value,
78
+ }
79
+ } catch (error: unknown) {
80
+ if (error instanceof ValueError) {
81
+ return {
82
+ ok: false,
83
+ error: error.message,
84
+ }
85
+ }
86
+
87
+ throw error
88
+ }
89
+ }
90
+ ```
91
+
92
+ This pattern keeps the domain rule strict while allowing the application layer
93
+ to decide how that failure is exposed.
94
+
95
+ > **Hint**
96
+ > `Email.from()` in the generated value objects uses this same error type.
97
+ > Reusing `ValueError` keeps invalid-value failures recognizable across the
98
+ > domain layer.
99
+
100
+ #### Next Step
101
+
102
+ The generated value objects that already throw `ValueError` are documented in
103
+ `value-objects.md`.
@@ -0,0 +1,148 @@
1
+ ### Value Objects
2
+
3
+ Value objects are responsible for modeling concepts whose identity is
4
+ determined entirely by their value.
5
+ They are used to keep domain rules, comparisons, and semantics close to the
6
+ data that those rules describe.
7
+
8
+ The generated template provides the abstract `ValueObject<T>` base class and two
9
+ concrete implementations: `NullableBoolean` and `Email`.
10
+
11
+ #### Base Class
12
+
13
+ `ValueObject<T>` is responsible for storing a value and defining its comparison
14
+ rules.
15
+
16
+ ```ts title="shared/domain/value-objects.ts"
17
+ export abstract class ValueObject<T = unknown> {
18
+ public abstract readonly value: T
19
+
20
+ public toString(): string {
21
+ return String(this.value)
22
+ }
23
+
24
+ public toJSON(): T {
25
+ return this.value
26
+ }
27
+
28
+ public abstract equals(other: ValueObject<T> | null | undefined): boolean
29
+
30
+ public static isValid(value: unknown): boolean {
31
+ return (
32
+ value !== null &&
33
+ value !== undefined &&
34
+ Object.is(value, NaN) === false
35
+ )
36
+ }
37
+ }
38
+ ```
39
+
40
+ The base class provides serialization helpers and a generic validation check.
41
+ Concrete value objects must define `value` and `equals()`.
42
+
43
+ #### Custom Value Object
44
+
45
+ In the following example we create a local value object for percentages.
46
+
47
+ ```ts title="users/domain/percentage.ts"
48
+ import { ValueObject } from '../../shared/domain/value-objects.js'
49
+ import { ValueError } from '../../shared/domain/errors.js'
50
+
51
+ export class Percentage extends ValueObject<number> {
52
+ public override readonly value: number
53
+
54
+ protected constructor(value: number) {
55
+ super()
56
+ this.value = value
57
+ }
58
+
59
+ public override equals(
60
+ other: Percentage | null | undefined,
61
+ ): boolean {
62
+ if (other === null || other === undefined) {
63
+ return false
64
+ }
65
+
66
+ return this.value === other.value
67
+ }
68
+
69
+ public static from(value: number): Percentage {
70
+ if (value < 0 || value > 100) {
71
+ throw new ValueError(String(value), Percentage.name)
72
+ }
73
+
74
+ return new Percentage(value)
75
+ }
76
+ }
77
+ ```
78
+
79
+ `Percentage` owns the validation rule of the concept and the equality rule of
80
+ the value. This is the normal responsibility split for a value object.
81
+
82
+ #### NullableBoolean
83
+
84
+ `NullableBoolean` is responsible for representing a tri-state Boolean.
85
+
86
+ ```ts title="shared/domain/value-objects.ts"
87
+ import { NullableBoolean } from '../../shared/domain/value-objects.js'
88
+
89
+ const active = NullableBoolean.from(true)
90
+ const unknown = NullableBoolean.from(null)
91
+
92
+ active.equals(NullableBoolean.from(true))
93
+ unknown.isIndeterminate()
94
+ ```
95
+
96
+ `from(value)` accepts `true`, `false`, or `null`. `equals()` compares the wrapped
97
+ value. `isIndeterminate()` returns `true` when the state is `null`.
98
+
99
+ #### Email
100
+
101
+ `Email` is responsible for validating and describing an email address.
102
+
103
+ ```ts title="shared/domain/value-objects.ts"
104
+ import { Email } from '../../shared/domain/value-objects.js'
105
+
106
+ const email = Email.from('ada@example.com')
107
+
108
+ email.username
109
+ email.domain
110
+ email.tld
111
+ ```
112
+
113
+ `Email.from()` validates the string and throws `ValueError` when the input does
114
+ not match the generated email rules. The getters expose common derived parts of
115
+ the address without repeating parsing logic in the rest of the domain.
116
+
117
+ #### Full Example
118
+
119
+ The following example combines both generated value objects in one small domain
120
+ shape.
121
+
122
+ ```ts title="users/domain/user-profile.ts"
123
+ import { Email, NullableBoolean } from '../../shared/domain/value-objects.js'
124
+
125
+ type UserProfile = {
126
+ email: Email
127
+ active: NullableBoolean
128
+ }
129
+
130
+ const profile: UserProfile = {
131
+ email: Email.from('ada@example.com'),
132
+ active: NullableBoolean.from(null),
133
+ }
134
+ ```
135
+
136
+ This example keeps the domain data explicit. The email carries its own
137
+ validation and derived fields. The active flag carries its own tri-state
138
+ semantics.
139
+
140
+ > **Warning**
141
+ > Use a value object only when equality depends on the value itself. If the
142
+ > concept must preserve identity independently from changing attributes, model it
143
+ > as an entity instead.
144
+
145
+ #### Next Step
146
+
147
+ After modeling values, the next step is usually to compose them inside entities
148
+ or aggregates.
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "tshex-cli",
3
- "version": "1.0.19",
3
+ "version": "1.0.21",
4
4
  "author": "https://github.com/virtualitems/",
5
+ "license": "MIT",
5
6
  "description": "Typescript Hexagonal Architecture CLI",
6
7
  "type": "module",
7
8
  "main": "./build/main.js",
@@ -22,7 +23,11 @@
22
23
  "typescript",
23
24
  "virtualitems"
24
25
  ],
25
- "homepage": "https://github.com/virtualitems/tshex-cli",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/virtualitems/tshex-cli.git"
29
+ },
30
+ "homepage": "https://github.com/virtualitems/tshex-cli#readme",
26
31
  "bugs": {
27
32
  "url": "https://github.com/virtualitems/tshex-cli/issues"
28
33
  },
@@ -30,7 +35,8 @@
30
35
  "files": [
31
36
  "./build",
32
37
  "./source",
33
- "./templates"
38
+ "./templates",
39
+ "./docs"
34
40
  ],
35
41
  "bin": {
36
42
  "tshex": "./build/main.js"