libmodulor 0.2.0 → 0.4.0
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/.github/ISSUE_TEMPLATE/generic-issue.md +16 -0
- package/CHANGELOG.md +31 -0
- package/README.md +7 -182
- package/dist/esm/index.react-native-pure.d.ts +10 -0
- package/dist/esm/index.react-native-pure.js +10 -0
- package/dist/esm/target/lib/react/entrypoint.d.ts +2 -0
- package/dist/esm/target/lib/rn/input.d.ts +8 -0
- package/dist/esm/target/lib/rn/input.js +26 -0
- package/dist/esm/target/node-express-server/NodeExpressServerManager.js +4 -1
- package/dist/esm/target/react-native-pure/UCAutoExecLoader.d.ts +2 -0
- package/dist/esm/target/react-native-pure/UCAutoExecLoader.js +5 -0
- package/dist/esm/target/react-native-pure/UCEntrypointTouchable.d.ts +4 -0
- package/dist/esm/target/react-native-pure/UCEntrypointTouchable.js +6 -0
- package/dist/esm/target/react-native-pure/UCExecTouchable.d.ts +4 -0
- package/dist/esm/target/react-native-pure/UCExecTouchable.js +9 -0
- package/dist/esm/target/react-native-pure/UCForm.d.ts +4 -0
- package/dist/esm/target/react-native-pure/UCForm.js +13 -0
- package/dist/esm/target/react-native-pure/UCFormField.d.ts +11 -0
- package/dist/esm/target/react-native-pure/UCFormField.js +32 -0
- package/dist/esm/target/react-native-pure/UCFormFieldControl.d.ts +11 -0
- package/dist/esm/target/react-native-pure/UCFormFieldControl.js +36 -0
- package/dist/esm/target/react-native-pure/UCFormFieldDesc.d.ts +7 -0
- package/dist/esm/target/react-native-pure/UCFormFieldDesc.js +11 -0
- package/dist/esm/target/react-native-pure/UCFormFieldErr.d.ts +7 -0
- package/dist/esm/target/react-native-pure/UCFormFieldErr.js +5 -0
- package/dist/esm/target/react-native-pure/UCFormFieldLabel.d.ts +7 -0
- package/dist/esm/target/react-native-pure/UCFormFieldLabel.js +8 -0
- package/dist/esm/target/react-native-pure/UCFormSubmitControl.d.ts +9 -0
- package/dist/esm/target/react-native-pure/UCFormSubmitControl.js +8 -0
- package/dist/esm/target/react-web-pure/UCEntrypointTouchable.d.ts +1 -1
- package/dist/esm/target/react-web-pure/UCExecTouchable.d.ts +1 -1
- package/dist/esm/target/react-web-pure/UCExecTouchable.js +1 -1
- package/dist/esm/target/react-web-pure/UCFormFieldControl.d.ts +1 -1
- package/dist/esm/target/react-web-pure/UCFormFieldControl.js +3 -3
- package/dist/esm/uc/ext.d.ts +3 -1
- package/dist/esm/uc/helpers/UCOutputReader.d.ts +1 -1
- package/dist/esm/uc/helpers/UCOutputReader.js +4 -2
- package/dist/esm/uc/output-part.d.ts +2 -1
- package/dist/esm/uc/utils/ucHTTPContract.d.ts +1 -0
- package/dist/esm/uc/utils/ucHTTPContract.js +5 -0
- package/package.json +12 -5
- package/docs/assets/trading-buy-asset-sequence-diagram.png +0 -0
- package/docs/assets/trading-target-mcp-server.png +0 -0
- package/docs/assets/trading-target-web-human.png +0 -0
- package/docs/assets/trading-target-web.png +0 -0
- package/docs/getting-started/001_Create_the_project.md +0 -168
- package/docs/getting-started/002_Create_the_App.md +0 -49
- package/docs/getting-started/003_Create_the_UseCase.md +0 -205
- package/docs/getting-started/004_Test_the_App.md +0 -114
- package/docs/getting-started/005_Create_the_Product.md +0 -46
- package/docs/getting-started/006_Create_the_server_Target.md +0 -130
- package/docs/getting-started/007_Create_the_web_Target.md +0 -262
- package/docs/getting-started/008_Switch_to_a_persistent_data_storage.md +0 -52
- package/docs/getting-started/009_Define_wording_for_humans.md +0 -42
- package/docs/getting-started/010_Create_the_cli_Target.md +0 -102
- package/docs/getting-started/011_Create_the_mcp_server_Target.md +0 -157
- package/docs/getting-started/012_Summary.md +0 -29
package/dist/esm/uc/ext.d.ts
CHANGED
|
@@ -2,13 +2,15 @@ import type { HTTPMethod, URLPath } from '../dt/index.js';
|
|
|
2
2
|
import type { UCOPIBase } from './opi.js';
|
|
3
3
|
import type { UCOutput } from './output.js';
|
|
4
4
|
import type { UCMountingPoint } from './utils/ucMountingPoint.js';
|
|
5
|
+
export type UCHTTPMountingPoint = `/${URLPath}`;
|
|
5
6
|
export interface UCExt<OPI0 extends UCOPIBase | undefined = undefined, OPI1 extends UCOPIBase | undefined = undefined> {
|
|
6
7
|
cmd?: {
|
|
7
8
|
mountAt?: UCMountingPoint;
|
|
8
9
|
};
|
|
9
10
|
http?: {
|
|
10
11
|
method?: HTTPMethod;
|
|
11
|
-
mountAt?:
|
|
12
|
+
mountAt?: UCHTTPMountingPoint;
|
|
13
|
+
mountAlsoAt?: UCHTTPMountingPoint[];
|
|
12
14
|
transform?: <T extends object>(output: UCOutput<OPI0, OPI1>) => T;
|
|
13
15
|
};
|
|
14
16
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TUUID } from '../../dt/index.js';
|
|
1
2
|
import { UCOutputField } from '../UCOutputField.js';
|
|
2
3
|
const ERR_NO_FIELD = (key) => `No field ${key} is defined for this use case`;
|
|
3
4
|
const ERR_NO_IO_OUTPUT = 'No output is defined for this use case';
|
|
@@ -64,7 +65,8 @@ export class UCOutputReader {
|
|
|
64
65
|
return undefined;
|
|
65
66
|
}
|
|
66
67
|
const { fields, layout, order, related } = partDef;
|
|
67
|
-
const
|
|
68
|
+
const allFields = { id: { type: new TUUID() }, ...fields };
|
|
69
|
+
const allFieldsKeys = order ?? Object.keys(allFields);
|
|
68
70
|
let items = [];
|
|
69
71
|
let total = 0;
|
|
70
72
|
const part = this._output?.parts[key];
|
|
@@ -73,7 +75,7 @@ export class UCOutputReader {
|
|
|
73
75
|
total = part.total;
|
|
74
76
|
}
|
|
75
77
|
return {
|
|
76
|
-
fields:
|
|
78
|
+
fields: allFieldsKeys.map((fieldKey) => new UCOutputField(fieldKey, allFields[fieldKey])),
|
|
77
79
|
idx,
|
|
78
80
|
items,
|
|
79
81
|
key,
|
|
@@ -10,8 +10,9 @@ export interface UCOutputPart<OPI extends UCOPIBase> {
|
|
|
10
10
|
pagination?: ListInput;
|
|
11
11
|
total: UIntQuantity;
|
|
12
12
|
}
|
|
13
|
+
export type UCOutputPartDefFields<OPI extends UCOPIBase> = Record<StringKeys<OPI>, UCOutputFieldDef<OPI, any>>;
|
|
13
14
|
export interface UCOutputPartDef<OPI extends UCOPIBase> {
|
|
14
|
-
fields: Omit<
|
|
15
|
+
fields: Omit<UCOutputPartDefFields<OPI>, 'id'>;
|
|
15
16
|
layout?: UCOPILayout<OPI>;
|
|
16
17
|
order?: StringKeys<OPI>[];
|
|
17
18
|
related?: {
|
|
@@ -10,5 +10,6 @@ export interface UCHTTPContract {
|
|
|
10
10
|
method: HTTPMethod;
|
|
11
11
|
mountingPoint: UCMountingPoint;
|
|
12
12
|
path: URLPath;
|
|
13
|
+
pathAliases: URLPath[];
|
|
13
14
|
}
|
|
14
15
|
export declare function ucHTTPContract<I extends UCInput | undefined = undefined, OPI0 extends UCOPIBase | undefined = undefined, OPI1 extends UCOPIBase | undefined = undefined>(uc: UC<I, OPI0, OPI1>, pathPrefix?: `/${URLPath}`): UCHTTPContract;
|
|
@@ -24,11 +24,16 @@ export function ucHTTPContract(uc, pathPrefix = '/api/v1') {
|
|
|
24
24
|
}
|
|
25
25
|
const mountingPoint = ucMountingPoint(uc);
|
|
26
26
|
const path = ext?.http?.mountAt ?? `${pathPrefix}/${mountingPoint}`;
|
|
27
|
+
const pathAliases = [];
|
|
28
|
+
if (ext?.http?.mountAlsoAt) {
|
|
29
|
+
pathAliases.push(...ext.http.mountAlsoAt);
|
|
30
|
+
}
|
|
27
31
|
return {
|
|
28
32
|
contentType,
|
|
29
33
|
envelope,
|
|
30
34
|
method,
|
|
31
35
|
mountingPoint,
|
|
32
36
|
path,
|
|
37
|
+
pathAliases,
|
|
33
38
|
};
|
|
34
39
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "libmodulor",
|
|
3
3
|
"description": "An opinionated TypeScript library to create business oriented applications",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.0",
|
|
5
5
|
"license": "LGPL-3.0",
|
|
6
6
|
"author": "Chafik H'nini <chafik.hnini@gmail.com>",
|
|
7
7
|
"homepage": "https://github.com/c100k/libmodulor#readme",
|
|
@@ -31,6 +31,9 @@
|
|
|
31
31
|
"./node": {
|
|
32
32
|
"import": "./dist/esm/index.node.js"
|
|
33
33
|
},
|
|
34
|
+
"./react-native-pure": {
|
|
35
|
+
"import": "./dist/esm/index.react-native-pure.js"
|
|
36
|
+
},
|
|
34
37
|
"./react-web-pure": {
|
|
35
38
|
"import": "./dist/esm/index.react-web-pure.js"
|
|
36
39
|
},
|
|
@@ -55,25 +58,26 @@
|
|
|
55
58
|
},
|
|
56
59
|
"peerDependencies": {
|
|
57
60
|
"@hono/node-server": "^1.13.7",
|
|
58
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
59
|
-
"@stricli/core": "^1.1.
|
|
61
|
+
"@modelcontextprotocol/sdk": "^1.4.1",
|
|
62
|
+
"@stricli/core": "^1.1.1",
|
|
60
63
|
"buffer": "^6.0.3",
|
|
61
64
|
"cookie-parser": "^1.4.7",
|
|
62
65
|
"express": "^4.21.2",
|
|
63
66
|
"express-fileupload": "^1.5.1",
|
|
64
67
|
"fast-check": "^3.23.2",
|
|
65
68
|
"helmet": "^8.0.0",
|
|
66
|
-
"hono": "^4.6.
|
|
69
|
+
"hono": "^4.6.19",
|
|
67
70
|
"inversify": "^6.2.1",
|
|
68
71
|
"jose": "^5.9.6",
|
|
69
72
|
"knex": "^3.1.0",
|
|
70
73
|
"pg": "^8.13.1",
|
|
71
74
|
"react": "^18.3.1",
|
|
72
75
|
"react-dom": "^18.3.1",
|
|
76
|
+
"react-native": "^0.76.6",
|
|
73
77
|
"reflect-metadata": "^0.2.2",
|
|
74
78
|
"sqlite3": "^5.1.7",
|
|
75
79
|
"typescript": "^5.7.3",
|
|
76
|
-
"vite": "^6.0.
|
|
80
|
+
"vite": "^6.0.11"
|
|
77
81
|
},
|
|
78
82
|
"peerDependenciesMeta": {
|
|
79
83
|
"@hono/node-server": {
|
|
@@ -103,6 +107,9 @@
|
|
|
103
107
|
"react-dom": {
|
|
104
108
|
"optional": true
|
|
105
109
|
},
|
|
110
|
+
"react-native": {
|
|
111
|
+
"optional": true
|
|
112
|
+
},
|
|
106
113
|
"sqlite3": {
|
|
107
114
|
"optional": true
|
|
108
115
|
},
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
# Create the project
|
|
2
|
-
|
|
3
|
-
Assuming you have the following installed (otherwise, install them or adapt the commands) :
|
|
4
|
-
|
|
5
|
-
- `node` >= 22
|
|
6
|
-
- `yarn` >= 1.x
|
|
7
|
-
- `wget` and `curl`
|
|
8
|
-
|
|
9
|
-
If you're on macOS, for the `sed` commands, add a `''` after `-i` ([Explanation](https://stackoverflow.com/a/4247319/1259118)).
|
|
10
|
-
|
|
11
|
-
```sh
|
|
12
|
-
# Create the directory
|
|
13
|
-
mkdir libmodulor-tuto && cd libmodulor-tuto # Note how the repository is generic to contain multiple apps and products
|
|
14
|
-
|
|
15
|
-
# Initialize git
|
|
16
|
-
git init
|
|
17
|
-
|
|
18
|
-
# Initialize config files
|
|
19
|
-
touch .gitignore biome.json package.json README.md tsconfig.json vitest.config.ts
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
## .gitignore
|
|
23
|
-
|
|
24
|
-
```.gitignore
|
|
25
|
-
coverage
|
|
26
|
-
dist
|
|
27
|
-
node_modules
|
|
28
|
-
src/apps/**/test/reports
|
|
29
|
-
.env
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## biome.json
|
|
33
|
-
|
|
34
|
-
```json
|
|
35
|
-
{
|
|
36
|
-
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
|
37
|
-
"files": {
|
|
38
|
-
"ignore": ["coverage", "dist", "node_modules"],
|
|
39
|
-
"ignoreUnknown": true
|
|
40
|
-
},
|
|
41
|
-
"formatter": {
|
|
42
|
-
"indentStyle": "space",
|
|
43
|
-
"indentWidth": 4
|
|
44
|
-
},
|
|
45
|
-
"javascript": {
|
|
46
|
-
"formatter": {
|
|
47
|
-
"quoteStyle": "single"
|
|
48
|
-
},
|
|
49
|
-
"parser": {
|
|
50
|
-
"unsafeParameterDecoratorsEnabled": true
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
## package.json
|
|
57
|
-
|
|
58
|
-
```json
|
|
59
|
-
{
|
|
60
|
-
"name": "libmodulor-tuto",
|
|
61
|
-
"version": "0.1.0",
|
|
62
|
-
"author": "Chafik H'nini <chafik.hnini@gmail.com>",
|
|
63
|
-
"type": "module",
|
|
64
|
-
"private": true,
|
|
65
|
-
"scripts": {
|
|
66
|
-
"helper": "node ./node_modules/libmodulor/dist/esm/products/Helper/index.js",
|
|
67
|
-
"lint": "biome check --write .",
|
|
68
|
-
"test": "tsc && vitest run"
|
|
69
|
-
},
|
|
70
|
-
"dependencies": {
|
|
71
|
-
"inversify": "^6.2.1",
|
|
72
|
-
"libmodulor": "^0.2.0",
|
|
73
|
-
"reflect-metadata": "^0.2.2"
|
|
74
|
-
},
|
|
75
|
-
"devDependencies": {
|
|
76
|
-
"@biomejs/biome": "^1.9.4",
|
|
77
|
-
"@types/node": "^22.10.2",
|
|
78
|
-
"@vitest/coverage-v8": "^2.1.1",
|
|
79
|
-
"buffer": "^6.0.3",
|
|
80
|
-
"cookie-parser": "^1.4.7",
|
|
81
|
-
"express": "^4.21.2",
|
|
82
|
-
"express-fileupload": "^1.5.1",
|
|
83
|
-
"fast-check": "^3.23.2",
|
|
84
|
-
"helmet": "^8.0.0",
|
|
85
|
-
"jose": "^5.9.6",
|
|
86
|
-
"typescript": "^5.7.2",
|
|
87
|
-
"vite": "^6.0.5",
|
|
88
|
-
"vitest": "^2.1.8"
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
## README.md
|
|
94
|
-
|
|
95
|
-
```md
|
|
96
|
-
# libmodulor-tuto
|
|
97
|
-
|
|
98
|
-
🚀🚀🚀
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
## tsconfig.json
|
|
102
|
-
|
|
103
|
-
```json
|
|
104
|
-
{
|
|
105
|
-
"compilerOptions": {
|
|
106
|
-
"allowSyntheticDefaultImports": true,
|
|
107
|
-
"declaration": true,
|
|
108
|
-
"lib": ["dom", "esnext"],
|
|
109
|
-
"module": "NodeNext",
|
|
110
|
-
"moduleResolution": "NodeNext",
|
|
111
|
-
"noEmit": true,
|
|
112
|
-
"removeComments": true,
|
|
113
|
-
"skipLibCheck": true,
|
|
114
|
-
"sourceMap": true,
|
|
115
|
-
"target": "ESNext",
|
|
116
|
-
|
|
117
|
-
"strict": true,
|
|
118
|
-
"allowUnreachableCode": false,
|
|
119
|
-
"allowUnusedLabels": false,
|
|
120
|
-
"exactOptionalPropertyTypes": true,
|
|
121
|
-
"noFallthroughCasesInSwitch": true,
|
|
122
|
-
"noPropertyAccessFromIndexSignature": true,
|
|
123
|
-
"noImplicitOverride": true,
|
|
124
|
-
"noImplicitReturns": true,
|
|
125
|
-
"noUncheckedIndexedAccess": true,
|
|
126
|
-
"noUnusedLocals": true,
|
|
127
|
-
"noUnusedParameters": true,
|
|
128
|
-
"verbatimModuleSyntax": true,
|
|
129
|
-
|
|
130
|
-
"emitDecoratorMetadata": true,
|
|
131
|
-
"experimentalDecorators": true,
|
|
132
|
-
|
|
133
|
-
"jsx": "react"
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
## vitest.config.ts
|
|
139
|
-
|
|
140
|
-
```typescript
|
|
141
|
-
import { defineConfig } from 'vitest/config';
|
|
142
|
-
|
|
143
|
-
export default defineConfig({
|
|
144
|
-
test: {
|
|
145
|
-
coverage: {
|
|
146
|
-
enabled: true,
|
|
147
|
-
exclude: ['src/apps/**/test', 'src/**/*.test.ts'],
|
|
148
|
-
include: ['src'],
|
|
149
|
-
reporter: ['html', 'lcov', 'text'],
|
|
150
|
-
},
|
|
151
|
-
reporters: ['verbose'],
|
|
152
|
-
},
|
|
153
|
-
});
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
## Install
|
|
157
|
-
|
|
158
|
-
```sh
|
|
159
|
-
yarn install
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
```sh
|
|
163
|
-
yarn lint && git add . && git commit -m "chore: init source code"
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
Optionally, you can create a remote repository (e.g. on GitHub) and push it.
|
|
167
|
-
|
|
168
|
-
Now that's done, let's [Create the App](./002_Create_the_App.md).
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
# Create the App
|
|
2
|
-
|
|
3
|
-
An app is composed of three main files : `i18n.ts`, `manifest.ts` and `index.ts`.
|
|
4
|
-
|
|
5
|
-
```sh
|
|
6
|
-
mkdir -p src/apps/Trading/src/ucds
|
|
7
|
-
touch src/apps/Trading/src/{i18n.ts,manifest.ts}
|
|
8
|
-
touch src/apps/Trading/index.ts
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
> [!NOTE]
|
|
12
|
-
> There is a lot of controversy about barrel files. In this specific context, they are useful to only expose the necessary things to the upper layers and keep the app isolated.
|
|
13
|
-
|
|
14
|
-
## i18n.ts
|
|
15
|
-
|
|
16
|
-
```typescript
|
|
17
|
-
import type { AppI18n } from 'libmodulor';
|
|
18
|
-
|
|
19
|
-
export const I18n: AppI18n = {
|
|
20
|
-
en: {},
|
|
21
|
-
};
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
## manifest.ts
|
|
25
|
-
|
|
26
|
-
```typescript
|
|
27
|
-
import type { AppManifest } from 'libmodulor';
|
|
28
|
-
|
|
29
|
-
export const Manifest = {
|
|
30
|
-
languageCodes: ['en'],
|
|
31
|
-
name: 'Trading',
|
|
32
|
-
ucReg: {},
|
|
33
|
-
} satisfies AppManifest;
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
## index.ts
|
|
37
|
-
|
|
38
|
-
```typescript
|
|
39
|
-
// Expose only what's necessary
|
|
40
|
-
|
|
41
|
-
export { I18n } from './src/i18n.js';
|
|
42
|
-
export { Manifest } from './src/manifest.js';
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
```sh
|
|
46
|
-
yarn lint && git add . && git commit -m "feat: add the app"
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
Now that's done, let's [Create the UseCase](./003_Create_the_UseCase.md).
|
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
# Create the UseCase
|
|
2
|
-
|
|
3
|
-
> [!NOTE]
|
|
4
|
-
> Starting now, you'll see `UC` or `uc` a lot. It's the abbreviation of `UseCase`. Acronyms are not good in codebases, except those that are commonly used ([debate](https://stackoverflow.com/questions/2236807/java-naming-convention-with-acronyms)). In any case, when you write `UseCase` hundreds of times, you're happy to be able to write `UC` instead. Thus, `UCD` stands for `Use Case Definition`, `UCIF` stands for `Use Case Input Field` and so on.
|
|
5
|
-
|
|
6
|
-
The app manifest registers all the use cases metadata. Use cases can depend on each other as we'll see later, and this dependency must go through the manifest. Never directly.
|
|
7
|
-
|
|
8
|
-
Update `manifest.ts` to register the new use case.
|
|
9
|
-
|
|
10
|
-
```typescript
|
|
11
|
-
// ...
|
|
12
|
-
ucReg: {
|
|
13
|
-
BuyAsset: {
|
|
14
|
-
action: 'Create',
|
|
15
|
-
icon: 'plus',
|
|
16
|
-
name: 'BuyAsset',
|
|
17
|
-
},
|
|
18
|
-
},
|
|
19
|
-
// ...
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
If you're using an IDE with auto-complete, you might have noticed the other properties like `beta`, `new`, `sensitive`. We'll come back to them later.
|
|
23
|
-
|
|
24
|
-
```sh
|
|
25
|
-
mkdir src/apps/Trading/src/dt
|
|
26
|
-
touch src/apps/Trading/src/dt/TISIN.ts
|
|
27
|
-
touch src/apps/Trading/src/ucds/{BuyAssetServerMain.ts,BuyAssetUCD.ts}
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
## TISIN.ts
|
|
31
|
-
|
|
32
|
-
An asset is usually identified by a unique code called [ISIN](https://www.isin.org). This is a typical business data type that has specific rules and that is not simply a `string`.
|
|
33
|
-
|
|
34
|
-
```typescript
|
|
35
|
-
import { type TName, TString, type TStringConstraints } from 'libmodulor';
|
|
36
|
-
|
|
37
|
-
export type ISIN = Capitalize<string>;
|
|
38
|
-
|
|
39
|
-
export class TISIN extends TString<ISIN, 'ISIN'> {
|
|
40
|
-
public static readonly FORMAT: RegExp = /^[A-Z]{2}[A-Z0-9]{9}[0-9]$/;
|
|
41
|
-
|
|
42
|
-
constructor(constraints?: TStringConstraints) {
|
|
43
|
-
super({
|
|
44
|
-
...constraints,
|
|
45
|
-
format: { f: 'ISIN', regexp: TISIN.FORMAT },
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
public override tName(): TName {
|
|
50
|
-
return 'ISIN';
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
public override example(): ISIN {
|
|
54
|
-
return 'US02079K3059';
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
## BuyAssetUCD.ts
|
|
60
|
-
|
|
61
|
-
```typescript
|
|
62
|
-
import {
|
|
63
|
-
type AggregateOPI0,
|
|
64
|
-
type Amount,
|
|
65
|
-
EverybodyUCPolicy,
|
|
66
|
-
TAmount,
|
|
67
|
-
TBoolean,
|
|
68
|
-
TUIntQuantity,
|
|
69
|
-
type UCDef,
|
|
70
|
-
type UCInput,
|
|
71
|
-
type UCInputFieldValue,
|
|
72
|
-
type UCMain,
|
|
73
|
-
type UCMainInput,
|
|
74
|
-
type UCOutputOrNothing,
|
|
75
|
-
type UCTransporter,
|
|
76
|
-
type UIntQuantity,
|
|
77
|
-
} from 'libmodulor';
|
|
78
|
-
import { inject, injectable } from 'inversify';
|
|
79
|
-
|
|
80
|
-
import { Manifest } from '../manifest.js';
|
|
81
|
-
|
|
82
|
-
import { type ISIN, TISIN } from '../dt/TISIN.js';
|
|
83
|
-
import { BuyAssetServerMain } from './BuyAssetServerMain.js';
|
|
84
|
-
|
|
85
|
-
export interface BuyAssetInput extends UCInput {
|
|
86
|
-
isin: UCInputFieldValue<ISIN>;
|
|
87
|
-
limit: UCInputFieldValue<Amount>;
|
|
88
|
-
qty: UCInputFieldValue<UIntQuantity>;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export interface BuyAssetOPI0 extends AggregateOPI0 {
|
|
92
|
-
executedDirectly: boolean;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
@injectable()
|
|
96
|
-
class BuyAssetClientMain implements UCMain<BuyAssetInput, BuyAssetOPI0> {
|
|
97
|
-
constructor(
|
|
98
|
-
@inject('UCTransporter')
|
|
99
|
-
private ucTransporter: UCTransporter,
|
|
100
|
-
) {}
|
|
101
|
-
|
|
102
|
-
public async exec({
|
|
103
|
-
uc,
|
|
104
|
-
}: UCMainInput<BuyAssetInput, BuyAssetOPI0>): Promise<
|
|
105
|
-
UCOutputOrNothing<BuyAssetOPI0>
|
|
106
|
-
> {
|
|
107
|
-
return this.ucTransporter.send(uc);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export const BuyAssetUCD: UCDef<BuyAssetInput, BuyAssetOPI0> = {
|
|
112
|
-
io: {
|
|
113
|
-
i: {
|
|
114
|
-
fields: {
|
|
115
|
-
isin: {
|
|
116
|
-
type: new TISIN(),
|
|
117
|
-
},
|
|
118
|
-
limit: {
|
|
119
|
-
type: new TAmount('USD'),
|
|
120
|
-
},
|
|
121
|
-
qty: {
|
|
122
|
-
type: new TUIntQuantity(),
|
|
123
|
-
},
|
|
124
|
-
},
|
|
125
|
-
},
|
|
126
|
-
o: {
|
|
127
|
-
parts: {
|
|
128
|
-
_0: {
|
|
129
|
-
fields: {
|
|
130
|
-
executedDirectly: {
|
|
131
|
-
type: new TBoolean(),
|
|
132
|
-
},
|
|
133
|
-
},
|
|
134
|
-
},
|
|
135
|
-
},
|
|
136
|
-
},
|
|
137
|
-
},
|
|
138
|
-
lifecycle: {
|
|
139
|
-
client: {
|
|
140
|
-
main: BuyAssetClientMain,
|
|
141
|
-
policy: EverybodyUCPolicy,
|
|
142
|
-
},
|
|
143
|
-
server: {
|
|
144
|
-
main: BuyAssetServerMain,
|
|
145
|
-
policy: EverybodyUCPolicy,
|
|
146
|
-
},
|
|
147
|
-
},
|
|
148
|
-
metadata: Manifest.ucReg.BuyAsset,
|
|
149
|
-
};
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
## BuyAssetServerMain.ts
|
|
153
|
-
|
|
154
|
-
```typescript
|
|
155
|
-
import {
|
|
156
|
-
type UCMain,
|
|
157
|
-
type UCMainInput,
|
|
158
|
-
type UCManager,
|
|
159
|
-
type UCOutput,
|
|
160
|
-
UCOutputBuilder,
|
|
161
|
-
} from 'libmodulor';
|
|
162
|
-
import { inject, injectable } from 'inversify';
|
|
163
|
-
|
|
164
|
-
import type { BuyAssetInput, BuyAssetOPI0 } from './BuyAssetUCD.js';
|
|
165
|
-
|
|
166
|
-
@injectable()
|
|
167
|
-
export class BuyAssetServerMain implements UCMain<BuyAssetInput, BuyAssetOPI0> {
|
|
168
|
-
constructor(@inject('UCManager') private ucManager: UCManager) {}
|
|
169
|
-
|
|
170
|
-
public async exec({
|
|
171
|
-
uc,
|
|
172
|
-
}: UCMainInput<BuyAssetInput, BuyAssetOPI0>): Promise<
|
|
173
|
-
UCOutput<BuyAssetOPI0>
|
|
174
|
-
> {
|
|
175
|
-
// >=> Persist the order
|
|
176
|
-
const { aggregateId } = await this.ucManager.persist(uc);
|
|
177
|
-
|
|
178
|
-
// >=> TODO : Check the user has enough funds to place the order
|
|
179
|
-
|
|
180
|
-
// >=> TODO : Send the order to a queue for processing
|
|
181
|
-
const executedDirectly: BuyAssetOPI0['executedDirectly'] = false;
|
|
182
|
-
|
|
183
|
-
return new UCOutputBuilder<BuyAssetOPI0>()
|
|
184
|
-
.add({
|
|
185
|
-
executedDirectly,
|
|
186
|
-
id: aggregateId,
|
|
187
|
-
})
|
|
188
|
-
.get();
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
For now, we won't detail all this code but take the time to read it and understand how it works. Hopefully it's clear enough and self-explanatory.
|
|
194
|
-
|
|
195
|
-
> [!TIP]
|
|
196
|
-
> Using a comment following the pattern `// >=> ` in `ClientMain` and `ServerMain` has a specific meaning as we'll see a little bit later.
|
|
197
|
-
|
|
198
|
-
> [!NOTE]
|
|
199
|
-
> Unlike `ClientMain`, `ServerMain` is put in another file for "historical" reasons, mainly for stripping and tree shaking reasons. More on this later.
|
|
200
|
-
|
|
201
|
-
```sh
|
|
202
|
-
yarn lint && git add . && git commit -m "feat: add the use case"
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
Now that's done, let's [Test the App](./004_Test_the_App.md).
|