@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.
Files changed (38) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
  2. package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorateParam.js +1 -1
  3. package/dist/index.js +3 -3
  4. package/dist/nodefony/controllers/DevkitController.js +1 -1
  5. package/dist/nodefony/controllers/McpController.js +2 -2
  6. package/dist/nodefony/controllers/ProfileSessionController.js +1 -1
  7. package/dist/nodefony/mcp/profileRoute.js +28 -9
  8. package/dist/nodefony/service/DevkitService.js +6 -6
  9. package/docs/index.md +1 -1
  10. package/package.json +13 -13
  11. package/skills/nodefony-add-crud/SKILL.md +38 -23
  12. package/skills/nodefony-add-grpc-service/SKILL.md +179 -0
  13. package/skills/nodefony-add-grpc-service/scripts/call.mjs +218 -0
  14. package/skills/nodefony-add-realtime-binary/SKILL.md +301 -0
  15. package/skills/nodefony-add-realtime-binary/references/messages.md +20 -0
  16. package/skills/nodefony-add-realtime-binary/references/mesures.md +59 -0
  17. package/skills/nodefony-add-realtime-channel/SKILL.md +452 -105
  18. package/skills/nodefony-add-realtime-channel/references/contrat.md +22 -0
  19. package/skills/nodefony-add-realtime-channel/references/production.md +140 -0
  20. package/skills/nodefony-add-realtime-presence/SKILL.md +328 -0
  21. package/skills/nodefony-authentication/SKILL.md +2 -2
  22. package/skills/nodefony-browser/SKILL.md +37 -2
  23. package/skills/nodefony-browser/references/sondes.md +19 -0
  24. package/skills/nodefony-browser/scripts/audit.mjs +17 -2
  25. package/skills/nodefony-browser/scripts/inspect.mjs +108 -87
  26. package/skills/nodefony-browser/scripts/lib/actions.mjs +164 -0
  27. package/skills/nodefony-browser/scripts/lib/browser.mjs +67 -11
  28. package/skills/nodefony-browser/scripts/lib/probes.mjs +108 -3
  29. package/skills/nodefony-browser/scripts/watch.mjs +27 -11
  30. package/skills/nodefony-dev/SKILL.md +31 -15
  31. package/skills/nodefony-dev/references/rapport-de-build.md +19 -0
  32. package/skills/nodefony-devops/SKILL.md +28 -21
  33. package/skills/nodefony-devops/references/dimensionnement.md +1 -1
  34. package/skills/nodefony-devops/references/frontal.md +5 -2
  35. package/skills/nodefony-devops/references/image.md +23 -15
  36. package/skills/nodefony-migrate-schema/SKILL.md +110 -20
  37. package/skills/nodefony-migrate-schema/references/verdicts.md +23 -20
  38. package/skills/nodefony-protect-route/SKILL.md +27 -32
@@ -1,4 +1,4 @@
1
- // \0@oxc-project+runtime@0.155.0/helpers/esm/decorate.js
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);
@@ -1,4 +1,4 @@
1
- // \0@oxc-project+runtime@0.155.0/helpers/esm/decorateParam.js
1
+ // \0@oxc-project+runtime@0.158.0/helpers/esm/decorateParam.js
2
2
  function __decorateParam(paramIndex, decorator) {
3
3
  return function(target, key) {
4
4
  decorator(target, key, paramIndex);
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.155.0/helpers/esm/decorate.js";
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 __decorate from "../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
2
- import __decorateParam from "../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorateParam.js";
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.155.0/helpers/esm/decorate.js";
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
- const res = await client.send({
178
- method: "POST",
179
- path: PROFILE_SESSION_PATH,
180
- headers: { [SESSION_TICKET_HEADER]: ticket },
181
- readBody: true
182
- }, signal);
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) return mcpText(opened.message, true);
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 { selectBuiltinMcpTools } from "../mcp/tools.js";
5
- import defaults from "../config/config.js";
4
+ import { readShotTarget } from "../mcp/shotClient.js";
6
5
  import { buildCard } from "../src/card.js";
7
- import __decorate from "../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
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.prependOnceListener("onTerminate", listener);
147
+ kernel.once("onDrain", listener);
148
148
  return () => {
149
- kernel.removeListener("onTerminate", listener);
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 onze **skills** au format [Agent Skills](https://agentskills.io) :
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.5",
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.5",
31
- "@nodefony/framework": "^10.0.0-beta.5",
32
- "@nodefony/http": "^10.0.0-beta.5",
33
- "lighthouse": "^13.0.0",
34
- "nodefony": "^10.0.0-beta.5",
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.5",
41
- "@nodefony/framework": "^10.0.0-beta.5",
42
- "@nodefony/http": "^10.0.0-beta.5",
43
- "nodefony": "^10.0.0-beta.5"
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.8.1"
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.github.io/nodefony-core/",
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": ">=24.14.0"
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 | PostgreSQL | MySQL / MariaDB | MongoDB |
61
- | -------------- | ----------------------------- | --------------- | ---------------- | --------------- | ---------------------- |
62
- | `string(n)` | texte court, borné (255) | `text` ¹ | `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` | `double` | `Number` |
66
- | `decimal(p,s)` | décimal EXACT (voyage chaîne) | `numeric` | `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` ¹ | `char(n)` | `char(n)` | `String`, `n` exact |
72
- | `enum(a,b)` | valeurs admises ² | `text` | `varchar(255)` | `varchar(255)` | `String` + `enum` |
73
- | `ref:<Entité>` | relation ³ | clé étrangère | clé étrangère | clé étrangère | `ObjectId` + `ref` |
74
-
75
- ¹ SQLite n'applique aucune longueur : c'est le schéma d'entrée (Zod) qui borne, sur TOUS les
76
- transports. ² Même colonne partout, sans type SQL nommé (qui exigerait une migration) : le type
77
- TypeScript et le schéma Zod bornent les valeurs. ³ En SQL, la colonne prend le type de la clé
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 n'altère rien** — aucun `ALTER` n'est émis. Une colonne ajoutée à une entité déjà
204
- créée n'apparaîtra pas dans une base existante.
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)?` écrit l'entité avec les colonnes du contrat plus
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` |