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.
- package/build/main.js +1 -158
- 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 +188 -106
- 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 +7 -8
- package/source/main.ts +34 -11
- package/templates/ctx/example-ports.ts +1 -0
- package/templates/lib/shared/application/data/capabilities.ts +60 -0
- package/templates/lib/shared/application/data/drivers.ts +2 -2
- package/templates/lib/shared/application/data/managers.ts +5 -62
- package/templates/lib/shared/application/data/repositories.ts +7 -19
- package/templates/lib/shared/application/events.ts +1 -1
- package/templates/lib/shared/application/http/errors.ts +2 -0
- package/templates/lib/shared/application/loggers.ts +0 -21
- package/templates/lib/shared/domain/entities.ts +1 -1
- package/templates/lib/shared/application/http/handlers.ts +0 -13
- package/templates/lib/shared/application/http/json-api.ts +0 -611
- package/templates/lib/shared/application/http/json-web-token.ts +0 -980
- package/templates/lib/shared/application/http/opengraph.ts +0 -533
package/build/main.js
CHANGED
|
@@ -1,159 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import fs from 'node:fs';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import readline from 'node:readline/promises';
|
|
6
|
-
import { stdin as input, stdout as output } from 'node:process';
|
|
7
|
-
function readPackageJson() {
|
|
8
|
-
const filePath = path.join(import.meta.dirname, '..', 'package.json');
|
|
9
|
-
const fileContents = fs.readFileSync(filePath, 'utf-8');
|
|
10
|
-
return JSON.parse(fileContents);
|
|
11
|
-
}
|
|
12
|
-
function executeCreateProject(templatesDir, projectDir) {
|
|
13
|
-
try {
|
|
14
|
-
fs.cpSync(path.join(templatesDir, 'lib'), projectDir, {
|
|
15
|
-
recursive: true,
|
|
16
|
-
filter: (src) => src.endsWith('.gitkeep') === false
|
|
17
|
-
});
|
|
18
|
-
console.log('Project created successfully');
|
|
19
|
-
}
|
|
20
|
-
catch (err) {
|
|
21
|
-
console.error(err);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
function executeCreateContext(templatesDir, contextDir) {
|
|
25
|
-
try {
|
|
26
|
-
fs.cpSync(path.join(templatesDir, 'ctx'), contextDir, {
|
|
27
|
-
recursive: true,
|
|
28
|
-
filter: (src) => src.endsWith('.gitkeep') === false
|
|
29
|
-
});
|
|
30
|
-
console.log('Context created successfully');
|
|
31
|
-
}
|
|
32
|
-
catch (err) {
|
|
33
|
-
console.error(err);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
function executeCreateReactContext(templatesDir, contextDir) {
|
|
37
|
-
try {
|
|
38
|
-
fs.cpSync(path.join(templatesDir, 'ctx-react'), contextDir, {
|
|
39
|
-
recursive: true,
|
|
40
|
-
filter: (src) => src.endsWith('.gitkeep') === false
|
|
41
|
-
});
|
|
42
|
-
console.log('React context created successfully');
|
|
43
|
-
}
|
|
44
|
-
catch (err) {
|
|
45
|
-
console.error(err);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
function executeCreateTests(sourceDir, destinationDir, fileContents, ignoredSourceDir, rootSourceDir = sourceDir) {
|
|
49
|
-
const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
|
|
50
|
-
if (fs.existsSync(destinationDir) === false) {
|
|
51
|
-
fs.mkdirSync(destinationDir, { recursive: true });
|
|
52
|
-
}
|
|
53
|
-
for (const entry of entries) {
|
|
54
|
-
const sourcePath = path.join(sourceDir, entry.name);
|
|
55
|
-
const destinationPath = path.join(destinationDir, entry.name);
|
|
56
|
-
if (entry.isDirectory()) {
|
|
57
|
-
if (entry.name === 'shared' && sourceDir === rootSourceDir) {
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
if (ignoredSourceDir !== undefined && sourcePath.startsWith(ignoredSourceDir)) {
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
if (fs.existsSync(destinationPath) && fs.statSync(destinationPath).isDirectory() === false) {
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
executeCreateTests(sourcePath, destinationPath, fileContents, ignoredSourceDir, rootSourceDir);
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
if (entry.isFile() && entry.name.endsWith('.ts') && fs.existsSync(destinationPath) === false) {
|
|
70
|
-
fs.writeFileSync(destinationPath, fileContents);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
async function ensureTestsDirectory(testsRootDir) {
|
|
75
|
-
if (fs.existsSync(testsRootDir)) {
|
|
76
|
-
if (fs.statSync(testsRootDir).isDirectory() === false) {
|
|
77
|
-
throw new Error(`Tests path exists but is not a directory: ${testsRootDir}`);
|
|
78
|
-
}
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
const rl = readline.createInterface({ input, output });
|
|
82
|
-
try {
|
|
83
|
-
const answer = await rl.question(`Tests directory does not exist at ${testsRootDir}. Create it? (y/N) `);
|
|
84
|
-
if (answer.trim().toLowerCase() !== 'y') {
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
finally {
|
|
89
|
-
rl.close();
|
|
90
|
-
}
|
|
91
|
-
fs.mkdirSync(testsRootDir, { recursive: true });
|
|
92
|
-
return true;
|
|
93
|
-
}
|
|
94
|
-
async function main(program) {
|
|
95
|
-
const templatesDir = path.join(import.meta.dirname, '..', 'templates');
|
|
96
|
-
const options = program.opts();
|
|
97
|
-
let targetDir = path.resolve(options.dir ?? process.cwd());
|
|
98
|
-
if (Object.keys(options).length === 0) {
|
|
99
|
-
program.help();
|
|
100
|
-
}
|
|
101
|
-
if (fs.existsSync(targetDir) === false) {
|
|
102
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
103
|
-
}
|
|
104
|
-
if (options.project !== undefined) {
|
|
105
|
-
targetDir = path.join(targetDir, options.project);
|
|
106
|
-
executeCreateProject(templatesDir, targetDir);
|
|
107
|
-
}
|
|
108
|
-
if (options.react === true && options.context === undefined) {
|
|
109
|
-
program.error('Option --react requires --context <name>');
|
|
110
|
-
}
|
|
111
|
-
if (options.context !== undefined) {
|
|
112
|
-
targetDir = path.join(targetDir, options.context);
|
|
113
|
-
if (options.react === true) {
|
|
114
|
-
executeCreateReactContext(templatesDir, targetDir);
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
executeCreateContext(templatesDir, targetDir);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
if (options.tests !== undefined) {
|
|
121
|
-
const sourceDir = path.resolve(options.tests);
|
|
122
|
-
const testsTemplateFile = path.join(templatesDir, 'tests', 'content.ts');
|
|
123
|
-
if (fs.existsSync(sourceDir) === false) {
|
|
124
|
-
program.error(`Tests source directory does not exist: ${sourceDir}`);
|
|
125
|
-
}
|
|
126
|
-
if (fs.statSync(sourceDir).isDirectory() === false) {
|
|
127
|
-
program.error(`Tests source path is not a directory: ${sourceDir}`);
|
|
128
|
-
}
|
|
129
|
-
if (fs.existsSync(testsTemplateFile) === false) {
|
|
130
|
-
program.error(`Tests template file does not exist: ${testsTemplateFile}`);
|
|
131
|
-
}
|
|
132
|
-
const testsTemplateContents = fs.readFileSync(testsTemplateFile, 'utf-8');
|
|
133
|
-
const testsRootDir = path.join(targetDir, 'tests');
|
|
134
|
-
const testsDirectoryCreated = await ensureTestsDirectory(testsRootDir);
|
|
135
|
-
if (testsDirectoryCreated === false) {
|
|
136
|
-
program.error('Tests directory creation cancelled');
|
|
137
|
-
}
|
|
138
|
-
const testsDir = path.join(testsRootDir, path.basename(sourceDir));
|
|
139
|
-
if (testsDir === sourceDir) {
|
|
140
|
-
program.error('Tests destination directory cannot be the same as the source directory');
|
|
141
|
-
}
|
|
142
|
-
executeCreateTests(sourceDir, testsDir, testsTemplateContents, testsRootDir);
|
|
143
|
-
console.log('Tests structure created successfully');
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
const packageJson = readPackageJson();
|
|
147
|
-
program
|
|
148
|
-
.name('tshex')
|
|
149
|
-
.version(packageJson.version)
|
|
150
|
-
.option('-P, --project <name>', "creates a new project with it's shared directory")
|
|
151
|
-
.option('-C, --context <name>', 'creates a new context')
|
|
152
|
-
.option('-R, --react', 'creates a React context with --context')
|
|
153
|
-
.option('-T, --tests <path>', 'creates a .ts tests structure from an existing directory')
|
|
154
|
-
.option('--dir <path>', 'sets the directory to create the new item')
|
|
155
|
-
.parse(process.argv);
|
|
156
|
-
void main(program).catch((err) => {
|
|
157
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
158
|
-
program.error(message);
|
|
159
|
-
});
|
|
2
|
+
import{program as l}from"commander";import r from"node:fs";import n from"node:path";import p from"node:readline/promises";import{stdin as m,stdout as x}from"node:process";function h(){let e=n.join(import.meta.dirname,"..","package.json"),s=r.readFileSync(e,"utf-8");return JSON.parse(s)}function g(e,s){try{r.cpSync(n.join(e,"lib"),s,{recursive:!0,filter:t=>t.endsWith(".gitkeep")===!1}),console.log("Project created successfully")}catch(t){console.error(t)}}function j(e,s){try{r.cpSync(n.join(e,"ctx"),s,{recursive:!0,filter:t=>t.endsWith(".gitkeep")===!1}),console.log("Context created successfully")}catch(t){console.error(t)}}function S(e,s){try{r.cpSync(n.join(e,"ctx-react"),s,{recursive:!0,filter:t=>t.endsWith(".gitkeep")===!1}),console.log("React context created successfully")}catch(t){console.error(t)}}function d(e,s,t,i,c=e){let u=r.readdirSync(e,{withFileTypes:!0});r.existsSync(s)===!1&&r.mkdirSync(s,{recursive:!0});for(let o of u){let a=n.join(e,o.name),f=n.join(s,o.name);if(o.isDirectory()){if(o.name==="shared"&&e===c||i!==void 0&&a.startsWith(i)||r.existsSync(f)&&r.statSync(f).isDirectory()===!1)continue;d(a,f,t,i,c);continue}o.isFile()&&o.name.endsWith(".ts")&&r.existsSync(f)===!1&&r.writeFileSync(f,t)}}async function w(e){if(r.existsSync(e)){if(r.statSync(e).isDirectory()===!1)throw new Error(`Tests path exists but is not a directory: ${e}`);return}let s=p.createInterface({input:m,output:x});try{if((await s.question(`Tests directory does not exist at ${e}. Create it? (y/N) `)).trim().toLowerCase()!=="y")return!1}finally{s.close()}return r.mkdirSync(e,{recursive:!0}),!0}async function T(e){let s=n.join(import.meta.dirname,"..","templates"),t=e.opts(),i=n.resolve(t.dir??process.cwd());if(Object.keys(t).length===0&&e.help(),r.existsSync(i)===!1&&r.mkdirSync(i,{recursive:!0}),t.project!==void 0&&(i=n.join(i,t.project),g(s,i)),t.react===!0&&t.context===void 0&&e.error("Option --react requires --context <name>"),t.context!==void 0&&(i=n.join(i,t.context),t.react===!0?S(s,i):j(s,i)),t.tests!==void 0){let c=n.resolve(t.tests),u=n.join(s,"tests","content.ts");r.existsSync(c)===!1&&e.error(`Tests source directory does not exist: ${c}`),r.statSync(c).isDirectory()===!1&&e.error(`Tests source path is not a directory: ${c}`),r.existsSync(u)===!1&&e.error(`Tests template file does not exist: ${u}`);let o=r.readFileSync(u,"utf-8"),a=n.join(i,"tests");await w(a)===!1&&e.error("Tests directory creation cancelled");let y=n.join(a,n.basename(c));y===c&&e.error("Tests destination directory cannot be the same as the source directory"),d(c,y,o,a),console.log("Tests structure created successfully")}}var v=h();l.name("tshex").version(v.version).option("-P, --project <name>","creates a new project with it's shared directory").option("-C, --context <name>","creates a new context").option("-R, --react","creates a React context with --context").option("-T, --tests <path>","creates a .ts tests structure from an existing directory").option("--dir <path>","sets the directory to create the new item").parse(process.argv);T(l).catch(e=>{let s=e instanceof Error?e.message:String(e);l.error(s)});
|
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
|