tshex-cli 1.0.28 → 1.0.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,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
- 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'
52
38
 
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.
39
+ export class Student extends Entity {
40
+ [property: string]: unknown
56
41
 
57
- #### Included Behavior
42
+ constructor(
43
+ public name: string,
44
+ public email: string
45
+ ) {
46
+ super()
47
+ }
58
48
 
59
- Now that the entity exists, the inherited helpers become useful.
49
+ public equals(other: Student): boolean {
50
+ return this.email === other.email
51
+ }
60
52
 
61
- ```ts
62
- const user = new User('usr_1', Email.from('ada@example.com'))
53
+ public override toJSON() {
54
+ return {
55
+ name: this.name,
56
+ email: this.email
57
+ }
58
+ }
59
+ }
60
+ ```
63
61
 
64
- user.toString()
65
- user.toJSON()
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
- `toString()` returns the constructor name. `toJSON()` returns the instance as a
69
- plain record, which is convenient for simple serialization or inspection.
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
- 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.
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,6 +1,6 @@
1
1
  {
2
2
  "name": "tshex-cli",
3
- "version": "1.0.28",
3
+ "version": "1.0.30",
4
4
  "author": "https://github.com/virtualitems/",
5
5
  "license": "MIT",
6
6
  "description": "Typescript Hexagonal Architecture CLI",
@@ -41,15 +41,14 @@
41
41
  "bin": {
42
42
  "tshex": "./build/main.js"
43
43
  },
44
+ "scripts": {
45
+ "build": "deno run --allow-all npm:esbuild --bundle --minify --platform=node --external:commander --format=esm --outfile=./build/main.js ./source/main.ts",
46
+ "test": "deno test --allow-all tests/"
47
+ },
44
48
  "dependencies": {
45
- "commander": "^12.1.0"
49
+ "commander": "^15.0.0"
46
50
  },
47
51
  "devDependencies": {
48
- "@fission-ai/openspec": "^1.6.0",
49
- "@types/node": "^22.5.4",
50
- "typescript": "^5.4.5"
51
- },
52
- "scripts": {
53
- "build": "tsc"
52
+ "@types/node": "^18"
54
53
  }
55
54
  }
package/source/main.ts CHANGED
@@ -9,12 +9,12 @@ import { program } from 'commander'
9
9
  import fs from 'node:fs'
10
10
  import path from 'node:path'
11
11
  import readline from 'node:readline/promises'
12
- import { stdin as input, stdout as output } from 'node:process'
12
+ import { stdin, stdout } from 'node:process'
13
13
 
14
14
  // FUNCTIONS
15
15
 
16
16
  function readPackageJson() {
17
- const filePath = path.join(import.meta.dirname, '..', 'package.json')
17
+ const filePath = path.join(import.meta.dirname!, '..', 'package.json')
18
18
  const fileContents = fs.readFileSync(filePath, 'utf-8')
19
19
  return JSON.parse(fileContents)
20
20
  }
@@ -77,19 +77,35 @@ function executeCreateTests(
77
77
  continue
78
78
  }
79
79
 
80
- if (ignoredSourceDir !== undefined && sourcePath.startsWith(ignoredSourceDir)) {
80
+ if (
81
+ ignoredSourceDir !== undefined &&
82
+ sourcePath.startsWith(ignoredSourceDir)
83
+ ) {
81
84
  continue
82
85
  }
83
86
 
84
- if (fs.existsSync(destinationPath) && fs.statSync(destinationPath).isDirectory() === false) {
87
+ if (
88
+ fs.existsSync(destinationPath) &&
89
+ fs.statSync(destinationPath).isDirectory() === false
90
+ ) {
85
91
  continue
86
92
  }
87
93
 
88
- executeCreateTests(sourcePath, destinationPath, fileContents, ignoredSourceDir, rootSourceDir)
94
+ executeCreateTests(
95
+ sourcePath,
96
+ destinationPath,
97
+ fileContents,
98
+ ignoredSourceDir,
99
+ rootSourceDir
100
+ )
89
101
  continue
90
102
  }
91
103
 
92
- if (entry.isFile() && entry.name.endsWith('.ts') && fs.existsSync(destinationPath) === false) {
104
+ if (
105
+ entry.isFile() &&
106
+ entry.name.endsWith('.ts') &&
107
+ fs.existsSync(destinationPath) === false
108
+ ) {
93
109
  fs.writeFileSync(destinationPath, fileContents)
94
110
  }
95
111
  }
@@ -104,10 +120,12 @@ async function ensureTestsDirectory(testsRootDir: string) {
104
120
  return
105
121
  }
106
122
 
107
- const rl = readline.createInterface({ input, output })
123
+ const rl = readline.createInterface({ input: stdin, output: stdout })
108
124
 
109
125
  try {
110
- const answer = await rl.question(`Tests directory does not exist at ${testsRootDir}. Create it? (y/N) `)
126
+ const answer = await rl.question(
127
+ `Tests directory does not exist at ${testsRootDir}. Create it? (y/N) `
128
+ )
111
129
 
112
130
  if (answer.trim().toLowerCase() !== 'y') {
113
131
  return false
@@ -121,7 +139,7 @@ async function ensureTestsDirectory(testsRootDir: string) {
121
139
  }
122
140
 
123
141
  async function main(program: typeof import('commander').program) {
124
- const templatesDir = path.join(import.meta.dirname, '..', 'templates')
142
+ const templatesDir = path.join(import.meta.dirname!, '..', 'templates')
125
143
 
126
144
  const options = program.opts()
127
145
 
@@ -182,7 +200,9 @@ async function main(program: typeof import('commander').program) {
182
200
  const testsDir = path.join(testsRootDir, path.basename(sourceDir))
183
201
 
184
202
  if (testsDir === sourceDir) {
185
- program.error('Tests destination directory cannot be the same as the source directory')
203
+ program.error(
204
+ 'Tests destination directory cannot be the same as the source directory'
205
+ )
186
206
  }
187
207
 
188
208
  executeCreateTests(sourceDir, testsDir, testsTemplateContents, testsRootDir)
@@ -198,7 +218,10 @@ program
198
218
  .option('-P, --project <name>', "creates a new project with it's shared directory")
199
219
  .option('-C, --context <name>', 'creates a new context')
200
220
  .option('-R, --react', 'creates a React context with --context')
201
- .option('-T, --tests <path>', 'creates a .ts tests structure from an existing directory')
221
+ .option(
222
+ '-T, --tests <path>',
223
+ 'creates a .ts tests structure from an existing directory'
224
+ )
202
225
  .option('--dir <path>', 'sets the directory to create the new item')
203
226
  .parse(process.argv)
204
227
 
@@ -1,4 +1,5 @@
1
1
  // Ports are exports from the context root level
2
+ // you can delete this file and create your own ports file in the context root level
2
3
 
3
4
  export function example(): void {
4
5
  // ...
@@ -0,0 +1,60 @@
1
+ type Generic = Record<string, unknown>
2
+
3
+ export interface Listable<DataShape extends Generic = Generic> {
4
+ all(): DataShape[]
5
+ }
6
+
7
+ /**
8
+ * @description Declares a filtering operation over plain source records.
9
+ */
10
+ export interface Filterable<DataShape extends Generic = Generic, Selector = unknown> {
11
+ filter(selector: Selector): DataShape[]
12
+ }
13
+
14
+ /**
15
+ * @description Declares a sorting operation over plain source records.
16
+ */
17
+ export interface Sortable<DataShape extends Generic = Generic, Selector = unknown> {
18
+ sort(selector: Selector): DataShape[]
19
+ }
20
+
21
+ /**
22
+ * @description Declares a creation operation for plain source records.
23
+ */
24
+ export interface Creatable<DataShape extends Generic = Generic, Feedback = unknown> {
25
+ create(data: DataShape): Feedback
26
+ }
27
+
28
+ /**
29
+ * @description Declares an update operation that selects source records and applies new plain data.
30
+ */
31
+ export interface Updatable<
32
+ DataShape extends Generic = Generic,
33
+ Selector = unknown,
34
+ Feedback = unknown
35
+ > {
36
+ update(selector: Selector, data: Partial<DataShape>): Feedback
37
+ }
38
+
39
+ /**
40
+ * @description Declares a deletion operation over source records selected by plain criteria.
41
+ */
42
+ export interface Deletable<Selector = unknown, Feedback = unknown> {
43
+ delete(selector: Selector): Feedback
44
+ }
45
+
46
+ /**
47
+ * @description Declares an aggregation operation over source records.
48
+ */
49
+ export interface Aggregatable<Selector = unknown, Feedback = unknown> {
50
+ aggregate(selector: Selector): Feedback
51
+ }
52
+
53
+ /**
54
+ * @description Declares operations for selecting or preloading relationships from a data source.
55
+ */
56
+ export interface Relatable {
57
+ selectRelated(...args: unknown[]): unknown
58
+
59
+ prefetchRelated(...args: unknown[]): unknown
60
+ }
@@ -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[]): Promise<M>
12
+ public abstract connect(...args: unknown[]): M
13
13
 
14
- public abstract disconnect(): Promise<unknown>
14
+ public abstract disconnect(): unknown
15
15
  } //:: class
@@ -1,66 +1,9 @@
1
- /**
2
- * @description Declares a filtering operation over plain source records.
3
- */
4
- export interface Filterable<S = Record<string, unknown>> {
5
- filter(selector: S): Promise<Array<S>>
6
- }
7
-
8
- /**
9
- * @description Declares a sorting operation over plain source records.
10
- */
11
- export interface Sortable<S = Record<string, unknown>> {
12
- sort(selector: S): Promise<Array<S>>
13
- }
14
-
15
- /**
16
- * @description Declares a creation operation for plain source records.
17
- */
18
- export interface Creatable<D = Record<string, unknown>> {
19
- create(data: D): Promise<unknown>
20
- }
21
-
22
- /**
23
- * @description Declares an update operation that selects source records and applies new plain data.
24
- */
25
- export interface Updatable<S = Record<string, unknown>, D = Record<string, unknown>> {
26
- update(selector: S, data: D): Promise<unknown>
27
- }
28
-
29
- /**
30
- * @description Declares a deletion operation over source records selected by plain criteria.
31
- */
32
- export interface Deletable<S = Record<string, unknown>> {
33
- delete(selector: S): Promise<unknown>
34
- }
35
-
36
- /**
37
- * @description Declares an aggregation operation over source records.
38
- */
39
- export interface Aggregatable<S = Record<string, unknown>> {
40
- aggregate(selector: S): Promise<S>
41
- }
42
-
43
- /**
44
- * @description Declares operations for selecting or preloading relationships from a data source.
45
- */
46
- export interface Relatable {
47
- selectRelated(...args: unknown[]): unknown
48
-
49
- prefetchRelated(...args: unknown[]): unknown
50
- }
51
-
52
1
  /**
53
2
  * @description Operates on a data source using plain objects and arrays.
54
3
  * It exposes the raw data without transforming it.
55
4
  */
56
5
  export abstract class DataManager<T = Record<string, unknown>> {
57
6
  [property: string]: unknown
58
-
59
- public none(): Array<T> {
60
- return []
61
- }
62
-
63
- public abstract all(): Promise<Array<T>>
64
7
  } //:: class
65
8
 
66
9
  /**
@@ -69,13 +12,13 @@ export abstract class DataManager<T = Record<string, unknown>> {
69
12
  export abstract class DatasetManager<T = Record<string, unknown>> extends DataManager<T> {
70
13
  [property: string]: unknown
71
14
 
72
- public abstract union(other: Array<T>): Promise<Array<T>>
15
+ public abstract union(other: Array<T>): Array<T>
73
16
 
74
- public abstract intersection(other: Array<T>): Promise<Array<T>>
17
+ public abstract intersection(other: Array<T>): Array<T>
75
18
 
76
- public abstract difference(other: Array<T>): Promise<Array<T>>
19
+ public abstract difference(other: Array<T>): Array<T>
77
20
 
78
- public abstract symmetricDifference(other: Array<T>): Promise<Array<T>>
21
+ public abstract symmetricDifference(other: Array<T>): Array<T>
79
22
 
80
- public abstract complement(other: Array<T>): Promise<Array<T>>
23
+ public abstract complement(other: Array<T>): Array<T>
81
24
  } //:: class