@nodefony/devkit 10.0.0-beta.5 → 10.0.0-beta.7
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/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +3 -3
- package/dist/nodefony/controllers/DevkitController.js +1 -1
- package/dist/nodefony/controllers/McpController.js +2 -2
- package/dist/nodefony/controllers/ProfileSessionController.js +1 -1
- package/dist/nodefony/mcp/profileRoute.js +28 -9
- package/dist/nodefony/service/DevkitService.js +6 -6
- package/docs/index.md +1 -1
- package/package.json +13 -13
- package/skills/nodefony-add-crud/SKILL.md +38 -23
- package/skills/nodefony-add-grpc-service/SKILL.md +179 -0
- package/skills/nodefony-add-grpc-service/scripts/call.mjs +218 -0
- package/skills/nodefony-add-realtime-binary/SKILL.md +301 -0
- package/skills/nodefony-add-realtime-binary/references/messages.md +20 -0
- package/skills/nodefony-add-realtime-binary/references/mesures.md +59 -0
- package/skills/nodefony-add-realtime-channel/SKILL.md +452 -105
- package/skills/nodefony-add-realtime-channel/references/contrat.md +22 -0
- package/skills/nodefony-add-realtime-channel/references/production.md +140 -0
- package/skills/nodefony-add-realtime-presence/SKILL.md +328 -0
- package/skills/nodefony-authentication/SKILL.md +2 -2
- package/skills/nodefony-browser/SKILL.md +37 -2
- package/skills/nodefony-browser/references/sondes.md +19 -0
- package/skills/nodefony-browser/scripts/audit.mjs +17 -2
- package/skills/nodefony-browser/scripts/inspect.mjs +108 -87
- package/skills/nodefony-browser/scripts/lib/actions.mjs +164 -0
- package/skills/nodefony-browser/scripts/lib/browser.mjs +67 -11
- package/skills/nodefony-browser/scripts/lib/probes.mjs +108 -3
- package/skills/nodefony-browser/scripts/watch.mjs +27 -11
- package/skills/nodefony-dev/SKILL.md +31 -15
- package/skills/nodefony-dev/references/rapport-de-build.md +19 -0
- package/skills/nodefony-devops/SKILL.md +28 -21
- package/skills/nodefony-devops/references/dimensionnement.md +1 -1
- package/skills/nodefony-devops/references/frontal.md +5 -2
- package/skills/nodefony-devops/references/image.md +23 -15
- package/skills/nodefony-migrate-schema/SKILL.md +110 -20
- package/skills/nodefony-migrate-schema/references/verdicts.md +23 -20
- package/skills/nodefony-protect-route/SKILL.md +27 -32
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// \0@oxc-project+runtime@0.
|
|
1
|
+
// \0@oxc-project+runtime@0.158.0/helpers/esm/decorate.js
|
|
2
2
|
function __decorate(decorators, target, key, desc) {
|
|
3
3
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
4
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { BUILTIN_MCP_TOOL_KEYS, builtinMcpTools, selectBuiltinMcpTools } from "./nodefony/mcp/tools.js";
|
|
2
1
|
import defaults, { devkitConfigSchema } from "./nodefony/config/config.js";
|
|
3
2
|
import { defineDevkitConfig } from "./nodefony/config/defineModuleConfig.js";
|
|
4
|
-
import { buildCard } from "./nodefony/src/card.js";
|
|
5
|
-
import __decorate from "./_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
|
|
6
3
|
import DevkitService_default from "./nodefony/service/DevkitService.js";
|
|
7
4
|
import DevkitController_default from "./nodefony/controllers/DevkitController.js";
|
|
8
5
|
import McpController_default from "./nodefony/controllers/McpController.js";
|
|
9
6
|
import ProfileSessionController_default from "./nodefony/controllers/ProfileSessionController.js";
|
|
7
|
+
import __decorate from "./_virtual/_@oxc-project_runtime@0.158.0/helpers/esm/decorate.js";
|
|
8
|
+
import { BUILTIN_MCP_TOOL_KEYS, builtinMcpTools, selectBuiltinMcpTools } from "./nodefony/mcp/tools.js";
|
|
9
|
+
import { buildCard } from "./nodefony/src/card.js";
|
|
10
10
|
import { Module, services } from "nodefony";
|
|
11
11
|
import { controllers } from "@nodefony/framework";
|
|
12
12
|
// index.ts
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import __decorate from "../../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.158.0/helpers/esm/decorate.js";
|
|
2
2
|
import { Controller, controller, route } from "@nodefony/framework";
|
|
3
3
|
// nodefony/controllers/DevkitController.ts
|
|
4
4
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import __decorateParam from "../../_virtual/_@oxc-project_runtime@0.158.0/helpers/esm/decorateParam.js";
|
|
2
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.158.0/helpers/esm/decorate.js";
|
|
3
3
|
import { Body, Controller, controller, route } from "@nodefony/framework";
|
|
4
4
|
// nodefony/controllers/McpController.ts
|
|
5
5
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import __decorate from "../../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.158.0/helpers/esm/decorate.js";
|
|
2
2
|
import { Controller, controller, route } from "@nodefony/framework";
|
|
3
3
|
// nodefony/controllers/ProfileSessionController.ts
|
|
4
4
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { renderProfileReport } from "./profileReport.js";
|
|
2
|
-
import { watchToolStop } from "./shutdown.js";
|
|
3
1
|
import { NOT_SERVING_MESSAGE } from "./observe.js";
|
|
2
|
+
import { renderProfileReport } from "./profileReport.js";
|
|
4
3
|
import { SHOT_REQUEST_TIMEOUT_MS, createShotClient } from "./shotClient.js";
|
|
4
|
+
import { watchToolStop } from "./shutdown.js";
|
|
5
5
|
import { PROFILE_SESSION_PATH } from "../src/profileSession.js";
|
|
6
6
|
import { ADMIN_SCOPE_WRITE, EVENT_LOOP_RESOLUTION_MS, ProcessProbe } from "nodefony";
|
|
7
7
|
import { MCP_RESERVED_PREFIX, SESSION_TICKET_HEADER, mcpText, readTicketSessionVerdict } from "@nodefony/ai";
|
|
@@ -171,26 +171,42 @@ async function openShotSession(client, issueTicket, account, signal) {
|
|
|
171
171
|
} catch (error) {
|
|
172
172
|
return {
|
|
173
173
|
ok: false,
|
|
174
|
+
aborted: false,
|
|
174
175
|
message: error instanceof Error ? error.message : String(error)
|
|
175
176
|
};
|
|
176
177
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
178
|
+
let res;
|
|
179
|
+
try {
|
|
180
|
+
res = await client.send({
|
|
181
|
+
method: "POST",
|
|
182
|
+
path: PROFILE_SESSION_PATH,
|
|
183
|
+
headers: { [SESSION_TICKET_HEADER]: ticket },
|
|
184
|
+
readBody: true
|
|
185
|
+
}, signal);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if (error instanceof Error && error.name === "AbortError") return {
|
|
188
|
+
ok: false,
|
|
189
|
+
aborted: true
|
|
190
|
+
};
|
|
191
|
+
return {
|
|
192
|
+
ok: false,
|
|
193
|
+
aborted: false,
|
|
194
|
+
message: `session de profilage non ouverte pour \`${account}\` (${error instanceof Error ? error.message : String(error)}). L'application écoute-t-elle sur son adresse ?`
|
|
195
|
+
};
|
|
196
|
+
}
|
|
183
197
|
const read = readTicketSessionVerdict(res.body);
|
|
184
198
|
if (res.status !== 200 || "message" in read) {
|
|
185
199
|
const why = "message" in read ? read.message : `statut ${res.status}`;
|
|
186
200
|
return {
|
|
187
201
|
ok: false,
|
|
202
|
+
aborted: false,
|
|
188
203
|
message: `session de profilage non ouverte pour \`${account}\` (${res.status} — ${why}). ` + (res.status === 403 ? "Compte inconnu, inactif ou verrouillé ?" : res.status === 503 ? "" : "La route de connexion du profilage répond-elle ?")
|
|
189
204
|
};
|
|
190
205
|
}
|
|
191
206
|
const cookie = res.setCookie.map((c) => c.split(";", 1)[0] ?? "").filter((c) => c.length > 0).join("; ");
|
|
192
207
|
if (cookie.length === 0) return {
|
|
193
208
|
ok: false,
|
|
209
|
+
aborted: false,
|
|
194
210
|
message: `session ouverte pour \`${account}\`, mais aucun cookie reçu`
|
|
195
211
|
};
|
|
196
212
|
return {
|
|
@@ -417,7 +433,10 @@ async function profileRoute(profiler, target, deps, opts) {
|
|
|
417
433
|
if (opts.account !== null) {
|
|
418
434
|
if (deps.issueTicket === void 0) return mcpText("Profilage authentifié indisponible : aucun émetteur de ticket.", true);
|
|
419
435
|
const opened = await openShotSession(client, deps.issueTicket, opts.account, stop.signal);
|
|
420
|
-
if (!opened.ok)
|
|
436
|
+
if (!opened.ok) {
|
|
437
|
+
if (!opened.aborted) return mcpText(opened.message, true);
|
|
438
|
+
return mcpText(stop.interrupted ? "tir interrompu avant le premier envoi : le serveur s'arrête pendant l'ouverture de la session de profilage." : "tir abandonné avant le premier envoi : la session de profilage n'était pas encore ouverte.");
|
|
439
|
+
}
|
|
421
440
|
session = opened.session;
|
|
422
441
|
headers.cookie = session.cookie;
|
|
423
442
|
identity = {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
import { selectBuiltinMcpTools } from "../mcp/tools.js";
|
|
1
2
|
import { asProfilerReader } from "../mcp/observe.js";
|
|
2
|
-
import { readShotTarget } from "../mcp/shotClient.js";
|
|
3
3
|
import { PROFILE_SESSION_FACTOR } from "../src/profileSession.js";
|
|
4
|
-
import {
|
|
5
|
-
import defaults from "../config/config.js";
|
|
4
|
+
import { readShotTarget } from "../mcp/shotClient.js";
|
|
6
5
|
import { buildCard } from "../src/card.js";
|
|
7
|
-
import
|
|
6
|
+
import defaults from "../config/config.js";
|
|
7
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.158.0/helpers/esm/decorate.js";
|
|
8
8
|
import { ADMIN_DEFAULT_ROLE, Nodefony, ROLES_WITHIN, Service, extend, injectable } from "nodefony";
|
|
9
9
|
import { defineMcpGate, defineTicketSessionGate, mcpDeclaredScopes } from "@nodefony/ai";
|
|
10
10
|
// nodefony/service/DevkitService.ts
|
|
@@ -144,9 +144,9 @@ let DevkitService = class DevkitService extends Service {
|
|
|
144
144
|
servesTraffic: () => kernel?.runProfile.servers === true,
|
|
145
145
|
onShutdown: (listener) => {
|
|
146
146
|
if (kernel === null) return () => {};
|
|
147
|
-
kernel.
|
|
147
|
+
kernel.once("onDrain", listener);
|
|
148
148
|
return () => {
|
|
149
|
-
kernel.removeListener("
|
|
149
|
+
kernel.removeListener("onDrain", listener);
|
|
150
150
|
};
|
|
151
151
|
},
|
|
152
152
|
profileRoute: {
|
package/docs/index.md
CHANGED
|
@@ -477,7 +477,7 @@ agent invente : faute d'une marche à suivre, il écrit un CRUD à la main, un
|
|
|
477
477
|
service à méthodes `static` que le conteneur ne voit pas, un contrôle de droits
|
|
478
478
|
dans le corps de l'action.
|
|
479
479
|
|
|
480
|
-
Le paquet livre donc
|
|
480
|
+
Le paquet livre donc douze **skills** au format [Agent Skills](https://agentskills.io) :
|
|
481
481
|
un qui porte la conduite commune de toute tâche, un qui part du symptôme d'une panne,
|
|
482
482
|
et un par geste où l'invention coûte cher :
|
|
483
483
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodefony/devkit",
|
|
3
|
-
"version": "10.0.0-beta.
|
|
3
|
+
"version": "10.0.0-beta.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Outillage de développement d'une application Nodefony : carte de visite du projet et portes de découverte pour un agent de développement",
|
|
6
6
|
"author": "Christophe CAMENSULI <ccamensuli@gmail.com>",
|
|
@@ -27,24 +27,24 @@
|
|
|
27
27
|
"test:integration": "vitest run --config vitest.integration.config.ts"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
|
-
"@nodefony/ai": "^10.0.0-beta.
|
|
31
|
-
"@nodefony/framework": "^10.0.0-beta.
|
|
32
|
-
"@nodefony/http": "^10.0.0-beta.
|
|
33
|
-
"lighthouse": "^13.
|
|
34
|
-
"nodefony": "^10.0.0-beta.
|
|
30
|
+
"@nodefony/ai": "^10.0.0-beta.7",
|
|
31
|
+
"@nodefony/framework": "^10.0.0-beta.7",
|
|
32
|
+
"@nodefony/http": "^10.0.0-beta.7",
|
|
33
|
+
"lighthouse": "^13.6.0",
|
|
34
|
+
"nodefony": "^10.0.0-beta.7",
|
|
35
35
|
"playwright": "^1.50.0",
|
|
36
36
|
"zod": "^4.6.7"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@modelcontextprotocol/sdk": "1.32.2",
|
|
40
|
-
"@nodefony/ai": "^10.0.0-beta.
|
|
41
|
-
"@nodefony/framework": "^10.0.0-beta.
|
|
42
|
-
"@nodefony/http": "^10.0.0-beta.
|
|
43
|
-
"nodefony": "^10.0.0-beta.
|
|
40
|
+
"@nodefony/ai": "^10.0.0-beta.7",
|
|
41
|
+
"@nodefony/framework": "^10.0.0-beta.7",
|
|
42
|
+
"@nodefony/http": "^10.0.0-beta.7",
|
|
43
|
+
"nodefony": "^10.0.0-beta.7"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"axe-core": "4.14.0",
|
|
47
|
-
"tslib": "2.
|
|
47
|
+
"tslib": "2.9.1"
|
|
48
48
|
},
|
|
49
49
|
"peerDependenciesMeta": {
|
|
50
50
|
"playwright": {
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"url": "git+https://github.com/nodefony/nodefony-core.git",
|
|
60
60
|
"directory": "src/packages/@nodefony/devkit"
|
|
61
61
|
},
|
|
62
|
-
"homepage": "https://nodefony.
|
|
62
|
+
"homepage": "https://nodefony.net/",
|
|
63
63
|
"bugs": {
|
|
64
64
|
"url": "https://github.com/nodefony/nodefony-core/issues"
|
|
65
65
|
},
|
|
@@ -77,6 +77,6 @@
|
|
|
77
77
|
"esm"
|
|
78
78
|
],
|
|
79
79
|
"engines": {
|
|
80
|
-
"node": "
|
|
80
|
+
"node": "^24.16.0 || >=26.0.0"
|
|
81
81
|
}
|
|
82
82
|
}
|
|
@@ -32,7 +32,7 @@ description: >
|
|
|
32
32
|
## Le geste
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
|
-
npx nodefony create entity Article title:string body:text? published:bool
|
|
35
|
+
npx nodefony create entity Article title:string "body:text?" published:bool
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
Une seule commande produit la chaîne entière : la table, son interface de ligne, les schémas de
|
|
@@ -57,24 +57,25 @@ fixe un défaut LITTÉRAL, `:unique` pose une contrainte d'unicité, `:index` un
|
|
|
57
57
|
camelCase (`publishedAt`). Une faute de casse ou un type d'un autre outil (`boolean`, `integer`)
|
|
58
58
|
est refusé avec la forme juste proposée.
|
|
59
59
|
|
|
60
|
-
| Type | Ce que ça produit | SQLite
|
|
61
|
-
| -------------- | ----------------------------- |
|
|
62
|
-
| `string(n)` | texte court, borné (255) | `text` ¹
|
|
63
|
-
| `text` | texte long | `text`
|
|
64
|
-
| `int` | entier | `integer`
|
|
65
|
-
| `float` | nombre à virgule | `real`
|
|
66
|
-
| `decimal(p,s)` | décimal EXACT (voyage chaîne) | `
|
|
67
|
-
| `bool` | booléen | `integer` (0/1)
|
|
68
|
-
| `json` | document libre | `text` (JSON)
|
|
69
|
-
| `date` | horodatage (ms) | `integer` (ms)
|
|
70
|
-
| `uuid` | identifiant | `text`
|
|
71
|
-
| `char(n)` | longueur EXACTE | `text` ¹
|
|
72
|
-
| `enum(a,b)` | valeurs admises ² | `text`
|
|
73
|
-
| `ref:<Entité>` | relation ³ | clé étrangère
|
|
74
|
-
|
|
75
|
-
¹ SQLite
|
|
76
|
-
|
|
77
|
-
|
|
60
|
+
| Type | Ce que ça produit | SQLite | PostgreSQL | MySQL / MariaDB | MongoDB |
|
|
61
|
+
| -------------- | ----------------------------- | ------------------- | ------------------ | --------------------- | ---------------------- |
|
|
62
|
+
| `string(n)` | texte court, borné (255) | `text(n)` + CHECK ¹ | `varchar(n)` | `varchar(n)` | `String` + `maxlength` |
|
|
63
|
+
| `text` | texte long | `text` | `text` | `text` | `String` |
|
|
64
|
+
| `int` | entier | `integer` | `integer` | `int` | `Number` |
|
|
65
|
+
| `float` | nombre à virgule | `real` | `double precision` | `double` | `Number` |
|
|
66
|
+
| `decimal(p,s)` | décimal EXACT (voyage chaîne) | `text` | `numeric(p,s)` | `decimal(p,s)` | `String` |
|
|
67
|
+
| `bool` | booléen | `integer` (0/1) | `boolean` | `boolean` | `Boolean` |
|
|
68
|
+
| `json` | document libre | `text` (JSON) | `jsonb` | `json` | `Mixed` |
|
|
69
|
+
| `date` | horodatage (ms) | `integer` (ms) | `timestamptz(3)` | `datetime(3)` | `Date` |
|
|
70
|
+
| `uuid` | identifiant | `text` | `uuid` | `varchar(36)` | `String` |
|
|
71
|
+
| `char(n)` | longueur EXACTE | `text` + CHECK ¹ | `char(n)` + CHECK | `char(n)` + CHECK | `String`, `n` exact |
|
|
72
|
+
| `enum(a,b)` | valeurs admises ² | `text` + CHECK | `text` + CHECK | `varchar(64)` + CHECK | `String` + `enum` |
|
|
73
|
+
| `ref:<Entité>` | relation ³ | clé étrangère | clé étrangère | clé étrangère | `ObjectId` + `ref` |
|
|
74
|
+
|
|
75
|
+
¹ SQLite ne borne pas un `text` : une contrainte `CHECK` sur la longueur le fait en base, et le
|
|
76
|
+
schéma d'entrée (Zod) avant elle, sur TOUS les transports. ² Aucun type `ENUM` SQL : une colonne
|
|
77
|
+
texte et une contrainte `CHECK` qui liste les valeurs, chacune de 64 caractères au plus ; le type
|
|
78
|
+
TypeScript et le schéma Zod bornent avant la base. ³ En SQL, la colonne prend le type de la clé
|
|
78
79
|
visée (`uuid`, entier pour `--id serial`, texte pour `User`) ; en MongoDB, aucune clé étrangère.
|
|
79
80
|
|
|
80
81
|
La table exacte, par moteur, telle que CETTE version l'écrit :
|
|
@@ -108,7 +109,7 @@ tests — sans table ni migration (la collection naît à la première écriture
|
|
|
108
109
|
les options qui ont un sens ici :
|
|
109
110
|
|
|
110
111
|
```bash
|
|
111
|
-
npx nodefony create entity Post title:string(120) body:text? views:int=0 status:enum(draft,published)=draft slug:string:unique tags:json? author:ref:User --soft-delete --route /api/posts --dry-run
|
|
112
|
+
npx nodefony create entity Post "title:string(120)" "body:text?" views:int=0 "status:enum(draft,published)=draft" slug:string:unique "tags:json?" author:ref:User --soft-delete --route /api/posts --dry-run
|
|
112
113
|
```
|
|
113
114
|
|
|
114
115
|
- La clé est l'`_id` natif, servie en `id` ; `ref:<Entité>` est un `ObjectId` indexé, chargé
|
|
@@ -121,6 +122,17 @@ npx nodefony create entity Post title:string(120) body:text? views:int=0 status:
|
|
|
121
122
|
`--id`, `--index`, `--unique`. Un index composite s'écrit à la main dans le schéma.
|
|
122
123
|
- `User` ne se régénère pas : il s'étend dans `nodefony/entity/User.ts` (son TSDoc donne le geste).
|
|
123
124
|
|
|
125
|
+
## Le graphe d'entités de la console
|
|
126
|
+
|
|
127
|
+
Les liens du graphe viennent des `ref:` eux-mêmes : la clé étrangère que la base porte devient
|
|
128
|
+
une arête, sans rien répéter ailleurs. Le regroupement et la couleur suivent le **domaine** de
|
|
129
|
+
l'entité, sinon son module — et une application de centaines de tables tient souvent dans UN
|
|
130
|
+
module :
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
npx nodefony create entity Invoice "number:string(30):unique" "total:decimal(24,8)" customer:ref:Customer --domain facturation --dry-run
|
|
134
|
+
```
|
|
135
|
+
|
|
124
136
|
## Épouser une table qui existe déjà
|
|
125
137
|
|
|
126
138
|
Trois réglages, et ils ne touchent **que** le SQL — la propriété TypeScript reste `id`, `siteId` :
|
|
@@ -200,8 +212,11 @@ refus tombe tout seul.
|
|
|
200
212
|
`--no-migrate` s'en dispense ; si des entités modifiées attendaient, la migration est écrite mais
|
|
201
213
|
PAS appliquée, et la sortie nomme les tables en trop. Sinon, la table naît au prochain démarrage
|
|
202
214
|
en développement (`CREATE TABLE IF NOT EXISTS`).
|
|
203
|
-
2. **La modifier
|
|
204
|
-
|
|
215
|
+
2. **La modifier passe par une migration** dès que l'application versionne ses migrations :
|
|
216
|
+
`npx nodefony orm:generate --name <nom> --apply` (skill **`nodefony-migrate-schema`**). Sans
|
|
217
|
+
migrations versionnées, le démarrage en développement rattrape seul une colonne ajoutée qui
|
|
218
|
+
**accepte le vide** (`ADD COLUMN`, annoncé) ; une colonne **obligatoire** ne l'est jamais — le
|
|
219
|
+
démarrage nomme l'écart au lieu d'inventer la valeur des lignes existantes.
|
|
205
220
|
3. **La production ne fabrique JAMAIS le schéma.** Elle l'applique par des migrations, écrites par
|
|
206
221
|
`npx nodefony orm:generate` et posées par `npx nodefony orm:migrate` — c'est un geste à part,
|
|
207
222
|
avec ses refus et ses interdits : skill **`nodefony-migrate-schema`**.
|
|
@@ -218,7 +233,7 @@ geste :
|
|
|
218
233
|
- **nom réservé par un module du framework** (`session`, `access_token`, `audit_event`…) — un
|
|
219
234
|
homonyme dépossède le module, et l'application ne démarre plus sur un message parlant d'une
|
|
220
235
|
colonne inconnue. **`User` fait exception** : l'identité appartient à l'application, et
|
|
221
|
-
`create entity User firstName:string(100)
|
|
236
|
+
`create entity User "firstName:string(100)?"` écrit l'entité avec les colonnes du contrat plus
|
|
222
237
|
les tiennes. Trois refus s'y appliquent alors — renommer la table ou changer la casse des
|
|
223
238
|
colonnes (des requêtes les écrivent en dur), la poser ailleurs que dans l'application racine
|
|
224
239
|
(l'ordre de chargement n'y est pas garanti), et déclarer un champ obligatoire **sans valeur par
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nodefony-add-grpc-service
|
|
3
|
+
metadata:
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
description: >
|
|
6
|
+
Sert un service gRPC dans une application Nodefony — contrat `.proto`, code généré par `buf`,
|
|
7
|
+
contrôleur `GrpcController` marqué `@GrpcService`, déclaré par `@controllers` — à charger AVANT
|
|
8
|
+
d'écrire le contrat ou le contrôleur. Porte les paquets à installer, les pièges de génération,
|
|
9
|
+
ce que le framework décide à ta place (ports, refus 16/7, erreurs, arrêt, tailles) et un
|
|
10
|
+
script qui appelle la méthode depuis n'importe quel système pour prouver qu'elle répond.
|
|
11
|
+
Déclencheurs : "service gRPC", "exposer un contrat protobuf", "un client Go doit appeler
|
|
12
|
+
l'application", "gRPC-Web", "Connect", "buf generate", "@GrpcService", "GrpcController",
|
|
13
|
+
"appeler une méthode gRPC", "UNAUTHENTICATED", "UNIMPLEMENTED", "flux gRPC".
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# add-grpc-service — un contrat typé, servi par le pipeline
|
|
17
|
+
|
|
18
|
+
> 🧭 **Tu es arrivé ici directement ? Charge aussi `nodefony-dev`** — il porte la conduite
|
|
19
|
+
> commune (par où commencer, comment prouver que c'est fait). Cette page-ci ne couvre QUE gRPC.
|
|
20
|
+
>
|
|
21
|
+
> La référence complète est INSTALLÉE : `node_modules/@nodefony/framework/docs/grpc.md` (le
|
|
22
|
+
> pourquoi de chaque choix, les tables de codes, les limites). Une commande la retrouve, ligne
|
|
23
|
+
> comprise :
|
|
24
|
+
> `node node_modules/@nodefony/devkit/skills/nodefony-dev/scripts/docs.mjs grpc`.
|
|
25
|
+
|
|
26
|
+
> ⚖️ **La confiance n'exclut pas le contrôle.** Un contrôleur qui compile n'est pas un service
|
|
27
|
+
> qui répond. Le juge est l'appel réel (§ Prouver).
|
|
28
|
+
|
|
29
|
+
## Ce qu'il faut savoir avant d'écrire
|
|
30
|
+
|
|
31
|
+
- **Une méthode gRPC est une route.** Le pare-feu, `@IsGranted`, l'identité (`this.user`) et le
|
|
32
|
+
journal valent comme sur toute route. N'ouvre JAMAIS un second serveur (`@grpc/grpc-js` et son
|
|
33
|
+
`bindAsync`) : il contournerait le pare-feu et les droits.
|
|
34
|
+
- **Trois protocoles, servis d'office** : gRPC (HTTP/2 seulement : port 5152 en TLS), gRPC-Web et
|
|
35
|
+
Connect (aussi en HTTP/1.1 : port 5151). Un navigateur et `curl` passent par Connect.
|
|
36
|
+
- **Surface expérimentale** : sa forme peut changer en version mineure, toujours annoncée.
|
|
37
|
+
|
|
38
|
+
## Le geste
|
|
39
|
+
|
|
40
|
+
### 1. Les paquets
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm i @connectrpc/connect @connectrpc/connect-node @bufbuild/protobuf
|
|
44
|
+
npm i -D @bufbuild/buf @bufbuild/protoc-gen-es
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Le runtime en `dependencies` (une installation de production doit l'avoir), le générateur en
|
|
48
|
+
développement. Si npm annonce que le script d'installation de `@bufbuild/buf` est bloqué
|
|
49
|
+
(`allowScripts`), ce n'est pas un échec : `buf` fonctionne sans.
|
|
50
|
+
|
|
51
|
+
### 2. Le contrat — `proto/<paquet>/v1/<nom>.proto`
|
|
52
|
+
|
|
53
|
+
```proto
|
|
54
|
+
edition = "2024";
|
|
55
|
+
|
|
56
|
+
package greet.v1;
|
|
57
|
+
|
|
58
|
+
service GreetService {
|
|
59
|
+
rpc Greet(GreetRequest) returns (GreetResponse);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
message GreetRequest {
|
|
63
|
+
string name = 1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
message GreetResponse {
|
|
67
|
+
string greeting = 1;
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Le dossier suit le paquet (`greet/v1/` pour `greet.v1`) : c'est ce que vérifie `buf lint`.
|
|
72
|
+
|
|
73
|
+
### 3. La génération — deux fichiers à la racine, à côté de `proto/`
|
|
74
|
+
|
|
75
|
+
```yaml
|
|
76
|
+
# buf.yaml
|
|
77
|
+
version: v2
|
|
78
|
+
modules:
|
|
79
|
+
- path: proto
|
|
80
|
+
lint:
|
|
81
|
+
use: [STANDARD]
|
|
82
|
+
breaking:
|
|
83
|
+
use: [FILE]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```yaml
|
|
87
|
+
# buf.gen.yaml
|
|
88
|
+
version: v2
|
|
89
|
+
inputs:
|
|
90
|
+
- directory: proto
|
|
91
|
+
plugins:
|
|
92
|
+
- local: protoc-gen-es
|
|
93
|
+
out: nodefony/gen
|
|
94
|
+
opt: target=ts
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npx buf lint
|
|
99
|
+
npx buf generate
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`nodefony/gen/greet/v1/greet_pb.ts` est **commité** et jamais édité : régénérer après chaque
|
|
103
|
+
changement du contrat. Nodefony ne génère rien au build ni au démarrage.
|
|
104
|
+
|
|
105
|
+
### 4. Le contrôleur — `nodefony/controllers/GreetController.ts`
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { GrpcController, GrpcService } from "@nodefony/framework/grpc";
|
|
109
|
+
import type { ContextType } from "@nodefony/http";
|
|
110
|
+
import { GreetService } from "../gen/greet/v1/greet_pb";
|
|
111
|
+
import type { GreetRequest } from "../gen/greet/v1/greet_pb";
|
|
112
|
+
|
|
113
|
+
@GrpcService(GreetService)
|
|
114
|
+
export class GreetController extends GrpcController {
|
|
115
|
+
constructor(context: ContextType) {
|
|
116
|
+
super("GreetController", context);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
greet(request: GreetRequest) {
|
|
120
|
+
return { greeting: `Bonjour, ${request.name} !` };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Une méthode par méthode du contrat, nom en minuscule initiale (`Greet` → `greet`). Une méthode
|
|
126
|
+
manquante ou mal typée ne compile pas. La logique métier va dans un service injecté
|
|
127
|
+
(`@Inject("nom")` au constructeur — skill `nodefony-add-service`).
|
|
128
|
+
|
|
129
|
+
### 5. La déclaration — `index.ts` de l'application
|
|
130
|
+
|
|
131
|
+
Ajouter la classe à la liste existante : `@controllers([HomeController, GreetController])`.
|
|
132
|
+
Sans cela, la classe compile et aucune route n'existe.
|
|
133
|
+
|
|
134
|
+
## Ce que le framework décide pour toi
|
|
135
|
+
|
|
136
|
+
| Sujet | Ce qui se passe | Ton geste |
|
|
137
|
+
| ---------------- | ----------------------------------------------------------------------------------------------------------- | -------------------------------------------------- |
|
|
138
|
+
| Droits | `@IsGranted` sur la méthode ; refus sans identité → 16 `UNAUTHENTICATED`, identifié → 7 `PERMISSION_DENIED` | rien : ne traduis pas toi-même |
|
|
139
|
+
| Erreurs | `ConnectError` part telle quelle ; `HttpError` traduite ; toute autre exception → 13 `INTERNAL` | lever `new ConnectError(msg, Code.X)` pour choisir |
|
|
140
|
+
| Flux | traduction des erreurs avant le premier message seulement ; arrêt du serveur → 14 ; identité révoquée → 16 | passer `ctx.signal` à tes sources |
|
|
141
|
+
| Tailles | 4 Mio par message, reçu ou émis | section `grpc` de la config http pour changer |
|
|
142
|
+
| Échéance | unaire : la plus courte du client et du serveur → 4 ; flux : aucune | rien |
|
|
143
|
+
| Décorateurs HTTP | `@Body`, `@UseSession`, `@Csp`, `@Idempotent`… sur une méthode du contrat ARRÊTENT le démarrage | les retirer : le message EST le paramètre |
|
|
144
|
+
|
|
145
|
+
## Prouver
|
|
146
|
+
|
|
147
|
+
Application démarrée (`npx nodefony development`) :
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
npx nodefony inspect routes --json
|
|
151
|
+
node node_modules/@nodefony/devkit/skills/nodefony-add-grpc-service/scripts/call.mjs greet.v1.GreetService/Greet --field name=Ada
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
- `inspect routes` doit montrer `POST /greet.v1.GreetService/Greet`. Absente : la classe n'est
|
|
155
|
+
pas dans `@controllers`, ou le `dist` est périmé.
|
|
156
|
+
- `call.mjs` rend `HTTP 200` et le message — ou le code gRPC d'un refus (`unauthenticated`…).
|
|
157
|
+
Il passe sous Windows, sans `curl` ni guillemets imbriqués. Message riche : `--file message.json`.
|
|
158
|
+
Jeton : `--header authorization=Bearer <jeton>`. Codes de sortie : 0 réponse, 1 refus,
|
|
159
|
+
64 mauvais usage, 69 application injoignable.
|
|
160
|
+
- `npm run typecheck` ne prouve que la forme ; seul l'appel prouve le service.
|
|
161
|
+
|
|
162
|
+
## Pièges
|
|
163
|
+
|
|
164
|
+
| Symptôme | Cause | Geste |
|
|
165
|
+
| ------------------------------------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
|
|
166
|
+
| `Cannot find package '@connectrpc/connect-node'` au démarrage | paquet non installé | `npm i @connectrpc/connect @connectrpc/connect-node @bufbuild/protobuf` |
|
|
167
|
+
| `buf generate` : `protoc-gen-es` introuvable | lancé hors de l'application, ou générateur non installé | le lancer à la racine de l'application, après `npm i -D @bufbuild/protoc-gen-es` |
|
|
168
|
+
| Un client gRPC natif échoue tout de suite en `INTERNAL` | il vise 5151, qui est en HTTP/1.1 | viser 5152 (TLS) |
|
|
169
|
+
| `unimplemented` sur un chemin qui semble juste | casse ou paquet faux dans le chemin (`/greet.v1.GreetService/Greet`) | recopier le chemin depuis `inspect routes` |
|
|
170
|
+
| La méthode tourne encore après l'arrêt | elle n'écoute pas `ctx.signal` | le passer à la base, à l'abonnement, à `fetch` |
|
|
171
|
+
|
|
172
|
+
## Voisins
|
|
173
|
+
|
|
174
|
+
| Besoin | Skill |
|
|
175
|
+
| ------------------------------------- | ------------------------------- |
|
|
176
|
+
| La logique métier du service | `nodefony-add-service` |
|
|
177
|
+
| Réserver une méthode à un rôle | `nodefony-protect-route` |
|
|
178
|
+
| Un flux temps réel vers le navigateur | `nodefony-add-realtime-channel` |
|
|
179
|
+
| Le service ne démarre pas | `nodefony-troubleshoot` |
|