tshex-cli 1.0.27 → 1.0.29
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/generated-file-reference.md +13 -10
- package/docs/library-structure.md +4 -4
- package/docs/shared/application/data.md +13 -1
- package/docs/shared/application/http/errors.md +96 -0
- package/docs/shared/application/http/handlers.md +102 -0
- package/docs/shared/application/http/json-api.md +235 -0
- package/docs/shared/application/http/json-web-token.md +209 -0
- package/docs/shared/application/http/opengraph.md +161 -0
- package/docs/shared/application/loggers.md +5 -4
- package/docs/types/json.md +82 -0
- package/docs/types/locales.md +77 -0
- package/docs/types/objects.md +70 -0
- package/docs/types/timezones.md +75 -0
- package/package.json +7 -8
- package/readme.md +23 -4
- package/source/main.ts +34 -11
- package/templates/ctx/example-ports.ts +1 -0
- package/templates/lib/shared/application/data/capabilities.ts +56 -0
- package/templates/lib/shared/application/data/managers.ts +0 -57
- package/templates/lib/shared/application/data/repositories.ts +7 -18
- package/templates/lib/shared/application/loggers.ts +0 -21
- package/templates/lib/shared/domain/entities.ts +1 -1
- package/docs/library-types.md +0 -112
- package/docs/shared/application/http.md +0 -283
- 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/templates/lib/types/{cldr.d.ts → locales.d.ts} +0 -0
- /package/templates/lib/types/{iana.d.ts → timezones.d.ts} +0 -0
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)});
|
|
@@ -22,13 +22,14 @@ The `types/` directory contains root-level ambient type declarations.
|
|
|
22
22
|
| --- | --- |
|
|
23
23
|
| `types/objects.d.ts` | Declares root-level shared types such as `Generic<T>`. |
|
|
24
24
|
| `types/json.d.ts` | Declares `JsonValue` and the other plain, serializable JSON shapes. |
|
|
25
|
-
| `types/
|
|
26
|
-
| `types/
|
|
25
|
+
| `types/locales.d.ts` | Declares the `Locale` union from Unicode CLDR. |
|
|
26
|
+
| `types/timezones.d.ts` | Declares the `TimeZone` union from the IANA time zone database. |
|
|
27
27
|
|
|
28
28
|
`types/objects.d.ts` and `types/json.d.ts` are the place for general-purpose
|
|
29
|
-
root-level type declarations. `types/
|
|
30
|
-
generated reference types consumed by other shared contracts, such as
|
|
31
|
-
`shared/application/loggers.ts`.
|
|
29
|
+
root-level type declarations. `types/locales.d.ts` and `types/timezones.d.ts`
|
|
30
|
+
are generated reference types consumed by other shared contracts, such as
|
|
31
|
+
`shared/application/loggers.ts`. Each file is documented in its own page under
|
|
32
|
+
`types/*.md`.
|
|
32
33
|
|
|
33
34
|
#### Shared Domain Files
|
|
34
35
|
|
|
@@ -66,15 +67,17 @@ boundary and the type-only specifications for common web content formats.
|
|
|
66
67
|
|
|
67
68
|
| File | Responsibility |
|
|
68
69
|
| --- | --- |
|
|
69
|
-
| `shared/application/http/
|
|
70
|
+
| `shared/application/http/handlers.ts` | Declares `HttpRequestHandler` and `HttpMiddleware`. |
|
|
71
|
+
| `shared/application/http/errors.ts` | Declares `HttpError`. |
|
|
70
72
|
| `shared/application/http/json-api.ts` | Type-only JSON:API v1.1 document, resource, and Atomic Operations declarations. |
|
|
71
73
|
| `shared/application/http/json-web-token.ts` | Type-only JOSE/JWT declarations (JWK, JWS, JWE, JWT claims). |
|
|
72
74
|
| `shared/application/http/opengraph.ts` | Type-only Open Graph, Twitter Card, and social metadata declarations. |
|
|
73
75
|
|
|
74
|
-
`
|
|
75
|
-
`json-web-token.ts`, and `opengraph.ts` contain
|
|
76
|
-
they describe the shape of external formats
|
|
77
|
-
validation, or serialization.
|
|
76
|
+
`handlers.ts` and `errors.ts` are the only files in this directory with
|
|
77
|
+
runtime code. `json-api.ts`, `json-web-token.ts`, and `opengraph.ts` contain
|
|
78
|
+
compile-time structure only; they describe the shape of external formats
|
|
79
|
+
without implementing parsing, validation, or serialization. Each file is
|
|
80
|
+
documented in its own page under `shared/application/http/*.md`.
|
|
78
81
|
|
|
79
82
|
#### Shared Data Files
|
|
80
83
|
|
|
@@ -34,14 +34,14 @@ library.
|
|
|
34
34
|
flowchart TD
|
|
35
35
|
types["types/"] --> typesObjects["objects.d.ts"]
|
|
36
36
|
types --> json["json.d.ts"]
|
|
37
|
-
types -->
|
|
38
|
-
types -->
|
|
37
|
+
types --> locales["locales.d.ts"]
|
|
38
|
+
types --> timezones["timezones.d.ts"]
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
`types/objects.d.ts` defines root-level types such as `Generic<T>`.
|
|
42
42
|
`types/json.d.ts` defines `JsonValue` and the other plain, serializable JSON
|
|
43
|
-
shapes. `types/
|
|
44
|
-
`types/
|
|
43
|
+
shapes. `types/locales.d.ts` declares the `Locale` union from Unicode CLDR.
|
|
44
|
+
`types/timezones.d.ts` declares the `TimeZone` union from the IANA time zone
|
|
45
45
|
database.
|
|
46
46
|
|
|
47
47
|
#### Shared
|
|
@@ -49,7 +49,7 @@ The base class provides `none()` as an explicit empty result and requires
|
|
|
49
49
|
contracts such as `Filterable`, `Creatable`, and `Updatable`, plus the
|
|
50
50
|
`DatasetManager` extension for set operations.
|
|
51
51
|
|
|
52
|
-
####
|
|
52
|
+
#### Implementation
|
|
53
53
|
|
|
54
54
|
In the following example we implement an in-memory manager and its driver.
|
|
55
55
|
|
|
@@ -92,6 +92,18 @@ export class MemoryUsersDriver extends DriverAdapter<MemoryUsersManager> {
|
|
|
92
92
|
raw records. The application layer can use both without knowing whether the
|
|
93
93
|
source is memory, SQL, or an HTTP-backed adapter.
|
|
94
94
|
|
|
95
|
+
Put all your complex data operations in `DataManager`. `Repository` should only handle the transformation of raw records into domain representations. For example, if you need to relate users to their posts, implement that in a manager:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
class ComplexUsersManager extends DataManager<EnrichedUserRecord> {
|
|
99
|
+
...
|
|
100
|
+
|
|
101
|
+
public async findAllAndRelate(): Promise<Array<EnrichedUserRecord>> {
|
|
102
|
+
...
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
95
107
|
#### Repository
|
|
96
108
|
|
|
97
109
|
`Repository` is responsible for transforming raw records into domain-oriented
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
### HTTP Errors
|
|
2
|
+
|
|
3
|
+
`HttpError` carries an HTTP status code alongside a matching message.
|
|
4
|
+
It is used when a use case or adapter needs to signal a specific HTTP outcome
|
|
5
|
+
without depending on a transport framework's own error type.
|
|
6
|
+
|
|
7
|
+
#### Declaration
|
|
8
|
+
|
|
9
|
+
```ts title="shared/application/http/errors.ts"
|
|
10
|
+
export class HttpError extends Error {
|
|
11
|
+
public static readonly messages: { [code: number]: string } = Object.freeze({
|
|
12
|
+
400: 'Bad Request',
|
|
13
|
+
401: 'Unauthorized',
|
|
14
|
+
404: 'Not Found',
|
|
15
|
+
409: 'Conflict',
|
|
16
|
+
// ...remaining standard 4xx/5xx status codes
|
|
17
|
+
500: 'Internal Server Error',
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
public readonly code: number
|
|
21
|
+
|
|
22
|
+
constructor(code: number, message?: string) {
|
|
23
|
+
super(message ?? HttpError.messages[code] ?? 'Unknown Error')
|
|
24
|
+
this.code = code
|
|
25
|
+
this.name = 'HttpError'
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`HttpError.messages` maps every standard 4xx/5xx status code registered by the
|
|
31
|
+
HTTP specification, from `400` to `511`, to its reason phrase.
|
|
32
|
+
|
|
33
|
+
#### Implementation Options
|
|
34
|
+
|
|
35
|
+
Constructing an `HttpError` supports three distinct outcomes, depending on
|
|
36
|
+
what arguments are passed.
|
|
37
|
+
|
|
38
|
+
**1. Known code, default message.** The message is looked up from
|
|
39
|
+
`HttpError.messages`.
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { HttpError } from '../../shared/application/http/errors.js'
|
|
43
|
+
|
|
44
|
+
const error = new HttpError(404)
|
|
45
|
+
|
|
46
|
+
error.code // 404
|
|
47
|
+
error.message // 'Not Found'
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**2. Known code, explicit message.** The explicit message always takes
|
|
51
|
+
precedence over the table.
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { HttpError } from '../../shared/application/http/errors.js'
|
|
55
|
+
|
|
56
|
+
const error = new HttpError(409, 'Email is already registered')
|
|
57
|
+
|
|
58
|
+
error.code // 409
|
|
59
|
+
error.message // 'Email is already registered'
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**3. Unrecognized code, no message.** Codes outside `HttpError.messages` fall
|
|
63
|
+
back to `'Unknown Error'` instead of throwing.
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { HttpError } from '../../shared/application/http/errors.js'
|
|
67
|
+
|
|
68
|
+
const error = new HttpError(499)
|
|
69
|
+
|
|
70
|
+
error.code // 499
|
|
71
|
+
error.message // 'Unknown Error'
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
#### Usage In A Handler
|
|
75
|
+
|
|
76
|
+
```ts title="users/adapters/get-user-handler.ts"
|
|
77
|
+
import { HttpError } from '../../shared/application/http/errors.js'
|
|
78
|
+
|
|
79
|
+
function assertFound<T>(value: T | null): T {
|
|
80
|
+
if (value === null) {
|
|
81
|
+
throw new HttpError(404)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return value
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`assertFound()` throws the standard `404` message for free. The caller of the
|
|
89
|
+
handler decides how a thrown `HttpError` is turned into a `Response`, since
|
|
90
|
+
`errors.ts` only declares the error shape and not the response translation.
|
|
91
|
+
|
|
92
|
+
> **Note**
|
|
93
|
+
> `HttpError` only carries the status code and message. Serializing it into a
|
|
94
|
+
> `Response` body, including it in a JSON:API error document (see
|
|
95
|
+
> `shared/application/http/json-api.md`), and logging it are the
|
|
96
|
+
> responsibility of the concrete adapter.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
### HTTP Handlers
|
|
2
|
+
|
|
3
|
+
The handler contracts define a framework-agnostic, transport-facing boundary.
|
|
4
|
+
They are used when an adapter needs to describe request processing and
|
|
5
|
+
cross-cutting request logic in a consistent way, without depending on a
|
|
6
|
+
specific server framework.
|
|
7
|
+
|
|
8
|
+
The generated template relies on the standard `Request` and `Response` types
|
|
9
|
+
from the Fetch API, so adapters work directly with the platform's own APIs.
|
|
10
|
+
|
|
11
|
+
#### Request Handler
|
|
12
|
+
|
|
13
|
+
`HttpRequestHandler` is responsible for processing a request and returning a
|
|
14
|
+
response.
|
|
15
|
+
|
|
16
|
+
```ts title="shared/application/http/handlers.ts"
|
|
17
|
+
export interface HttpRequestHandler {
|
|
18
|
+
handle(request: Request): Response | Promise<Response>
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
In the following example we implement a handler using the standard `Request`
|
|
23
|
+
and `Response` objects.
|
|
24
|
+
|
|
25
|
+
```ts title="users/adapters/get-user-handler.ts"
|
|
26
|
+
import { HttpRequestHandler } from '../../shared/application/http/handlers.js'
|
|
27
|
+
|
|
28
|
+
export class GetUserHandler implements HttpRequestHandler {
|
|
29
|
+
public handle(request: Request): Response {
|
|
30
|
+
const id = new URL(request.url).pathname.split('/').at(-1)
|
|
31
|
+
|
|
32
|
+
return Response.json({ data: { id } }, { status: 200 })
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Using the standard `Request` and `Response` types means the adapter relies on
|
|
38
|
+
the platform's own APIs, such as `request.url`, `request.headers`, and
|
|
39
|
+
`Response.json`. The shape of the JSON body itself, when the adapter follows
|
|
40
|
+
JSON:API, is described by the document types in
|
|
41
|
+
`shared/application/http/json-api.md`.
|
|
42
|
+
|
|
43
|
+
`handle()` can be synchronous or asynchronous; both `Response` and
|
|
44
|
+
`Promise<Response>` are valid return types, so an adapter that awaits a
|
|
45
|
+
database call satisfies the same contract as one that returns immediately.
|
|
46
|
+
|
|
47
|
+
#### Middleware
|
|
48
|
+
|
|
49
|
+
`HttpMiddleware` is responsible for running logic before or around the
|
|
50
|
+
handler.
|
|
51
|
+
|
|
52
|
+
```ts title="shared/application/http/handlers.ts"
|
|
53
|
+
export interface HttpMiddleware {
|
|
54
|
+
process(
|
|
55
|
+
request: Request,
|
|
56
|
+
handler: HttpRequestHandler,
|
|
57
|
+
): Response | Promise<Response>
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Now that the handler exists, middleware can wrap it.
|
|
62
|
+
|
|
63
|
+
```ts title="users/adapters/request-logger.ts"
|
|
64
|
+
import {
|
|
65
|
+
HttpMiddleware,
|
|
66
|
+
HttpRequestHandler,
|
|
67
|
+
} from '../../shared/application/http/handlers.js'
|
|
68
|
+
|
|
69
|
+
export class RequestLoggerMiddleware implements HttpMiddleware {
|
|
70
|
+
public async process(
|
|
71
|
+
request: Request,
|
|
72
|
+
handler: HttpRequestHandler,
|
|
73
|
+
): Promise<Response> {
|
|
74
|
+
void request
|
|
75
|
+
return handler.handle(request)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
This middleware passes the request through unchanged, showing where
|
|
81
|
+
cross-cutting behavior belongs in the generated HTTP abstraction. A
|
|
82
|
+
short-circuiting middleware, such as an authentication guard, follows the same
|
|
83
|
+
shape but returns a `Response` (for example built from
|
|
84
|
+
`shared/application/http/errors.md`'s `HttpError`) without calling
|
|
85
|
+
`handler.handle()`.
|
|
86
|
+
|
|
87
|
+
> **Note**
|
|
88
|
+
> The handler contracts define the minimum boundary for adapters. Routing,
|
|
89
|
+
> middleware chaining/composition, and status code policies beyond
|
|
90
|
+
> `HttpError` are the responsibility of the concrete transport.
|
|
91
|
+
|
|
92
|
+
#### Example Flow
|
|
93
|
+
|
|
94
|
+
```mermaid
|
|
95
|
+
flowchart LR
|
|
96
|
+
request[Request] --> middleware[Middleware]
|
|
97
|
+
middleware --> handler[Handler]
|
|
98
|
+
handler --> response["Response body"]
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
This flow keeps the transport boundary explicit while leaving framework
|
|
102
|
+
choices to the adapter layer.
|