libmodulor 0.1.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/CHANGELOG.md +5 -0
- package/README.md +1485 -0
- package/dist/esm/index.js +1 -0
- 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/package.json +111 -0
package/README.md
ADDED
|
@@ -0,0 +1,1485 @@
|
|
|
1
|
+
# libmodulor
|
|
2
|
+
|
|
3
|
+
An opinionated TypeScript library to create business oriented applications.
|
|
4
|
+
|
|
5
|
+
Applications created with `libmodulor` have **6 main properties** :
|
|
6
|
+
|
|
7
|
+
- Strictly typed with explicit business data types
|
|
8
|
+
- Fully typed e2e without code generation
|
|
9
|
+
- Auto documented
|
|
10
|
+
- Auto tested
|
|
11
|
+
- Multi platforms/runtimes
|
|
12
|
+
- Runnable anywhere
|
|
13
|
+
|
|
14
|
+
> [!WARNING]
|
|
15
|
+
> The project is still in active development. Although already used in pilot projects, it's not suitable for all production scenarios yet.
|
|
16
|
+
> Being developed by only one person, it may keep going for years or stop at any time.
|
|
17
|
+
> In the meantime, it's still a "research project" that needs improvement. Thus, it will be subject to BREAKING CHANGES as long as the version is not 1.0.0.
|
|
18
|
+
|
|
19
|
+
> [!NOTE]
|
|
20
|
+
> At the beginning, the whole documentation will be contained in this single file to make it easier to have the big picture and <kbd>cmd</kbd> + <kbd>F</kbd>. At some point, each section will move to a dedicated page. I'll even consider having a beautiful documentation website like all the cool kids.
|
|
21
|
+
|
|
22
|
+
All that said, the end goal is really to have a **production-grade library** to help everyone build **quality projects faster**. If you want to help in any way or have questions, feel free to contact me (cf. `author` in `package.json`).
|
|
23
|
+
|
|
24
|
+
## Philosophy
|
|
25
|
+
|
|
26
|
+
One might argue that, with so many "JS frameworks" on the market, there are already too many ways to build new applications today. And they would be right.
|
|
27
|
+
|
|
28
|
+
That's why the angle taken by `libmodulor` is different. Although opinionated about some things (see below), it is not, regarding the technical side. Instead, it focuses mainly on the "core" of your application.
|
|
29
|
+
|
|
30
|
+
Thus, you are free to use :
|
|
31
|
+
|
|
32
|
+
- the data store of your choice (PostgreSQL, MySQL, MariaDB, DynamoDB, SQLite, MongoDB...),
|
|
33
|
+
- the frontend framework of your choice (React, Svelte, Angular, Vue, Solid...),
|
|
34
|
+
- the server of your choice (Express, Fastify, Hono...),
|
|
35
|
+
- the meta framework of your choice (Next, Remix, Astro, Nuxt...),
|
|
36
|
+
- the runtime of your choice (Node, Deno, Bun...)
|
|
37
|
+
- the libraries of your choice (Lodash, React Query...)
|
|
38
|
+
- the tools of your choice (Biome, ESLint, Prettier...)
|
|
39
|
+
- the styling library of your choice for web (tailwind, shadcn, bootstrap, vanilla CSS...)
|
|
40
|
+
- the hosting of your choice (Cloud, IaaS, PaaS, On-Prem, RaspberryPi, your fridge...)
|
|
41
|
+
|
|
42
|
+
The main goal is to offer higher level primitives that make building business applications faster, without having to use a boilerplate or worse, no/low code, and thus, avoid vendor lock-in.
|
|
43
|
+
|
|
44
|
+
## How it works
|
|
45
|
+
|
|
46
|
+
The library defines a **4-layer architecture** composed of : `UseCase`, `App`, `Product`, `Target`.
|
|
47
|
+
|
|
48
|
+
```mermaid
|
|
49
|
+
block-beta
|
|
50
|
+
Target1:2
|
|
51
|
+
Target2:2
|
|
52
|
+
Target3:2
|
|
53
|
+
columns 6
|
|
54
|
+
Product1:6
|
|
55
|
+
App1:3
|
|
56
|
+
App2:3
|
|
57
|
+
UseCase1
|
|
58
|
+
UseCase2
|
|
59
|
+
UseCase3
|
|
60
|
+
UseCase4
|
|
61
|
+
UseCase6
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### UseCase
|
|
65
|
+
|
|
66
|
+
A use case is the smallest unit. It defines the contract, mainly as an `Input` that goes into lifecycle methods (`client` and/or `server`) to finally give an `Output`. In the end, it constitutes a piece of business functionality.
|
|
67
|
+
|
|
68
|
+
Inspired by [UML's Use case diagram](https://en.wikipedia.org/wiki/Use_case_diagram) and [Event-driven architecture](https://en.wikipedia.org/wiki/Event-driven_architecture), schematically, it could be defined as follows :
|
|
69
|
+
|
|
70
|
+
```math
|
|
71
|
+
O = clientMain(serverMain(I))
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
_Examples : `SignIn`, `CreatePost`, `TransferAccount`, `InviteContacts`_...
|
|
75
|
+
|
|
76
|
+
Note how it always starts with a verb.
|
|
77
|
+
|
|
78
|
+
### App
|
|
79
|
+
|
|
80
|
+
An app is a logical group of use cases.
|
|
81
|
+
|
|
82
|
+
It's like a "module" (_whatever that means_), inspired by [Domain-driven design (DDD)](https://en.wikipedia.org/wiki/Domain-driven_design) bounded contexts.
|
|
83
|
+
|
|
84
|
+
_Examples : `Auth`, `Accounting`, `CMS`..._
|
|
85
|
+
|
|
86
|
+
### Product
|
|
87
|
+
|
|
88
|
+
A product is a logical group of apps that are assembled together.
|
|
89
|
+
|
|
90
|
+
Behind this barbaric definition, it's simply what end users know and use.
|
|
91
|
+
|
|
92
|
+
_Examples : `GitHub`, `Facebook`, `LinkedIn`, `Airbnb`..._
|
|
93
|
+
|
|
94
|
+
When defined correctly, apps are reusable across multiple products (e.g. `Auth`).
|
|
95
|
+
|
|
96
|
+
### Target
|
|
97
|
+
|
|
98
|
+
A target defines how a product is "exposed" to the end user. It's a combination of platform and runtime.
|
|
99
|
+
|
|
100
|
+
_Examples : `web-react`, `web-angular`, `server-node`, `cli-node`, `cli-stricli`..._
|
|
101
|
+
|
|
102
|
+
Note that it's the only place where the "infrastructure" choices are applied.
|
|
103
|
+
|
|
104
|
+
## Getting Started
|
|
105
|
+
|
|
106
|
+
Enough theory, let's dive in and learn by doing.
|
|
107
|
+
|
|
108
|
+
> [!NOTE]
|
|
109
|
+
> This Guide is voluntarily very verbose and not scripted so you can get a full overview of how things work. `npx` magic is good. But understanding what happens behind the scenes is good as well.
|
|
110
|
+
|
|
111
|
+
In this Guide, we'll init a repository (a repository can contain multiple apps and products) and create a real life application using the `libmodulor` primitives.
|
|
112
|
+
|
|
113
|
+
We'll build a small trading application. It will contain one `App` named `Trading`, which will contain one `UseCase` named `BuyAsset`. The `App` will be mounted in a `Product` called `SuperTrader` which will be exposed via a `server` `Target`, a `web` `Target`, a `cli` `Target` and finally, a `mcp-server` `Target`.
|
|
114
|
+
|
|
115
|
+
> [!NOTE]
|
|
116
|
+
> MCP stands for [Model Context Protocol](https://modelcontextprotocol.io) introduced recently by [@anthropics](https://github.com/anthropics).
|
|
117
|
+
|
|
118
|
+
If we adapt the abstract mermaid chart displayed above, concretely, it looks like this :
|
|
119
|
+
|
|
120
|
+
```mermaid
|
|
121
|
+
block-beta
|
|
122
|
+
server
|
|
123
|
+
web
|
|
124
|
+
cli
|
|
125
|
+
mcp_server
|
|
126
|
+
columns 4
|
|
127
|
+
SuperTrader:4
|
|
128
|
+
Trading:2
|
|
129
|
+
Auth:2
|
|
130
|
+
BuyAsset
|
|
131
|
+
ListOrders
|
|
132
|
+
SignUp
|
|
133
|
+
SignIn
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Note that we'll only develop one use case in this Guide but you get the idea.
|
|
137
|
+
|
|
138
|
+
### Create the project
|
|
139
|
+
|
|
140
|
+
> [!IMPORTANT]
|
|
141
|
+
> At this stage, I'm following a documentation and spec first process. Thus, the source code is not yet published, just like the package on `npm`. Therefore, the commands below are not fully executable since `yarn install` will fail fetching the dependency. Currently, the main goal is to expose the docs, optimize the process, and most of all, get feedback on the mechanism. Thank you.
|
|
142
|
+
|
|
143
|
+
Assuming you have the following installed (otherwise, install them or adapt the commands) :
|
|
144
|
+
|
|
145
|
+
- `node` >= 22
|
|
146
|
+
- `yarn` >= 1.x
|
|
147
|
+
- `wget` and `curl`
|
|
148
|
+
|
|
149
|
+
If you're on macOS, for the `sed` commands, add a `''` after `-i` ([Explanation](https://stackoverflow.com/a/4247319/1259118)).
|
|
150
|
+
|
|
151
|
+
```sh
|
|
152
|
+
# Create the directory
|
|
153
|
+
mkdir libmodulor-tuto && cd libmodulor-tuto # Note how the repository is generic to contain multiple apps and products
|
|
154
|
+
|
|
155
|
+
# Initialize git
|
|
156
|
+
git init
|
|
157
|
+
|
|
158
|
+
# Initialize config files
|
|
159
|
+
touch .gitignore biome.json package.json README.md tsconfig.json vitest.config.ts
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
#### .gitignore
|
|
163
|
+
|
|
164
|
+
```.gitignore
|
|
165
|
+
coverage
|
|
166
|
+
dist
|
|
167
|
+
node_modules
|
|
168
|
+
src/apps/**/test/reports
|
|
169
|
+
.env
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
#### biome.json
|
|
173
|
+
|
|
174
|
+
```json
|
|
175
|
+
{
|
|
176
|
+
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
|
177
|
+
"files": {
|
|
178
|
+
"ignore": ["coverage", "dist", "node_modules"],
|
|
179
|
+
"ignoreUnknown": true
|
|
180
|
+
},
|
|
181
|
+
"formatter": {
|
|
182
|
+
"indentStyle": "space",
|
|
183
|
+
"indentWidth": 4
|
|
184
|
+
},
|
|
185
|
+
"javascript": {
|
|
186
|
+
"formatter": {
|
|
187
|
+
"quoteStyle": "single"
|
|
188
|
+
},
|
|
189
|
+
"parser": {
|
|
190
|
+
"unsafeParameterDecoratorsEnabled": true
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
#### package.json
|
|
197
|
+
|
|
198
|
+
```json
|
|
199
|
+
{
|
|
200
|
+
"name": "libmodulor-tuto",
|
|
201
|
+
"version": "0.1.0",
|
|
202
|
+
"author": "Chafik H'nini <chafik.hnini@gmail.com>",
|
|
203
|
+
"type": "module",
|
|
204
|
+
"private": true,
|
|
205
|
+
"scripts": {
|
|
206
|
+
"cli": "node ./node_modules/libmodulor/dist/esm/products/Wizard/index.js",
|
|
207
|
+
"lint": "biome check --write .",
|
|
208
|
+
"test": "tsc && vitest run"
|
|
209
|
+
},
|
|
210
|
+
"dependencies": {
|
|
211
|
+
"inversify": "^6.2.1",
|
|
212
|
+
"libmodulor": "c100k/libmodulor#master",
|
|
213
|
+
"reflect-metadata": "^0.2.2"
|
|
214
|
+
},
|
|
215
|
+
"devDependencies": {
|
|
216
|
+
"@biomejs/biome": "^1.9.4",
|
|
217
|
+
"@types/node": "^22.10.2",
|
|
218
|
+
"@vitest/coverage-v8": "^2.1.1",
|
|
219
|
+
"buffer": "^6.0.3",
|
|
220
|
+
"cookie-parser": "^1.4.7",
|
|
221
|
+
"express": "^4.21.2",
|
|
222
|
+
"express-fileupload": "^1.5.1",
|
|
223
|
+
"fast-check": "^3.23.2",
|
|
224
|
+
"helmet": "^8.0.0",
|
|
225
|
+
"jose": "^5.9.6",
|
|
226
|
+
"typescript": "^5.7.2",
|
|
227
|
+
"vite": "^6.0.5",
|
|
228
|
+
"vitest": "^2.1.8"
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
#### README.md
|
|
234
|
+
|
|
235
|
+
```md
|
|
236
|
+
# libmodulor-tuto
|
|
237
|
+
|
|
238
|
+
🚀🚀🚀
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
#### tsconfig.json
|
|
242
|
+
|
|
243
|
+
```json
|
|
244
|
+
{
|
|
245
|
+
"compilerOptions": {
|
|
246
|
+
"allowSyntheticDefaultImports": true,
|
|
247
|
+
"declaration": true,
|
|
248
|
+
"lib": ["dom", "esnext"],
|
|
249
|
+
"module": "NodeNext",
|
|
250
|
+
"moduleResolution": "NodeNext",
|
|
251
|
+
"noEmit": true,
|
|
252
|
+
"removeComments": true,
|
|
253
|
+
"skipLibCheck": true,
|
|
254
|
+
"sourceMap": true,
|
|
255
|
+
"target": "ESNext",
|
|
256
|
+
|
|
257
|
+
"strict": true,
|
|
258
|
+
"allowUnreachableCode": false,
|
|
259
|
+
"allowUnusedLabels": false,
|
|
260
|
+
"exactOptionalPropertyTypes": true,
|
|
261
|
+
"noFallthroughCasesInSwitch": true,
|
|
262
|
+
"noPropertyAccessFromIndexSignature": true,
|
|
263
|
+
"noImplicitOverride": true,
|
|
264
|
+
"noImplicitReturns": true,
|
|
265
|
+
"noUncheckedIndexedAccess": true,
|
|
266
|
+
"noUnusedLocals": true,
|
|
267
|
+
"noUnusedParameters": true,
|
|
268
|
+
"verbatimModuleSyntax": true,
|
|
269
|
+
|
|
270
|
+
"emitDecoratorMetadata": true,
|
|
271
|
+
"experimentalDecorators": true,
|
|
272
|
+
|
|
273
|
+
"jsx": "react"
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
#### vitest.config.ts
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
import { defineConfig } from 'vitest/config';
|
|
282
|
+
|
|
283
|
+
export default defineConfig({
|
|
284
|
+
test: {
|
|
285
|
+
coverage: {
|
|
286
|
+
enabled: true,
|
|
287
|
+
exclude: ['src/apps/**/test', 'src/**/*.test.ts'],
|
|
288
|
+
include: ['src'],
|
|
289
|
+
reporter: ['html', 'lcov', 'text'],
|
|
290
|
+
},
|
|
291
|
+
reporters: ['verbose'],
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
#### Install
|
|
297
|
+
|
|
298
|
+
```sh
|
|
299
|
+
yarn install
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
```sh
|
|
303
|
+
yarn lint && git add . && git commit -m "chore: init source code"
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Optionally, you can create a remote repository (e.g. on GitHub) and push it.
|
|
307
|
+
|
|
308
|
+
### Create the App
|
|
309
|
+
|
|
310
|
+
An app is composed of three main files : `i18n.ts`, `manifest.ts` and `index.ts`.
|
|
311
|
+
|
|
312
|
+
```sh
|
|
313
|
+
mkdir -p src/apps/Trading/src/ucds
|
|
314
|
+
touch src/apps/Trading/src/{i18n.ts,manifest.ts}
|
|
315
|
+
touch src/apps/Trading/index.ts
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
> [!NOTE]
|
|
319
|
+
> 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.
|
|
320
|
+
|
|
321
|
+
#### i18n.ts
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
import type { AppI18n } from 'libmodulor';
|
|
325
|
+
|
|
326
|
+
export const I18n: AppI18n = {
|
|
327
|
+
en: {},
|
|
328
|
+
};
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
#### manifest.ts
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
import type { AppManifest } from 'libmodulor';
|
|
335
|
+
|
|
336
|
+
export const Manifest = {
|
|
337
|
+
languageCodes: ['en'],
|
|
338
|
+
name: 'Trading',
|
|
339
|
+
ucReg: {},
|
|
340
|
+
} satisfies AppManifest;
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
#### index.ts
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
// Expose only what's necessary
|
|
347
|
+
|
|
348
|
+
export { I18n } from './src/i18n.js';
|
|
349
|
+
export { Manifest } from './src/manifest.js';
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
```sh
|
|
353
|
+
yarn lint && git add . && git commit -m "feat: add the app"
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### Create the UseCase
|
|
357
|
+
|
|
358
|
+
> [!NOTE]
|
|
359
|
+
> 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.
|
|
360
|
+
|
|
361
|
+
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.
|
|
362
|
+
|
|
363
|
+
Update `manifest.ts` to register the new use case.
|
|
364
|
+
|
|
365
|
+
```typescript
|
|
366
|
+
// ...
|
|
367
|
+
ucReg: {
|
|
368
|
+
BuyAsset: {
|
|
369
|
+
action: 'Create',
|
|
370
|
+
icon: 'plus',
|
|
371
|
+
name: 'BuyAsset',
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
// ...
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
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.
|
|
378
|
+
|
|
379
|
+
```sh
|
|
380
|
+
mkdir src/apps/Trading/src/dt
|
|
381
|
+
touch src/apps/Trading/src/dt/TISIN.ts
|
|
382
|
+
touch src/apps/Trading/src/ucds/{BuyAssetServerMain.ts,BuyAssetUCD.ts}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
#### TISIN.ts
|
|
386
|
+
|
|
387
|
+
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`.
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
import { type TName, TString, type TStringConstraints } from 'libmodulor';
|
|
391
|
+
|
|
392
|
+
export type ISIN = Capitalize<string>;
|
|
393
|
+
|
|
394
|
+
export class TISIN extends TString<ISIN, 'ISIN'> {
|
|
395
|
+
public static readonly FORMAT: RegExp = /^[A-Z]{2}[A-Z0-9]{9}[0-9]$/;
|
|
396
|
+
|
|
397
|
+
constructor(constraints?: TStringConstraints) {
|
|
398
|
+
super({
|
|
399
|
+
...constraints,
|
|
400
|
+
format: { f: 'ISIN', regexp: TISIN.FORMAT },
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
public override tName(): TName {
|
|
405
|
+
return 'ISIN';
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
public override example(): ISIN {
|
|
409
|
+
return 'US02079K3059';
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
#### BuyAssetUCD.ts
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
import {
|
|
418
|
+
type AggregateOPI0,
|
|
419
|
+
type Amount,
|
|
420
|
+
EverybodyUCPolicy,
|
|
421
|
+
TAmount,
|
|
422
|
+
TBoolean,
|
|
423
|
+
TUIntQuantity,
|
|
424
|
+
type UCDef,
|
|
425
|
+
type UCInput,
|
|
426
|
+
type UCInputFieldValue,
|
|
427
|
+
type UCMain,
|
|
428
|
+
type UCMainInput,
|
|
429
|
+
type UCOutputOrNothing,
|
|
430
|
+
type UCTransporter,
|
|
431
|
+
type UIntQuantity,
|
|
432
|
+
} from 'libmodulor';
|
|
433
|
+
import { inject, injectable } from 'inversify';
|
|
434
|
+
|
|
435
|
+
import { Manifest } from '../manifest.js';
|
|
436
|
+
|
|
437
|
+
import { type ISIN, TISIN } from '../dt/TISIN.js';
|
|
438
|
+
import { BuyAssetServerMain } from './BuyAssetServerMain.js';
|
|
439
|
+
|
|
440
|
+
export interface BuyAssetInput extends UCInput {
|
|
441
|
+
isin: UCInputFieldValue<ISIN>;
|
|
442
|
+
limit: UCInputFieldValue<Amount>;
|
|
443
|
+
qty: UCInputFieldValue<UIntQuantity>;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export interface BuyAssetOPI0 extends AggregateOPI0 {
|
|
447
|
+
executedDirectly: boolean;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
@injectable()
|
|
451
|
+
class BuyAssetClientMain implements UCMain<BuyAssetInput, BuyAssetOPI0> {
|
|
452
|
+
constructor(
|
|
453
|
+
@inject('UCTransporter')
|
|
454
|
+
private ucTransporter: UCTransporter,
|
|
455
|
+
) {}
|
|
456
|
+
|
|
457
|
+
public async exec({
|
|
458
|
+
uc,
|
|
459
|
+
}: UCMainInput<BuyAssetInput, BuyAssetOPI0>): Promise<
|
|
460
|
+
UCOutputOrNothing<BuyAssetOPI0>
|
|
461
|
+
> {
|
|
462
|
+
return this.ucTransporter.send(uc);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export const BuyAssetUCD: UCDef<BuyAssetInput, BuyAssetOPI0> = {
|
|
467
|
+
io: {
|
|
468
|
+
input: {
|
|
469
|
+
fields: {
|
|
470
|
+
isin: {
|
|
471
|
+
type: new TISIN(),
|
|
472
|
+
},
|
|
473
|
+
limit: {
|
|
474
|
+
type: new TAmount('USD'),
|
|
475
|
+
},
|
|
476
|
+
qty: {
|
|
477
|
+
type: new TUIntQuantity(),
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
output: {
|
|
482
|
+
parts: {
|
|
483
|
+
_0: {
|
|
484
|
+
fields: {
|
|
485
|
+
executedDirectly: {
|
|
486
|
+
type: new TBoolean(),
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
},
|
|
493
|
+
lifecycle: {
|
|
494
|
+
client: {
|
|
495
|
+
main: BuyAssetClientMain,
|
|
496
|
+
policy: EverybodyUCPolicy,
|
|
497
|
+
},
|
|
498
|
+
server: {
|
|
499
|
+
main: BuyAssetServerMain,
|
|
500
|
+
policy: EverybodyUCPolicy,
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
metadata: Manifest.ucReg.BuyAsset,
|
|
504
|
+
};
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
#### BuyAssetServerMain.ts
|
|
508
|
+
|
|
509
|
+
```typescript
|
|
510
|
+
import {
|
|
511
|
+
type UCMain,
|
|
512
|
+
type UCMainInput,
|
|
513
|
+
type UCManager,
|
|
514
|
+
type UCOutput,
|
|
515
|
+
UCOutputBuilder,
|
|
516
|
+
} from 'libmodulor';
|
|
517
|
+
import { inject, injectable } from 'inversify';
|
|
518
|
+
|
|
519
|
+
import type { BuyAssetInput, BuyAssetOPI0 } from './BuyAssetUCD.js';
|
|
520
|
+
|
|
521
|
+
@injectable()
|
|
522
|
+
export class BuyAssetServerMain implements UCMain<BuyAssetInput, BuyAssetOPI0> {
|
|
523
|
+
constructor(@inject('UCManager') private ucManager: UCManager) {}
|
|
524
|
+
|
|
525
|
+
public async exec({
|
|
526
|
+
uc,
|
|
527
|
+
}: UCMainInput<BuyAssetInput, BuyAssetOPI0>): Promise<
|
|
528
|
+
UCOutput<BuyAssetOPI0>
|
|
529
|
+
> {
|
|
530
|
+
// >=> Persist the order
|
|
531
|
+
const { aggregateId } = await this.ucManager.persist(uc);
|
|
532
|
+
|
|
533
|
+
// >=> TODO : Check the user has enough funds to place the order
|
|
534
|
+
|
|
535
|
+
// >=> TODO : Send the order to a queue for processing
|
|
536
|
+
const executedDirectly: BuyAssetOPI0['executedDirectly'] = false;
|
|
537
|
+
|
|
538
|
+
return new UCOutputBuilder<BuyAssetOPI0>()
|
|
539
|
+
.add({
|
|
540
|
+
executedDirectly,
|
|
541
|
+
id: aggregateId,
|
|
542
|
+
})
|
|
543
|
+
.get();
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
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.
|
|
549
|
+
|
|
550
|
+
> [!TIP]
|
|
551
|
+
> Using a comment following the pattern `// >=> ` in `ClientMain` and `ServerMain` has a specific meaning as we'll see a little bit later.
|
|
552
|
+
|
|
553
|
+
> [!NOTE]
|
|
554
|
+
> Unlike `ClientMain`, `ServerMain` is put in another file for "historical" reasons, mainly for stripping and tree shaking reasons. More on this later.
|
|
555
|
+
|
|
556
|
+
```sh
|
|
557
|
+
yarn lint && git add . && git commit -m "feat: add the use case"
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
### Test the App
|
|
561
|
+
|
|
562
|
+
#### Preliminary test
|
|
563
|
+
|
|
564
|
+
By default, we rely on [vitest](https://vitest.dev) to run the tests and [@vitest/coverage-v8](https://vitest.dev/guide/coverage) for the coverage.
|
|
565
|
+
|
|
566
|
+
```sh
|
|
567
|
+
yarn test
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
```sh
|
|
571
|
+
% Coverage report from v8
|
|
572
|
+
------------------------|---------|----------|---------|---------|-------------------
|
|
573
|
+
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
|
|
574
|
+
------------------------|---------|----------|---------|---------|-------------------
|
|
575
|
+
All files | 0 | 40 | 40 | 0 |
|
|
576
|
+
Trading | 0 | 0 | 0 | 0 |
|
|
577
|
+
index.ts | 0 | 0 | 0 | 0 | 1
|
|
578
|
+
Trading/src | 0 | 100 | 100 | 0 |
|
|
579
|
+
i18n.ts | 0 | 100 | 100 | 0 | 3-5
|
|
580
|
+
manifest.ts | 0 | 100 | 100 | 0 | 3-14
|
|
581
|
+
Trading/src/ucds | 0 | 0 | 0 | 0 |
|
|
582
|
+
BuyAssetServerMain.ts | 0 | 0 | 0 | 0 | 1-28
|
|
583
|
+
BuyAssetUCD.ts | 0 | 0 | 0 | 0 | 1-92
|
|
584
|
+
------------------------|---------|----------|---------|---------|-------------------
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
As expected, the coverage report is pretty lame. Which is understandable, since we haven't written any tests yet.
|
|
588
|
+
|
|
589
|
+
```sh
|
|
590
|
+
mkdir src/apps/Trading/test
|
|
591
|
+
touch src/apps/Trading/test/Configurator.ts
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
#### Configurator.ts
|
|
595
|
+
|
|
596
|
+
```typescript
|
|
597
|
+
import { type AppTesterCtx, type CryptoManager, bindCommon } from 'libmodulor';
|
|
598
|
+
import {
|
|
599
|
+
NodeDeterministicCryptoManager,
|
|
600
|
+
bindNodeCore,
|
|
601
|
+
bindServer,
|
|
602
|
+
} from 'libmodulor/node';
|
|
603
|
+
import { SimpleAppTesterConfigurator } from 'libmodulor/node-test';
|
|
604
|
+
import { injectable } from 'inversify';
|
|
605
|
+
|
|
606
|
+
@injectable()
|
|
607
|
+
export class Configurator extends SimpleAppTesterConfigurator {
|
|
608
|
+
public override async bindImplementations(
|
|
609
|
+
ctx: AppTesterCtx,
|
|
610
|
+
): Promise<void> {
|
|
611
|
+
await super.bindImplementations(ctx);
|
|
612
|
+
|
|
613
|
+
const { container } = ctx;
|
|
614
|
+
|
|
615
|
+
bindCommon(container);
|
|
616
|
+
bindNodeCore(container);
|
|
617
|
+
bindServer(container);
|
|
618
|
+
|
|
619
|
+
container
|
|
620
|
+
.rebind<CryptoManager>('CryptoManager')
|
|
621
|
+
.to(NodeDeterministicCryptoManager);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
#### Automated test
|
|
627
|
+
|
|
628
|
+
Generate the automated tests and execute them with the CLI (it does more than a simple `yarn test`).
|
|
629
|
+
|
|
630
|
+
```sh
|
|
631
|
+
yarn cli GenerateAppsTests
|
|
632
|
+
yarn cli TestApp --appName Trading
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
```sh
|
|
636
|
+
% Coverage report from v8
|
|
637
|
+
------------------------|---------|----------|---------|---------|-------------------
|
|
638
|
+
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
|
|
639
|
+
------------------------|---------|----------|---------|---------|-------------------
|
|
640
|
+
All files | 98.83 | 80 | 80 | 98.83 |
|
|
641
|
+
Trading | 0 | 0 | 0 | 0 |
|
|
642
|
+
index.ts | 0 | 0 | 0 | 0 | 1
|
|
643
|
+
Trading/src | 100 | 100 | 100 | 100 |
|
|
644
|
+
i18n.ts | 100 | 100 | 100 | 100 |
|
|
645
|
+
manifest.ts | 100 | 100 | 100 | 100 |
|
|
646
|
+
Trading/src/ucds | 100 | 100 | 100 | 100 |
|
|
647
|
+
BuyAssetServerMain.ts | 100 | 100 | 100 | 100 |
|
|
648
|
+
BuyAssetUCD.ts | 100 | 100 | 100 | 100 |
|
|
649
|
+
------------------------|---------|----------|---------|---------|-------------------
|
|
650
|
+
2024-12-29T11:00:53.178Z [info] Coverage Report => open src/apps/Trading/test/reports/coverage/index.html
|
|
651
|
+
2024-12-29T11:00:53.178Z [info] Simple HTML Report => open src/apps/Trading/test/reports/simple-html/index.html
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
That's much better. Without writing any line of test code, we've reached almost 100% coverage. Although coverage is a vanity metric in some way, it still is a valuable one.
|
|
655
|
+
|
|
656
|
+
Note another important thing : auto documentation. Check out the generated `src/apps/Trading/README.md` that shows a mermaid chart for each use case and a technical summary. This is very valuable to whoever wants to understand what the app does.
|
|
657
|
+
|
|
658
|
+
To vizualize the mermaid chart, you can copy/paste it [here](https://mermaid.live) or if you've published your repository to GitHub, they are displayed out of the box.
|
|
659
|
+
|
|
660
|
+
<img src="/docs/assets/trading-buy-asset-sequence-diagram.png" width="600px">
|
|
661
|
+
|
|
662
|
+
You can see how the special comments we've mentioned earlier show up in this chart. It's nice in order to describe with more details what happens at each step.
|
|
663
|
+
|
|
664
|
+
Note also the generated "Coverage Report" and the "Simple HTML Report". The former is provided by `c8` while the other is built by `libmodulor`. It gives a great overview of the test scenarios.
|
|
665
|
+
|
|
666
|
+
> [!NOTE]
|
|
667
|
+
> You might have seen that `Configurator` is extensible. It allows you to define specific flows to test a suite of use cases, define specific assertions, etc.
|
|
668
|
+
|
|
669
|
+
```sh
|
|
670
|
+
yarn lint && git add . && git commit -m "test: init app tests"
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
### Create the Product
|
|
674
|
+
|
|
675
|
+
Like the app, the product has a `i18n.ts` and `manifest.ts`.
|
|
676
|
+
|
|
677
|
+
```sh
|
|
678
|
+
mkdir -p src/products/SuperTrader
|
|
679
|
+
touch src/products/SuperTrader/{i18n.ts,manifest.ts}
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
#### i18n.ts
|
|
683
|
+
|
|
684
|
+
```typescript
|
|
685
|
+
import type { ProductI18n } from 'libmodulor';
|
|
686
|
+
import { I18nEN } from 'libmodulor/locales/en';
|
|
687
|
+
|
|
688
|
+
import { I18n as TradingI18n } from '../../apps/Trading/index.js';
|
|
689
|
+
|
|
690
|
+
export const I18n: ProductI18n = {
|
|
691
|
+
en: {
|
|
692
|
+
...I18nEN,
|
|
693
|
+
...TradingI18n.en,
|
|
694
|
+
p_desc: 'A simple app to trade crypto, shares and other assets',
|
|
695
|
+
p_slogan: 'Trading made simple',
|
|
696
|
+
},
|
|
697
|
+
};
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
#### manifest.ts
|
|
701
|
+
|
|
702
|
+
```typescript
|
|
703
|
+
import type { ProductManifest } from 'libmodulor';
|
|
704
|
+
|
|
705
|
+
export const Manifest: ProductManifest = {
|
|
706
|
+
appReg: [{ name: 'Trading' }],
|
|
707
|
+
name: 'SuperTrader',
|
|
708
|
+
};
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
> [!NOTE]
|
|
712
|
+
> The same way we register use cases in an app, we register apps in a product. Since apps are reusable across products, you can even exclude some use cases for a given product.
|
|
713
|
+
|
|
714
|
+
```sh
|
|
715
|
+
yarn lint && git add . && git commit -m "feat: add the product"
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
### Create the server Target
|
|
719
|
+
|
|
720
|
+
We'll use the pre-built [express](https://expressjs.com) `ServerManager`.
|
|
721
|
+
|
|
722
|
+
```sh
|
|
723
|
+
mkdir src/products/SuperTrader/server
|
|
724
|
+
touch src/products/SuperTrader/server/{container.ts,index.ts}
|
|
725
|
+
touch tsconfig.build.json
|
|
726
|
+
touch .env
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
#### container.ts
|
|
730
|
+
|
|
731
|
+
```typescript
|
|
732
|
+
import {
|
|
733
|
+
CONTAINER_OPTS,
|
|
734
|
+
EnvSettingsManager,
|
|
735
|
+
type ServerManager,
|
|
736
|
+
type ServerManagerSettings,
|
|
737
|
+
type SettingsManager,
|
|
738
|
+
TARGET_DEFAULT_SERVER_MANAGER_SETTINGS,
|
|
739
|
+
bindCommon,
|
|
740
|
+
bindProduct,
|
|
741
|
+
} from 'libmodulor';
|
|
742
|
+
import {
|
|
743
|
+
NodeExpressServerManager,
|
|
744
|
+
bindNodeCore,
|
|
745
|
+
bindServer,
|
|
746
|
+
} from 'libmodulor/node';
|
|
747
|
+
import { Container } from 'inversify';
|
|
748
|
+
|
|
749
|
+
import { I18n } from '../i18n.js';
|
|
750
|
+
import { Manifest } from '../manifest.js';
|
|
751
|
+
|
|
752
|
+
type S = ServerManagerSettings;
|
|
753
|
+
|
|
754
|
+
const container = new Container(CONTAINER_OPTS);
|
|
755
|
+
|
|
756
|
+
bindCommon<S>(container, () => ({
|
|
757
|
+
...TARGET_DEFAULT_SERVER_MANAGER_SETTINGS,
|
|
758
|
+
}));
|
|
759
|
+
bindNodeCore(container);
|
|
760
|
+
bindServer(container);
|
|
761
|
+
bindProduct(container, Manifest, I18n);
|
|
762
|
+
|
|
763
|
+
container.rebind<SettingsManager>('SettingsManager').to(EnvSettingsManager);
|
|
764
|
+
|
|
765
|
+
container.bind<ServerManager>('ServerManager').to(NodeExpressServerManager);
|
|
766
|
+
|
|
767
|
+
export default container;
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
#### index.ts
|
|
771
|
+
|
|
772
|
+
```typescript
|
|
773
|
+
import {
|
|
774
|
+
APPS_ROOT_DIR_NAME,
|
|
775
|
+
type FSManager,
|
|
776
|
+
type I18nManager,
|
|
777
|
+
ServerBooter,
|
|
778
|
+
} from 'libmodulor';
|
|
779
|
+
|
|
780
|
+
import container from './container.js';
|
|
781
|
+
|
|
782
|
+
await container.get<I18nManager>('I18nManager').init();
|
|
783
|
+
|
|
784
|
+
await container.resolve(ServerBooter).exec({
|
|
785
|
+
appsRootPath: container
|
|
786
|
+
.get<FSManager>('FSManager')
|
|
787
|
+
.path('..', '..', '..', APPS_ROOT_DIR_NAME),
|
|
788
|
+
srcImporter: (path) => import(path),
|
|
789
|
+
});
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
#### tsconfig.build.json
|
|
793
|
+
|
|
794
|
+
```json
|
|
795
|
+
{
|
|
796
|
+
"extends": "./tsconfig.json",
|
|
797
|
+
"compilerOptions": {
|
|
798
|
+
"noEmit": false,
|
|
799
|
+
"outDir": "dist"
|
|
800
|
+
},
|
|
801
|
+
"include": ["src"]
|
|
802
|
+
}
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
#### .env
|
|
806
|
+
|
|
807
|
+
```properties
|
|
808
|
+
app_logger_level=trace # the default is 'debug'
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
> [!TIP]
|
|
812
|
+
> A setting named `my_setting` in the code can be overriden with an environment variable called `app_my_setting`.
|
|
813
|
+
|
|
814
|
+
#### Build & Run
|
|
815
|
+
|
|
816
|
+
Update `package.json` to add new entries to the `scripts`.
|
|
817
|
+
|
|
818
|
+
```json
|
|
819
|
+
"build": "tsc --project tsconfig.build.json && cp .env dist/products/SuperTrader/server/.env",
|
|
820
|
+
"run:server": "cd dist/products/SuperTrader/server && node --env-file .env index.js",
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
```sh
|
|
824
|
+
yarn build && yarn run:server
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
Et voilà ! The server is running !
|
|
828
|
+
|
|
829
|
+
```sh
|
|
830
|
+
curl -X POST -H "Content-Type: application/json" http://localhost:7443/api/v1/BuyAsset
|
|
831
|
+
# ❌ {"message":"Invalid credentials"}
|
|
832
|
+
curl -X POST -H "Content-Type: application/json" -H "X-API-Key: PublicApiKeyToBeChangedWhenDeploying" http://localhost:7443/api/v1/BuyAsset
|
|
833
|
+
# ❌ {"message":"isin must be filled"}
|
|
834
|
+
curl -X POST -H "Content-Type: application/json" -H "X-API-Key: PublicApiKeyToBeChangedWhenDeploying" -d '{"isin":"US02079K3059","limit":123.5,"qty":150}' http://localhost:7443/api/v1/BuyAsset
|
|
835
|
+
# ✅ {"parts":{"_0":{"items":[{"executedDirectly":false,"id":"95dddca5-5e9d-48ac-a90c-71a58d4e8554"}],"total":1}}}
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
As you can see, validation comes out of the box. Later we'll see how to add even more precise rules to the data types.
|
|
839
|
+
|
|
840
|
+
> [!NOTE]
|
|
841
|
+
> The `public_api_key` is just a first layer of security to "authenticate" the client apps calling the server. Hopefully this is not the only security mechanism because of course, this key must be present in clear client side (web, cli, curl...). We'll dive deeper in security when we study the policies.
|
|
842
|
+
|
|
843
|
+
```sh
|
|
844
|
+
yarn lint && git add . && git commit -m "feat: add the server target"
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
### Create the web Target
|
|
848
|
+
|
|
849
|
+
We'll use the pre-built [React](https://react.dev) components to build a SPA (Single Page Application), bundled with [vite](https://vite.dev) and served with the server defined above.
|
|
850
|
+
|
|
851
|
+
> [!WARNING]
|
|
852
|
+
> For readers used to "beautiful" websites à la Linear, Vercel and related, your eyes will burn. You're going to discover the simple and pure CSS-less Web. The most beautiful one.
|
|
853
|
+
> Of course, feel free to add CSS if you want to. The main goal here is to focus on the essence of the UI and not the UI design.
|
|
854
|
+
|
|
855
|
+
```sh
|
|
856
|
+
yarn add --dev "@types/react@^18.3.17" "@types/react-dom@^18.3.5"
|
|
857
|
+
yarn add "react@^18.3.1" "react-dom@^18.3.1"
|
|
858
|
+
|
|
859
|
+
mkdir -p src/products/SuperTrader/web/components
|
|
860
|
+
touch src/products/SuperTrader/vite.config.web.ts
|
|
861
|
+
touch src/products/SuperTrader/web/{container.ts,index.html,index.tsx}
|
|
862
|
+
touch src/products/SuperTrader/web/components/App.tsx
|
|
863
|
+
```
|
|
864
|
+
|
|
865
|
+
#### vite.config.web.ts
|
|
866
|
+
|
|
867
|
+
```typescript
|
|
868
|
+
import { join } from 'node:path';
|
|
869
|
+
|
|
870
|
+
import { StripUCDLifecycleServerPlugin } from 'libmodulor/vite';
|
|
871
|
+
import { defineConfig } from 'vite';
|
|
872
|
+
|
|
873
|
+
const base = process.cwd();
|
|
874
|
+
const root = join('src', 'products', 'SuperTrader', 'web');
|
|
875
|
+
const outDir = join(
|
|
876
|
+
base,
|
|
877
|
+
'dist',
|
|
878
|
+
'products',
|
|
879
|
+
'SuperTrader',
|
|
880
|
+
'server',
|
|
881
|
+
'public',
|
|
882
|
+
);
|
|
883
|
+
|
|
884
|
+
export default defineConfig({
|
|
885
|
+
build: {
|
|
886
|
+
emptyOutDir: true,
|
|
887
|
+
outDir,
|
|
888
|
+
},
|
|
889
|
+
plugins: [StripUCDLifecycleServerPlugin],
|
|
890
|
+
root,
|
|
891
|
+
});
|
|
892
|
+
```
|
|
893
|
+
|
|
894
|
+
#### container.ts
|
|
895
|
+
|
|
896
|
+
```typescript
|
|
897
|
+
import {
|
|
898
|
+
CONTAINER_OPTS,
|
|
899
|
+
type ServerClientManagerSettings,
|
|
900
|
+
TARGET_DEFAULT_SERVER_CLIENT_MANAGER_SETTINGS,
|
|
901
|
+
bindCommon,
|
|
902
|
+
bindProduct,
|
|
903
|
+
} from 'libmodulor';
|
|
904
|
+
import { bindWeb } from 'libmodulor/web';
|
|
905
|
+
import { Container } from 'inversify';
|
|
906
|
+
|
|
907
|
+
import { I18n } from '../i18n.js';
|
|
908
|
+
import { Manifest } from '../manifest.js';
|
|
909
|
+
|
|
910
|
+
type S = ServerClientManagerSettings;
|
|
911
|
+
|
|
912
|
+
const container = new Container(CONTAINER_OPTS);
|
|
913
|
+
|
|
914
|
+
bindCommon<S>(container, () => ({
|
|
915
|
+
...TARGET_DEFAULT_SERVER_CLIENT_MANAGER_SETTINGS,
|
|
916
|
+
}));
|
|
917
|
+
bindWeb(container);
|
|
918
|
+
bindProduct(container, Manifest, I18n);
|
|
919
|
+
|
|
920
|
+
export default container;
|
|
921
|
+
```
|
|
922
|
+
|
|
923
|
+
#### index.html
|
|
924
|
+
|
|
925
|
+
```html
|
|
926
|
+
<!DOCTYPE html>
|
|
927
|
+
<html lang="en">
|
|
928
|
+
<head>
|
|
929
|
+
<meta charset="utf-8" />
|
|
930
|
+
<meta content="width=device-width, initial-scale=1" name="viewport"
|
|
931
|
+
/>
|
|
932
|
+
</head>
|
|
933
|
+
<body>
|
|
934
|
+
<div id="root"></div>
|
|
935
|
+
<script type="module" src="/index.tsx"></script>
|
|
936
|
+
</body>
|
|
937
|
+
</html>
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
#### App.tsx
|
|
941
|
+
|
|
942
|
+
Update `src/apps/Trading/index.ts` to expose the use case.
|
|
943
|
+
|
|
944
|
+
```typescript
|
|
945
|
+
export { BuyAssetUCD } from './src/ucds/BuyAssetUCD.js';
|
|
946
|
+
```
|
|
947
|
+
|
|
948
|
+
Naturally, in real life scenarios, we would never have such a bloated `App.tsx` and we could create fine-grained components. Everybody does that, right ?
|
|
949
|
+
|
|
950
|
+
```tsx
|
|
951
|
+
import { type Logger, type ProductManifest, UCOutputReader } from 'libmodulor';
|
|
952
|
+
import {
|
|
953
|
+
UCPanel,
|
|
954
|
+
type UCPanelOnError,
|
|
955
|
+
useDIContext,
|
|
956
|
+
useUC,
|
|
957
|
+
useUCOR,
|
|
958
|
+
} from 'libmodulor/react';
|
|
959
|
+
import {
|
|
960
|
+
UCAutoExecLoader,
|
|
961
|
+
UCExecTouchable,
|
|
962
|
+
UCForm,
|
|
963
|
+
} from 'libmodulor/react-web-pure';
|
|
964
|
+
import React, { useEffect, useState, type ReactElement } from 'react';
|
|
965
|
+
|
|
966
|
+
import { BuyAssetUCD, Manifest } from '../../../../apps/Trading/index.js';
|
|
967
|
+
|
|
968
|
+
export default function App(): ReactElement {
|
|
969
|
+
const { container, i18nManager, wordingManager } = useDIContext();
|
|
970
|
+
const [logger] = useState(container.get<Logger>('Logger'));
|
|
971
|
+
const [productManifest] = useState(
|
|
972
|
+
container.get<ProductManifest>('ProductManifest'),
|
|
973
|
+
);
|
|
974
|
+
|
|
975
|
+
const [buyAssetUC] = useUC(Manifest, BuyAssetUCD, null);
|
|
976
|
+
const [buyAssetPart0, _buyAssetPart1, { append0 }] = useUCOR(
|
|
977
|
+
new UCOutputReader(BuyAssetUCD, undefined),
|
|
978
|
+
);
|
|
979
|
+
|
|
980
|
+
const [loading, setLoading] = useState(true);
|
|
981
|
+
|
|
982
|
+
useEffect(() => {
|
|
983
|
+
(async () => {
|
|
984
|
+
logger.debug('Initializing i18n');
|
|
985
|
+
await i18nManager.init();
|
|
986
|
+
logger.debug('Done initializing i18n');
|
|
987
|
+
setLoading(false);
|
|
988
|
+
})();
|
|
989
|
+
|
|
990
|
+
const { slogan } = wordingManager.p();
|
|
991
|
+
document.title = `${productManifest.name} : ${slogan}`;
|
|
992
|
+
}, [i18nManager, logger, productManifest, wordingManager]);
|
|
993
|
+
|
|
994
|
+
const onError: UCPanelOnError = async (err) => alert(err.message);
|
|
995
|
+
|
|
996
|
+
const { slogan } = wordingManager.p();
|
|
997
|
+
const { label } = wordingManager.uc(buyAssetUC.def);
|
|
998
|
+
const { label: idLabel } = wordingManager.ucof('id');
|
|
999
|
+
const { label: executedDirectlyLabel } =
|
|
1000
|
+
wordingManager.ucof('executedDirectly');
|
|
1001
|
+
|
|
1002
|
+
return (
|
|
1003
|
+
<div>
|
|
1004
|
+
{loading && 'Loading...'}
|
|
1005
|
+
|
|
1006
|
+
{!loading && (
|
|
1007
|
+
<>
|
|
1008
|
+
<h1>
|
|
1009
|
+
{productManifest.name} : {slogan}
|
|
1010
|
+
</h1>
|
|
1011
|
+
|
|
1012
|
+
<h2>{label}</h2>
|
|
1013
|
+
|
|
1014
|
+
<UCPanel
|
|
1015
|
+
clearAfterExec={false}
|
|
1016
|
+
onDone={async (ucor) => append0(ucor)}
|
|
1017
|
+
onError={onError}
|
|
1018
|
+
renderAutoExecLoader={UCAutoExecLoader}
|
|
1019
|
+
renderExecTouchable={UCExecTouchable}
|
|
1020
|
+
renderForm={UCForm}
|
|
1021
|
+
sleepInMs={200} // Fake delay to see submit wording changing
|
|
1022
|
+
uc={buyAssetUC}
|
|
1023
|
+
/>
|
|
1024
|
+
|
|
1025
|
+
<table>
|
|
1026
|
+
<thead>
|
|
1027
|
+
<tr>
|
|
1028
|
+
<th>{idLabel}</th>
|
|
1029
|
+
<th>{executedDirectlyLabel}</th>
|
|
1030
|
+
</tr>
|
|
1031
|
+
</thead>
|
|
1032
|
+
<tbody>
|
|
1033
|
+
{buyAssetPart0?.items.map((i) => (
|
|
1034
|
+
<tr key={i.id}>
|
|
1035
|
+
<td>{i.id}</td>
|
|
1036
|
+
<td>{i.executedDirectly ? '✅' : '❌'}</td>
|
|
1037
|
+
</tr>
|
|
1038
|
+
))}
|
|
1039
|
+
</tbody>
|
|
1040
|
+
<tfoot>
|
|
1041
|
+
<tr>
|
|
1042
|
+
<th>{i18nManager.t('total')}</th>
|
|
1043
|
+
<th>{buyAssetPart0?.pagination.total}</th>
|
|
1044
|
+
</tr>
|
|
1045
|
+
</tfoot>
|
|
1046
|
+
</table>
|
|
1047
|
+
</>
|
|
1048
|
+
)}
|
|
1049
|
+
</div>
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
```
|
|
1053
|
+
|
|
1054
|
+
#### index.tsx
|
|
1055
|
+
|
|
1056
|
+
```typescript
|
|
1057
|
+
import { DIContextProvider } from 'libmodulor/react';
|
|
1058
|
+
import React, { StrictMode } from 'react';
|
|
1059
|
+
import ReactDOM from 'react-dom/client';
|
|
1060
|
+
|
|
1061
|
+
import App from './components/App.js';
|
|
1062
|
+
import container from './container.js';
|
|
1063
|
+
|
|
1064
|
+
const rootElt = document.getElementById('root');
|
|
1065
|
+
if (!rootElt) {
|
|
1066
|
+
throw new Error('Add a div#root in index.html');
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
ReactDOM.createRoot(rootElt).render(
|
|
1070
|
+
<StrictMode>
|
|
1071
|
+
<DIContextProvider container={container}>
|
|
1072
|
+
<App />
|
|
1073
|
+
</DIContextProvider>
|
|
1074
|
+
</StrictMode>,
|
|
1075
|
+
);
|
|
1076
|
+
```
|
|
1077
|
+
|
|
1078
|
+
#### Build & Run
|
|
1079
|
+
|
|
1080
|
+
Update `package.json` to add the `web` build to the `build` command.
|
|
1081
|
+
|
|
1082
|
+
```json
|
|
1083
|
+
"build": "tsc --project tsconfig.build.json && cp .env dist/products/SuperTrader/server/.env && vite -c src/products/SuperTrader/vite.config.web.ts build",
|
|
1084
|
+
```
|
|
1085
|
+
|
|
1086
|
+
Update `src/products/SuperTrader/server/container.ts` to mount the `public` directory.
|
|
1087
|
+
|
|
1088
|
+
```diff
|
|
1089
|
+
...TARGET_DEFAULT_SERVER_MANAGER_SETTINGS,
|
|
1090
|
+
+server_static_dir_path: 'public',
|
|
1091
|
+
```
|
|
1092
|
+
|
|
1093
|
+
Press <kbd>ctrl</kbd> + <kbd>C</kbd> to stop the server (we'll setup hot reload later).
|
|
1094
|
+
|
|
1095
|
+
```sh
|
|
1096
|
+
yarn build && yarn run:server
|
|
1097
|
+
open http://localhost:7443
|
|
1098
|
+
```
|
|
1099
|
+
|
|
1100
|
+
Et voilà ! The server is running ! Fill the form and see how it automatically submits to the server with client side and server side validation out of the box.
|
|
1101
|
+
|
|
1102
|
+
<img src="/docs/assets/trading-target-web.png" width="600px">
|
|
1103
|
+
|
|
1104
|
+
```sh
|
|
1105
|
+
yarn lint && git add . && git commit -m "feat: add the web target"
|
|
1106
|
+
```
|
|
1107
|
+
|
|
1108
|
+
### Switch to a persistent data storage
|
|
1109
|
+
|
|
1110
|
+
By default, the data is stored in memory on the server. Therefore, whenever we restart it, we lose everything. That is not very practical in real life scenarios. Let's use SQLite instead.
|
|
1111
|
+
|
|
1112
|
+
```sh
|
|
1113
|
+
yarn add "knex@^3.1.0" "sqlite3@^5.1.7"
|
|
1114
|
+
```
|
|
1115
|
+
|
|
1116
|
+
Update `src/products/SuperTrader/server/container.ts` to change the implementation.
|
|
1117
|
+
|
|
1118
|
+
```diff
|
|
1119
|
+
[...]
|
|
1120
|
+
TARGET_DEFAULT_SERVER_MANAGER_SETTINGS,
|
|
1121
|
+
+ type UCDataStore,
|
|
1122
|
+
[...]
|
|
1123
|
+
+import { KnexUCDataStore } from 'libmodulor/uc-data-store/knex';
|
|
1124
|
+
[...]
|
|
1125
|
+
+type S = KnexUCDataStoreSettings & ServerManagerSettings;
|
|
1126
|
+
[...]
|
|
1127
|
+
...TARGET_DEFAULT_SERVER_MANAGER_SETTINGS,
|
|
1128
|
+
+ knex_uc_data_store_conn_string: 'postgresql://toto',
|
|
1129
|
+
+ knex_uc_data_store_file_path: 'uc-data-store.sqlite',
|
|
1130
|
+
+ knex_uc_data_store_pool_max: 5,
|
|
1131
|
+
+ knex_uc_data_store_pool_min: 0,
|
|
1132
|
+
+ knex_uc_data_store_type: 'sqlite3',
|
|
1133
|
+
server_static_dir_path: 'public',
|
|
1134
|
+
[...]
|
|
1135
|
+
container.rebind<SettingsManager>('SettingsManager').to(EnvSettingsManager);
|
|
1136
|
+
+container.rebind<UCDataStore>('UCDataStore').to(KnexUCDataStore);
|
|
1137
|
+
```
|
|
1138
|
+
|
|
1139
|
+
Press <kbd>ctrl</kbd> + <kbd>C</kbd> to stop the server.
|
|
1140
|
+
|
|
1141
|
+
```sh
|
|
1142
|
+
yarn build && yarn run:server
|
|
1143
|
+
```
|
|
1144
|
+
|
|
1145
|
+
Fill and submit the use case multiple times.
|
|
1146
|
+
|
|
1147
|
+
Open the SQLite database with you with your favorite DB editor (e.g. TablePlus, DBeaver...).
|
|
1148
|
+
|
|
1149
|
+
```sh
|
|
1150
|
+
open dist/products/SuperTrader/server/uc-data-store.sqlite
|
|
1151
|
+
```
|
|
1152
|
+
|
|
1153
|
+
You should see all your submissions stored in the database.
|
|
1154
|
+
|
|
1155
|
+
```sh
|
|
1156
|
+
yarn lint && git add . && git commit -m "feat: persist data in SQLite"
|
|
1157
|
+
```
|
|
1158
|
+
|
|
1159
|
+
### Define wording for humans
|
|
1160
|
+
|
|
1161
|
+
By default, the UI simply "humanizes" the use case field keys. It's fine for technical people, but not for humans.
|
|
1162
|
+
|
|
1163
|
+
Update `src/apps/Trading/src/i18n.ts` and add the following keys to `en`.
|
|
1164
|
+
|
|
1165
|
+
```typescript
|
|
1166
|
+
uc_BuyAsset_label: 'Buy an asset',
|
|
1167
|
+
uc_BuyAsset_i_submit_idle: 'Send buy order',
|
|
1168
|
+
uc_BuyAsset_i_submit_submitting: 'Sending',
|
|
1169
|
+
ucif_isin_label: 'ISIN',
|
|
1170
|
+
ucif_qty_label: 'Quantity',
|
|
1171
|
+
ucof_executedDirectly_label: '🚀 Executed directly',
|
|
1172
|
+
ucof_id_label: 'Identifier',
|
|
1173
|
+
validation_format_ISIN:
|
|
1174
|
+
'Must be 2 uppercase letters, followed by 9 alphanumeric characters and 1 digit',
|
|
1175
|
+
```
|
|
1176
|
+
|
|
1177
|
+
Update `src/products/SuperTrader/i18n.ts` and add the following keys to `en`.
|
|
1178
|
+
|
|
1179
|
+
```typescript
|
|
1180
|
+
total: 'Total',
|
|
1181
|
+
```
|
|
1182
|
+
|
|
1183
|
+
> [!NOTE]
|
|
1184
|
+
> We distinguish what's related to the app from what's related to the product. Usually, in the app's `i18n`, you'll add only translations following a certain convention like `dt_*` (data type choices), `err_*` (error messages), `uc_*` (use cases), `ucif_*` (use case input fields), `ucof_*` (use case output fields), `validation_*` (validation messages), etc.
|
|
1185
|
+
|
|
1186
|
+
Press <kbd>ctrl</kbd> + <kbd>C</kbd> to stop the server.
|
|
1187
|
+
|
|
1188
|
+
```sh
|
|
1189
|
+
yarn build && yarn run:server
|
|
1190
|
+
```
|
|
1191
|
+
|
|
1192
|
+
Refresh the page. You should see a better wording. Try to type an invalid `ISIN` and see how the full validation message is displayed as well.
|
|
1193
|
+
|
|
1194
|
+
<img src="/docs/assets/trading-target-web-human.png" width="600px">
|
|
1195
|
+
|
|
1196
|
+
```sh
|
|
1197
|
+
yarn lint && git add . && git commit -m "feat: define wording for humans"
|
|
1198
|
+
```
|
|
1199
|
+
|
|
1200
|
+
### Create the cli Target
|
|
1201
|
+
|
|
1202
|
+
We'll use the pre-built [Node.js parseArgs](https://nodejs.org/api/util.html#utilparseargsconfig) CLI program.
|
|
1203
|
+
|
|
1204
|
+
```sh
|
|
1205
|
+
mkdir src/products/SuperTrader/cli
|
|
1206
|
+
touch src/products/SuperTrader/cli/{container.ts,index.ts}
|
|
1207
|
+
```
|
|
1208
|
+
|
|
1209
|
+
#### container.ts
|
|
1210
|
+
|
|
1211
|
+
```typescript
|
|
1212
|
+
import {
|
|
1213
|
+
CONTAINER_OPTS,
|
|
1214
|
+
type ServerClientManagerSettings,
|
|
1215
|
+
TARGET_DEFAULT_SERVER_CLIENT_MANAGER_SETTINGS,
|
|
1216
|
+
bindCommon,
|
|
1217
|
+
bindProduct,
|
|
1218
|
+
} from 'libmodulor';
|
|
1219
|
+
import { bindNodeCLI, bindNodeCore } from 'libmodulor/node';
|
|
1220
|
+
import { Container } from 'inversify';
|
|
1221
|
+
|
|
1222
|
+
import { I18n } from '../i18n.js';
|
|
1223
|
+
import { Manifest } from '../manifest.js';
|
|
1224
|
+
|
|
1225
|
+
const container = new Container(CONTAINER_OPTS);
|
|
1226
|
+
|
|
1227
|
+
bindCommon<ServerClientManagerSettings>(container, () => ({
|
|
1228
|
+
...TARGET_DEFAULT_SERVER_CLIENT_MANAGER_SETTINGS,
|
|
1229
|
+
}));
|
|
1230
|
+
bindNodeCore(container);
|
|
1231
|
+
bindNodeCLI(container);
|
|
1232
|
+
bindProduct(container, Manifest, I18n);
|
|
1233
|
+
|
|
1234
|
+
export default container;
|
|
1235
|
+
```
|
|
1236
|
+
|
|
1237
|
+
#### index.ts
|
|
1238
|
+
|
|
1239
|
+
```typescript
|
|
1240
|
+
import {
|
|
1241
|
+
APPS_ROOT_DIR_NAME,
|
|
1242
|
+
type FSManager,
|
|
1243
|
+
type I18nManager,
|
|
1244
|
+
} from 'libmodulor';
|
|
1245
|
+
import { NodeCoreCLIManager } from 'libmodulor/node';
|
|
1246
|
+
|
|
1247
|
+
import container from './container.js';
|
|
1248
|
+
|
|
1249
|
+
await container.get<I18nManager>('I18nManager').init();
|
|
1250
|
+
|
|
1251
|
+
await container.resolve(NodeCoreCLIManager).handleCommand({
|
|
1252
|
+
appsRootPath: container
|
|
1253
|
+
.get<FSManager>('FSManager')
|
|
1254
|
+
.path('..', '..', '..', APPS_ROOT_DIR_NAME),
|
|
1255
|
+
srcImporter: (path) => import(path),
|
|
1256
|
+
});
|
|
1257
|
+
```
|
|
1258
|
+
|
|
1259
|
+
#### Build & Run
|
|
1260
|
+
|
|
1261
|
+
Update `package.json` to add a new entry to the `scripts`.
|
|
1262
|
+
|
|
1263
|
+
```json
|
|
1264
|
+
"run:cli": "cd dist/products/SuperTrader/cli && node index.js",
|
|
1265
|
+
```
|
|
1266
|
+
|
|
1267
|
+
```sh
|
|
1268
|
+
yarn build && yarn run:cli
|
|
1269
|
+
```
|
|
1270
|
+
|
|
1271
|
+
You can see the CLI help appearing with the available commands.
|
|
1272
|
+
|
|
1273
|
+
> [!TIP]
|
|
1274
|
+
> Update the app's `i18n.ts` to add `uc_BuyAsset_desc` and `ucif_isin_desc` to have a more detailed help section.
|
|
1275
|
+
|
|
1276
|
+
Start the server if it's not running.
|
|
1277
|
+
|
|
1278
|
+
```sh
|
|
1279
|
+
yarn run:server
|
|
1280
|
+
```
|
|
1281
|
+
|
|
1282
|
+
Execute the CLI in another terminal or tab.
|
|
1283
|
+
|
|
1284
|
+
```sh
|
|
1285
|
+
yarn run:cli BuyAsset
|
|
1286
|
+
# ❌ ISIN must be filled
|
|
1287
|
+
yarn run:cli BuyAsset --isin US02079K3059 --limit 123.5 --qty 150
|
|
1288
|
+
# ✅ {"parts":{"_0":{"items":[{"executedDirectly":false,"id":"da3dc295-6d7c-41b1-a00a-62683f3e6ab9"}],"total":1}}}
|
|
1289
|
+
```
|
|
1290
|
+
|
|
1291
|
+
Open the SQLite database with you with your favorite DB editor (e.g. TablePlus, DBeaver...).
|
|
1292
|
+
|
|
1293
|
+
```sh
|
|
1294
|
+
open dist/products/SuperTrader/server/uc-data-store.sqlite
|
|
1295
|
+
```
|
|
1296
|
+
|
|
1297
|
+
```sh
|
|
1298
|
+
yarn lint && git add . && git commit -m "feat: add the cli target"
|
|
1299
|
+
```
|
|
1300
|
+
|
|
1301
|
+
### Create the mcp-server Target
|
|
1302
|
+
|
|
1303
|
+
We'll use the pre-built local [stdio transport](https://modelcontextprotocol.io/docs/concepts/transports#standard-input-output-stdio) server.
|
|
1304
|
+
|
|
1305
|
+
```sh
|
|
1306
|
+
yarn add "@modelcontextprotocol/sdk@^1.0.4"
|
|
1307
|
+
```
|
|
1308
|
+
|
|
1309
|
+
```sh
|
|
1310
|
+
mkdir src/products/SuperTrader/mcp-server
|
|
1311
|
+
touch src/products/SuperTrader/mcp-server/{container.ts,index.ts}
|
|
1312
|
+
```
|
|
1313
|
+
|
|
1314
|
+
#### container.ts
|
|
1315
|
+
|
|
1316
|
+
```typescript
|
|
1317
|
+
import {
|
|
1318
|
+
CONTAINER_OPTS,
|
|
1319
|
+
type ServerClientManagerSettings,
|
|
1320
|
+
type ServerManager,
|
|
1321
|
+
TARGET_DEFAULT_SERVER_CLIENT_MANAGER_SETTINGS,
|
|
1322
|
+
bindCommon,
|
|
1323
|
+
bindProduct,
|
|
1324
|
+
} from 'libmodulor';
|
|
1325
|
+
import { bindNodeCore, bindServer } from 'libmodulor/node';
|
|
1326
|
+
import { NodeLocalStdioMCPServerManager } from 'libmodulor/node-mcp';
|
|
1327
|
+
import { Container } from 'inversify';
|
|
1328
|
+
|
|
1329
|
+
import { I18n } from '../i18n.js';
|
|
1330
|
+
import { Manifest } from '../manifest.js';
|
|
1331
|
+
|
|
1332
|
+
type S = ServerClientManagerSettings;
|
|
1333
|
+
|
|
1334
|
+
const container = new Container(CONTAINER_OPTS);
|
|
1335
|
+
|
|
1336
|
+
bindCommon<S>(container, () => ({
|
|
1337
|
+
...TARGET_DEFAULT_SERVER_CLIENT_MANAGER_SETTINGS,
|
|
1338
|
+
logger_level: 'error',
|
|
1339
|
+
}));
|
|
1340
|
+
bindNodeCore(container);
|
|
1341
|
+
bindServer(container);
|
|
1342
|
+
bindProduct(container, Manifest, I18n);
|
|
1343
|
+
|
|
1344
|
+
container
|
|
1345
|
+
.bind<ServerManager>('ServerManager')
|
|
1346
|
+
.to(NodeLocalStdioMCPServerManager);
|
|
1347
|
+
|
|
1348
|
+
export default container;
|
|
1349
|
+
```
|
|
1350
|
+
|
|
1351
|
+
#### index.ts
|
|
1352
|
+
|
|
1353
|
+
```typescript
|
|
1354
|
+
import {
|
|
1355
|
+
APPS_ROOT_DIR_NAME,
|
|
1356
|
+
type FSManager,
|
|
1357
|
+
type I18nManager,
|
|
1358
|
+
} from 'libmodulor';
|
|
1359
|
+
import { MCPServerBooter } from 'libmodulor/node-mcp';
|
|
1360
|
+
|
|
1361
|
+
import container from './container.js';
|
|
1362
|
+
|
|
1363
|
+
await container.get<I18nManager>('I18nManager').init();
|
|
1364
|
+
|
|
1365
|
+
await container.resolve(MCPServerBooter).exec({
|
|
1366
|
+
appsRootPath: container
|
|
1367
|
+
.get<FSManager>('FSManager')
|
|
1368
|
+
.path('..', '..', '..', APPS_ROOT_DIR_NAME),
|
|
1369
|
+
srcImporter: (path) => import(path),
|
|
1370
|
+
});
|
|
1371
|
+
```
|
|
1372
|
+
|
|
1373
|
+
> [!NOTE]
|
|
1374
|
+
> Note how we increase the level of logs to `error` because logging on stdout [messes with the stdio transport](https://modelcontextprotocol.io/docs/tools/debugging#server-side-logging).
|
|
1375
|
+
|
|
1376
|
+
#### Claude Desktop
|
|
1377
|
+
|
|
1378
|
+
If you don't have [Claude Desktop](https://claude.ai/download) on your machine, install it.
|
|
1379
|
+
|
|
1380
|
+
The following instructions are for macOS. You might need to adapt the paths if you are using another OS.
|
|
1381
|
+
|
|
1382
|
+
Register the mcp server in Claude (make sure you adapt the absolute path to your `libmodulor-tuto` directory).
|
|
1383
|
+
|
|
1384
|
+
```sh
|
|
1385
|
+
nano ~/Library/Application\ Support/Claude/claude_desktop_config.json
|
|
1386
|
+
```
|
|
1387
|
+
|
|
1388
|
+
```json
|
|
1389
|
+
{
|
|
1390
|
+
"mcpServers": {
|
|
1391
|
+
"libmodulor-tuto": {
|
|
1392
|
+
"command": "node",
|
|
1393
|
+
"args": [
|
|
1394
|
+
"/Users/toto/libmodulor-tuto/dist/products/SuperTrader/mcp-server/index.js"
|
|
1395
|
+
]
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
```
|
|
1400
|
+
|
|
1401
|
+
If you want to enable debugging within Claude Desktop :
|
|
1402
|
+
|
|
1403
|
+
```sh
|
|
1404
|
+
nano ~/Library/Application\ Support/Claude/developer_settings.json
|
|
1405
|
+
```
|
|
1406
|
+
|
|
1407
|
+
```json
|
|
1408
|
+
{
|
|
1409
|
+
"allowDevTools": true
|
|
1410
|
+
}
|
|
1411
|
+
```
|
|
1412
|
+
|
|
1413
|
+
To vizualize the logs :
|
|
1414
|
+
|
|
1415
|
+
```sh
|
|
1416
|
+
ls -la ~/Library/Logs/Claude
|
|
1417
|
+
tail -f ~/Library/Logs/Claude/mcp.log
|
|
1418
|
+
tail -f ~/Library/Logs/Claude/mcp-server-libmodulor-tuto.log
|
|
1419
|
+
```
|
|
1420
|
+
|
|
1421
|
+
To open the Chrome Developer Tools wihtin Claude, press <kbd>cmd</kbd> + <kbd>option</kbd> + <kbd>shift</kbd> + <kbd>I</kbd> (should be easy if you're an emacs user).
|
|
1422
|
+
|
|
1423
|
+
#### Build & Run
|
|
1424
|
+
|
|
1425
|
+
Press <kbd>ctrl</kbd> + <kbd>C</kbd> to stop the server.
|
|
1426
|
+
|
|
1427
|
+
```sh
|
|
1428
|
+
yarn build && yarn run:server
|
|
1429
|
+
```
|
|
1430
|
+
|
|
1431
|
+
Launch Claude Desktop.
|
|
1432
|
+
|
|
1433
|
+
At the bottom right of the prompt you should see a little hammer 🔨 indicating `1 MCP Tool available`.
|
|
1434
|
+
|
|
1435
|
+
Click on it. You should see the `BuyAsset` use case registered.
|
|
1436
|
+
|
|
1437
|
+
Now just write a prompt like below :
|
|
1438
|
+
|
|
1439
|
+
```txt
|
|
1440
|
+
Dear Claude. Please buy 150 shares of Google.
|
|
1441
|
+
```
|
|
1442
|
+
|
|
1443
|
+
And let the magic happens.
|
|
1444
|
+
|
|
1445
|
+
<img src="/docs/assets/trading-target-mcp-server.png" width="600px">
|
|
1446
|
+
|
|
1447
|
+
Open the SQLite database with you with your favorite DB editor (e.g. TablePlus, DBeaver...).
|
|
1448
|
+
|
|
1449
|
+
```sh
|
|
1450
|
+
open dist/products/SuperTrader/server/uc-data-store.sqlite
|
|
1451
|
+
```
|
|
1452
|
+
|
|
1453
|
+
```sh
|
|
1454
|
+
yarn lint && git add . && git commit -m "feat: add the mcp-server target"
|
|
1455
|
+
```
|
|
1456
|
+
|
|
1457
|
+
### Summary
|
|
1458
|
+
|
|
1459
|
+
```sh
|
|
1460
|
+
git log --oneline
|
|
1461
|
+
```
|
|
1462
|
+
|
|
1463
|
+
```sh
|
|
1464
|
+
7be4e06 (HEAD -> master) feat: add the mcp-server target
|
|
1465
|
+
0fb3d57 feat: add the cli target
|
|
1466
|
+
e8a52a6 feat: define wording for humans
|
|
1467
|
+
3d41f82 feat: persist data in SQLite
|
|
1468
|
+
d55bc42 feat: add the web target
|
|
1469
|
+
1af90ea feat: add the server target
|
|
1470
|
+
dffa202 feat: add the product
|
|
1471
|
+
8b83e1f test: init app tests
|
|
1472
|
+
0f99382 feat: add the use case
|
|
1473
|
+
33ac247 feat: add the app
|
|
1474
|
+
09909aa chore: init source code
|
|
1475
|
+
```
|
|
1476
|
+
|
|
1477
|
+
That was cool ! In a couple of minutes we have created the foundations of a multi-platform application.
|
|
1478
|
+
|
|
1479
|
+
And we have only touched the surface as `libmodulor` has much more to offer.
|
|
1480
|
+
|
|
1481
|
+
We have used the "pre-built" targets, but it's totally possible to do the same with a Hono server, a stricli CLI, a Vue.js SPA and so on. All the targets implement generic interfaces provided by `libmodulor`, making the whole thing fully modular.
|
|
1482
|
+
|
|
1483
|
+
Coming soon : the Advanced Guide.
|
|
1484
|
+
|
|
1485
|
+
If you appreciated this Guide or have any feedback, of all kinds, please feel free to send me a message. I'll be happy to discuss and help.
|