@nodefony/devkit 10.0.0-alpha.8 → 10.0.0-beta.1
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.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorateParam.js +1 -1
- package/dist/index.js +4 -3
- package/dist/nodefony/controllers/DevkitController.js +2 -2
- package/dist/nodefony/controllers/McpController.js +3 -3
- package/dist/nodefony/service/DevkitService.js +4 -3
- package/docs/index.md +27 -19
- package/package.json +7 -7
- package/skills/nodefony-add-crud/SKILL.md +71 -31
- package/skills/nodefony-add-service/SKILL.md +22 -3
- package/skills/nodefony-browser/scripts/inspect.mjs +15 -13
- package/skills/nodefony-browser/scripts/lib/probes.mjs +8 -10
- package/skills/nodefony-browser/scripts/lib/wcag.mjs +4 -4
- package/skills/nodefony-browser/scripts/socket.mjs +2 -2
- package/skills/nodefony-dev/SKILL.md +7 -6
- package/skills/nodefony-dev/scripts/docs.mjs +1 -1
- package/skills/nodefony-migrate-schema/SKILL.md +13 -12
- package/skills/nodefony-migrate-schema/references/verdicts.md +19 -18
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.151.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
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.151.0/helpers/esm/decorateMetadata.js
|
|
2
2
|
function __decorateMetadata(k, v) {
|
|
3
3
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
4
4
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import defaults, { devkitConfigSchema } from "./nodefony/config/config.js";
|
|
2
2
|
import { defineDevkitConfig } from "./nodefony/config/defineModuleConfig.js";
|
|
3
3
|
import { buildCard } from "./nodefony/src/card.js";
|
|
4
|
-
import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.
|
|
5
|
-
import __decorate from "./_virtual/_@oxc-project_runtime@0.
|
|
4
|
+
import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
|
|
5
|
+
import __decorate from "./_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
|
|
6
6
|
import DevkitService_default from "./nodefony/service/DevkitService.js";
|
|
7
7
|
import DevkitController_default from "./nodefony/controllers/DevkitController.js";
|
|
8
8
|
import McpController_default from "./nodefony/controllers/McpController.js";
|
|
@@ -33,7 +33,8 @@ let DevkitModule = class DevkitModule extends Module {
|
|
|
33
33
|
* fautif nommé, plutôt qu'en `undefined.x` au premier appel en production.
|
|
34
34
|
*/
|
|
35
35
|
async onKernelRegister() {
|
|
36
|
-
|
|
36
|
+
const input = this.options;
|
|
37
|
+
this.options = defineDevkitConfig(input ?? {});
|
|
37
38
|
return this;
|
|
38
39
|
}
|
|
39
40
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import __decorate from "../../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
|
|
3
3
|
import { Controller, controller, route } from "@nodefony/framework";
|
|
4
4
|
//#region nodefony/controllers/DevkitController.ts
|
|
5
5
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.
|
|
2
|
-
import __decorate from "../../_virtual/_@oxc-project_runtime@0.
|
|
3
|
-
import __decorateParam from "../../_virtual/_@oxc-project_runtime@0.
|
|
1
|
+
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
|
|
3
|
+
import __decorateParam from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateParam.js";
|
|
4
4
|
import { ACCESS_TOKEN_VERIFIER, JsonRpcError, Nodefony, authorizeProtectedResource, checkMcpAccess, collectMcpTools, handleMcpMessage, jsonRpcFailure, mcpCallerRoles, protectedResourceMetadataUrl, readBearerHeader } from "nodefony";
|
|
5
5
|
import { Body, Controller, Headers, controller, route } from "@nodefony/framework";
|
|
6
6
|
//#region nodefony/controllers/McpController.ts
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import defaults from "../config/config.js";
|
|
2
2
|
import { buildCard } from "../src/card.js";
|
|
3
|
-
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.
|
|
4
|
-
import __decorate from "../../_virtual/_@oxc-project_runtime@0.
|
|
3
|
+
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
|
|
4
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
|
|
5
5
|
import { Module, Nodefony, Service, extend, injectable, mcpDeclaredScopes } from "nodefony";
|
|
6
6
|
//#region nodefony/service/DevkitService.ts
|
|
7
7
|
/**
|
|
@@ -57,7 +57,8 @@ let DevkitService = class DevkitService extends Service {
|
|
|
57
57
|
module;
|
|
58
58
|
cfg;
|
|
59
59
|
constructor(module) {
|
|
60
|
-
const
|
|
60
|
+
const options = module.options;
|
|
61
|
+
const merged = extend(true, {}, defaults, options ?? {});
|
|
61
62
|
super("devkit", module.container, module.notificationsCenter, merged);
|
|
62
63
|
this.module = module;
|
|
63
64
|
this.cfg = merged;
|
package/docs/index.md
CHANGED
|
@@ -4,7 +4,7 @@ navTitle: devkit
|
|
|
4
4
|
lang: fr
|
|
5
5
|
module: "@nodefony/devkit"
|
|
6
6
|
topic: overview
|
|
7
|
-
audience: [
|
|
7
|
+
audience: [developer]
|
|
8
8
|
tags: [module, developpement, agent]
|
|
9
9
|
status: stable
|
|
10
10
|
updated: 2026-09-01
|
|
@@ -106,9 +106,13 @@ la rend éprouvable sans serveur, et ce qui l'empêche d'inventer quoi que ce so
|
|
|
106
106
|
npx nodefony ai:mcp # écrit .mcp.json ; --dry-run pour voir sans écrire
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
Sept outils. Quatre sont les commandes que vous connaissez déjà :
|
|
110
110
|
`nodefony_inspect` (ce qui est monté), `nodefony_check` (ce qui manque),
|
|
111
111
|
`nodefony_symbols` (ce qu'une API signifie), `nodefony_card` (par où commencer).
|
|
112
|
+
`nodefony_docs` sert la documentation des modules. `nodefony_admin_list` et
|
|
113
|
+
`nodefony_admin_call` ouvrent les lectures du data plane d'administration — le
|
|
114
|
+
catalogue, puis l'appel — et exigent un jeton porteur du scope `admin:read` :
|
|
115
|
+
sans lui, ils n'apparaissent pas dans la liste.
|
|
112
116
|
|
|
113
117
|
**Il n'y a pas de process à lancer.** Depuis la révision `2026-07-28` du
|
|
114
118
|
transport, un serveur MCP est un endpoint `POST` sans session : c'est donc une
|
|
@@ -280,16 +284,20 @@ agent invente : faute d'une marche à suivre, il écrit un CRUD à la main, un
|
|
|
280
284
|
service à méthodes `static` que le conteneur ne voit pas, un contrôle de droits
|
|
281
285
|
dans le corps de l'action.
|
|
282
286
|
|
|
283
|
-
Le paquet livre donc
|
|
284
|
-
un
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
|
288
|
-
|
|
|
289
|
-
| `nodefony-
|
|
290
|
-
| `nodefony-
|
|
291
|
-
| `nodefony-add-
|
|
292
|
-
| `nodefony-
|
|
287
|
+
Le paquet livre donc huit **skills** au format [Agent Skills](https://agentskills.io) :
|
|
288
|
+
un qui porte la conduite commune de toute tâche, et un par geste où l'invention
|
|
289
|
+
coûte cher :
|
|
290
|
+
|
|
291
|
+
| Skill | Le besoin qu'il couvre |
|
|
292
|
+
| ------------------------------- | --------------------------------------------------------------- |
|
|
293
|
+
| `nodefony-dev` | conduire une tâche de bout en bout, avant toute modification |
|
|
294
|
+
| `nodefony-add-crud` | exposer une ressource REST complète, entité comprise |
|
|
295
|
+
| `nodefony-add-service` | ajouter de la logique métier réutilisable, vue du conteneur |
|
|
296
|
+
| `nodefony-protect-route` | réserver une route à qui est habilité |
|
|
297
|
+
| `nodefony-add-realtime-channel` | ouvrir un canal temps réel où le serveur pousse |
|
|
298
|
+
| `nodefony-browser` | voir **et mesurer** un écran, sans navigateur sur le poste |
|
|
299
|
+
| `nodefony-migrate-schema` | faire évoluer le schéma d'une base et le porter en production |
|
|
300
|
+
| `nodefony-devops` | exploiter l'application en conteneur, sans réécrire son contrat |
|
|
293
301
|
|
|
294
302
|
Tous portent le préfixe `nodefony-` : leurs pointeurs arrivent dans le dossier où
|
|
295
303
|
vous écrivez aussi les vôtres, et sans namespace un skill maison du même nom
|
|
@@ -330,13 +338,13 @@ absente sans `NODE_ENV`) ; les skills, eux, doivent se mettre à jour par npm.
|
|
|
330
338
|
|
|
331
339
|
## Configuration
|
|
332
340
|
|
|
333
|
-
| Clé | Type | Défaut
|
|
334
|
-
| -------------------- | ---------- |
|
|
335
|
-
| `enabled` | `boolean` | `true`
|
|
336
|
-
| `mcp.enabled` | `boolean` | `true`
|
|
337
|
-
| `mcp.allowedOrigins` | `string[]` | `[]`
|
|
338
|
-
| `mcp.allowRemote` | `boolean` | `false`
|
|
339
|
-
| `mcp.tools` | `string[]` | `
|
|
341
|
+
| Clé | Type | Défaut | Rôle |
|
|
342
|
+
| -------------------- | ---------- | -------------------------------------------------- | ---------------------------------------------- |
|
|
343
|
+
| `enabled` | `boolean` | `true` | Interrupteur du module |
|
|
344
|
+
| `mcp.enabled` | `boolean` | `true` | Répond-on aux requêtes MCP ? Coupé → `404` |
|
|
345
|
+
| `mcp.allowedOrigins` | `string[]` | `[]` | Origines de navigateur admises ; vide = aucune |
|
|
346
|
+
| `mcp.allowRemote` | `boolean` | `false` | Accepter un appel d'une adresse non locale |
|
|
347
|
+
| `mcp.tools` | `string[]` | les sept outils intégrés (`BUILTIN_MCP_TOOL_KEYS`) | Allowlist des outils — lecture seule |
|
|
340
348
|
|
|
341
349
|
Les clés, leurs types et leurs défauts viennent du schéma Zod
|
|
342
350
|
(`nodefony/config/config.ts`) — **source unique** dont dérivent la
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodefony/devkit",
|
|
3
|
-
"version": "10.0.0-
|
|
3
|
+
"version": "10.0.0-beta.1",
|
|
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>",
|
|
@@ -25,17 +25,17 @@
|
|
|
25
25
|
"coverage": "vitest run --coverage"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"@nodefony/framework": "^10.0.0-
|
|
29
|
-
"@nodefony/http": "^10.0.0-
|
|
30
|
-
"nodefony": "^10.0.0-
|
|
28
|
+
"@nodefony/framework": "^10.0.0-beta.1",
|
|
29
|
+
"@nodefony/http": "^10.0.0-beta.1",
|
|
30
|
+
"nodefony": "^10.0.0-beta.1",
|
|
31
31
|
"zod": "^4.6.5",
|
|
32
32
|
"playwright": "^1.50.0",
|
|
33
33
|
"lighthouse": "^13.0.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@nodefony/framework": "^10.0.0-
|
|
37
|
-
"@nodefony/http": "^10.0.0-
|
|
38
|
-
"nodefony": "^10.0.0-
|
|
36
|
+
"@nodefony/framework": "^10.0.0-beta.1",
|
|
37
|
+
"@nodefony/http": "^10.0.0-beta.1",
|
|
38
|
+
"nodefony": "^10.0.0-beta.1"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"axe-core": "4.13.0",
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: nodefony-add-crud
|
|
3
3
|
description: >
|
|
4
|
-
Crée une ressource complète dans une application Nodefony —
|
|
4
|
+
Crée une ressource complète dans une application Nodefony — entité, schémas de validation,
|
|
5
5
|
service CRUD, controller REST+WebSocket et tests — par le générateur `nodefony create entity`,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
controller de ressource.
|
|
6
|
+
sur SQL comme sur MongoDB. Porte la grammaire de champs, la
|
|
7
|
+
table des types moteur par moteur, les relations et clés étrangères,
|
|
8
|
+
les réglages pour épouser une table SQL existante, et ce qu'on découvre autrement en
|
|
9
|
+
production : la table naît au démarrage, un champ ajouté n'est rattrapé que s'il accepte le
|
|
10
|
+
vide, et la production s'applique par des migrations (skill `nodefony-migrate-schema`). À
|
|
11
|
+
charger AVANT d'écrire une entité, un repository ou un controller de ressource.
|
|
12
12
|
Déclencheurs : "ajoute une entité", "crée un CRUD", "nouvelle table", "modèle de données",
|
|
13
|
-
"ressource REST", "
|
|
14
|
-
|
|
15
|
-
"
|
|
13
|
+
"ressource REST", "je veux stocker des articles/commandes", "comment définir un champ", "quels types de champ ?", "une relation entre
|
|
14
|
+
deux entités", "clé étrangère", "index composite", "épouser une table existante",
|
|
15
|
+
"entité MongoDB", "schéma Mongoose".
|
|
16
16
|
---
|
|
17
17
|
|
|
18
18
|
# add-crud — une ressource complète, générée
|
|
@@ -44,29 +44,48 @@ se connecte qu'au démarrage), la pagination bornée **et son tri déclaré**, l
|
|
|
44
44
|
|
|
45
45
|
## La grammaire de champs
|
|
46
46
|
|
|
47
|
-
`nom:type[?][:index|:unique]` — **non-null par défaut**, `?` rend facultatif,
|
|
48
|
-
d'unicité, `:index` un index simple.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
|
56
|
-
|
|
|
57
|
-
| `
|
|
58
|
-
| `
|
|
59
|
-
| `
|
|
60
|
-
|
|
61
|
-
|
|
47
|
+
`nom:type[?][=défaut][:index|:unique]` — **non-null par défaut**, `?` rend facultatif, `=valeur`
|
|
48
|
+
fixe un défaut LITTÉRAL, `:unique` pose une contrainte d'unicité, `:index` un index simple.
|
|
49
|
+
🔴 **`!` est REFUSÉ** : il ne veut pas dire « obligatoire » ici — un champ l'est déjà.
|
|
50
|
+
|
|
51
|
+
**Casse** : l'entité et la cible d'une relation en PascalCase (`Post`, `ref:User`), le champ en
|
|
52
|
+
camelCase (`publishedAt`). Une faute de casse ou un type d'un autre outil (`boolean`, `integer`)
|
|
53
|
+
est refusé avec la forme juste proposée.
|
|
54
|
+
|
|
55
|
+
| Type | Ce que ça produit | SQLite | PostgreSQL | MySQL / MariaDB | MongoDB |
|
|
56
|
+
| -------------- | ----------------------------- | --------------- | ---------------- | --------------- | ---------------------- |
|
|
57
|
+
| `string(n)` | texte court, borné (255) | `text` ¹ | `varchar(n)` | `varchar(n)` | `String` + `maxlength` |
|
|
58
|
+
| `text` | texte long | `text` | `text` | `text` | `String` |
|
|
59
|
+
| `int` | entier | `integer` | `integer` | `int` | `Number` |
|
|
60
|
+
| `float` | nombre à virgule | `real` | `double` | `double` | `Number` |
|
|
61
|
+
| `decimal(p,s)` | décimal EXACT (voyage chaîne) | `numeric` | `numeric(p,s)` | `decimal(p,s)` | `String` |
|
|
62
|
+
| `bool` | booléen | `integer` (0/1) | `boolean` | `boolean` | `Boolean` |
|
|
63
|
+
| `json` | document libre | `text` (JSON) | `jsonb` | `json` | `Mixed` |
|
|
64
|
+
| `date` | horodatage (ms) | `integer` (ms) | `timestamptz(3)` | `datetime(3)` | `Date` |
|
|
65
|
+
| `uuid` | identifiant | `text` | `uuid` | `varchar(36)` | `String` |
|
|
66
|
+
| `char(n)` | longueur EXACTE | `text` ¹ | `char(n)` | `char(n)` | `String`, `n` exact |
|
|
67
|
+
| `enum(a,b)` | valeurs admises ² | `text` | `varchar(255)` | `varchar(255)` | `String` + `enum` |
|
|
68
|
+
| `ref:<Entité>` | relation ³ | clé étrangère | clé étrangère | clé étrangère | `ObjectId` + `ref` |
|
|
69
|
+
|
|
70
|
+
¹ SQLite n'applique aucune longueur : c'est le schéma d'entrée (Zod) qui borne, sur TOUS les
|
|
71
|
+
transports. ² Même colonne partout, sans type SQL nommé (qui exigerait une migration) : le type
|
|
72
|
+
TypeScript et le schéma Zod bornent les valeurs. ³ En SQL, la colonne prend le type de la clé
|
|
73
|
+
visée (`uuid`, entier pour `--id serial`, texte pour `User`) ; en MongoDB, aucune clé étrangère.
|
|
74
|
+
|
|
75
|
+
La table exacte, par moteur, telle que CETTE version l'écrit :
|
|
76
|
+
`npx nodefony create entity --describe-json` (champ `context.columnTypes`).
|
|
77
|
+
|
|
78
|
+
**Une relation** s'écrit `<champ>:ref:<Entité>` — le champ d'abord, toujours :
|
|
62
79
|
|
|
63
80
|
```bash
|
|
64
|
-
npx nodefony create entity Comment body:text ref:Article
|
|
81
|
+
npx nodefony create entity Comment body:text article:ref:Article
|
|
65
82
|
```
|
|
66
83
|
|
|
67
|
-
La colonne de jointure est **indexée d'office** — c'est elle que traverse un `?include=`.
|
|
68
|
-
|
|
69
|
-
|
|
84
|
+
La colonne de jointure est **indexée d'office** — c'est elle que traverse un `?include=`. En SQL,
|
|
85
|
+
la **clé étrangère est émise** (`.references()`), du type de la clé visée, et son effacement se
|
|
86
|
+
déduit de la nullabilité : `article:ref:Article` (obligatoire) → `restrict`, le parent ne peut pas
|
|
87
|
+
partir ; `article:ref:Article?` → `set null`. `cascade` n'est jamais un défaut : il s'écrit à la
|
|
88
|
+
main dans la table générée.
|
|
70
89
|
|
|
71
90
|
**Un index de table** porte plusieurs colonnes, et c'est le seul à le pouvoir :
|
|
72
91
|
|
|
@@ -77,6 +96,26 @@ npx nodefony create entity Visit siteId:uuid path:string at:date --index "siteId
|
|
|
77
96
|
Les deux options sont **répétables** — un couple par index. Sur un schéma réel, la majorité des
|
|
78
97
|
index utiles sont composites : c'est ainsi qu'une table est réellement interrogée.
|
|
79
98
|
|
|
99
|
+
## Sur une application MongoDB
|
|
100
|
+
|
|
101
|
+
La même commande écrit une entité **document** : schéma Mongoose, service CRUD, controller et
|
|
102
|
+
tests — sans table ni migration (la collection naît à la première écriture). Une ligne, toutes
|
|
103
|
+
les options qui ont un sens ici :
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
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
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
- La clé est l'`_id` natif, servie en `id` ; `ref:<Entité>` est un `ObjectId` indexé, chargé
|
|
110
|
+
par `?include=`, que le schéma d'entrée exige bien formé (24 caractères hexadécimaux → 422).
|
|
111
|
+
- **MongoDB ne tient aucune clé étrangère, l'ORM tient l'effacement** : supprimer un parent
|
|
112
|
+
encore désigné par une référence obligatoire est refusé (409), une facultative est remise à
|
|
113
|
+
`null` — la politique du SQL. Hors transaction, ce contrôle n'est pas atomique ; et
|
|
114
|
+
l'INSERTION n'est pas gardée (un identifiant inexistant est accepté, `?include=` rend `null`).
|
|
115
|
+
- Refusées en le disant (options SQL) : `--table`, `--column-case`, `--id-name`, `--dialect`,
|
|
116
|
+
`--id`, `--index`, `--unique`. Un index composite s'écrit à la main dans le schéma.
|
|
117
|
+
- `User` ne se régénère pas : il s'étend dans `nodefony/entity/User.ts` (son TSDoc donne le geste).
|
|
118
|
+
|
|
80
119
|
## Épouser une table qui existe déjà
|
|
81
120
|
|
|
82
121
|
Trois réglages, et ils ne touchent **que** le SQL — la propriété TypeScript reste `id`, `siteId` :
|
|
@@ -149,7 +188,7 @@ Ce contrat vaut aussi quand tu écris une liste **à la main** (un endpoint d'ad
|
|
|
149
188
|
listing filtré) : le côté serveur déclare ce qu'il sait trier, le point d'entrée le demande, et le
|
|
150
189
|
refus tombe tout seul.
|
|
151
190
|
|
|
152
|
-
## Les trois vérités à savoir avant de livrer
|
|
191
|
+
## Les trois vérités à savoir avant de livrer (SQL)
|
|
153
192
|
|
|
154
193
|
1. **La table naît au prochain démarrage en développement** (`CREATE TABLE IF NOT EXISTS`).
|
|
155
194
|
2. **La modifier n'altère rien** — aucun `ALTER` n'est émis. Une colonne ajoutée à une entité déjà
|
|
@@ -164,7 +203,8 @@ Le générateur s'arrête plutôt que de produire un fichier bancal — lis le m
|
|
|
164
203
|
geste :
|
|
165
204
|
|
|
166
205
|
- **hors projet** (aucun `nodefony.config.ts` au-dessus) ;
|
|
167
|
-
-
|
|
206
|
+
- **aucun ORM** dans l'application (ni `@nodefony/drizzle`, ni `@nodefony/mongoose`) ;
|
|
207
|
+
- **une option SQL sur une application MongoDB** (liste ci-dessus) ;
|
|
168
208
|
- **entité déjà déclarée** ;
|
|
169
209
|
- **nom réservé par un module du framework** (`session`, `access_token`, `audit_event`…) — un
|
|
170
210
|
homonyme dépossède le module, et l'application ne démarre plus sur un message parlant d'une
|
|
@@ -4,12 +4,14 @@ description: >
|
|
|
4
4
|
Crée un service injectable dans une application Nodefony par `nodefony create service`, et le
|
|
5
5
|
fait entrer dans le conteneur — la moitié qu'on oublie. Porte la distinction entre le nom de la
|
|
6
6
|
CLASSE et le nom de l'INSTANCE, les deux façons d'obtenir un service depuis un autre
|
|
7
|
-
(`@inject` au constructeur ou `container.get` à l'usage),
|
|
7
|
+
(`@inject` au constructeur ou `container.get` à l'usage), le choix de sa durée de vie
|
|
8
|
+
(un exemplaire pour l'application, ou un par requête), et le défaut mesuré qu'un service
|
|
8
9
|
écrit à la main produit : une classe qui compile, dont les tests passent, et que le conteneur
|
|
9
10
|
ignore. À charger AVANT d'écrire une classe de service ou d'appeler un service depuis un autre.
|
|
10
11
|
Déclencheurs : "crée un service", "un service métier", "logique métier partagée", "injecter une
|
|
11
12
|
dépendance", "container.get", "@injectable", "@services", "appeler un service depuis un autre",
|
|
12
|
-
"mon service est undefined", "le conteneur ne trouve pas mon service"
|
|
13
|
+
"mon service est undefined", "le conteneur ne trouve pas mon service", "un service par
|
|
14
|
+
requête", "un état propre à chaque requête", "scope request".
|
|
13
15
|
---
|
|
14
16
|
|
|
15
17
|
# add-service — un service que le conteneur connaît
|
|
@@ -58,7 +60,7 @@ et à l'introspection, et n'est construit qu'à la première requête qui le ré
|
|
|
58
60
|
|
|
59
61
|
## Deux noms, et ils ne servent pas à la même chose
|
|
60
62
|
|
|
61
|
-
C'est le piège n°1, et il ne produit aucune erreur — juste un `
|
|
63
|
+
C'est le piège n°1, et il ne produit aucune erreur — juste un `null` :
|
|
62
64
|
|
|
63
65
|
```ts
|
|
64
66
|
@injectable() // ← nomme la CLASSE : @inject("BillingService")
|
|
@@ -69,6 +71,23 @@ export class BillingService extends Service {
|
|
|
69
71
|
}
|
|
70
72
|
```
|
|
71
73
|
|
|
74
|
+
## Choisir sa durée de vie — un seul exemplaire, ou un par requête
|
|
75
|
+
|
|
76
|
+
Chaque requête a son propre conteneur (un scope, calque posé sur celui de l'application et jeté à
|
|
77
|
+
la fin ; en WebSocket, à la fin de la connexion). La portée décide où vit l'exemplaire :
|
|
78
|
+
|
|
79
|
+
| `@injectable(…)` | Exemplaires | Pour |
|
|
80
|
+
| ---------------------------------------- | --------------------------------------- | --------------------------------------------------------- |
|
|
81
|
+
| `()` ou `("nom")` — défaut `singleton` | un pour toute l'application | cache, pool, client : ce qui doit être unique |
|
|
82
|
+
| `({ scope: "transient" })` | un par résolution | un objet jetable sans état partagé |
|
|
83
|
+
| `({ name: "tenant", scope: "request" })` | un par requête, créé si elle le demande | ce qui naît et meurt avec la requête (`clean()` à la fin) |
|
|
84
|
+
|
|
85
|
+
Un service `request` reçoit le scope au constructeur (`super("tenant", scope, false)`, même nom
|
|
86
|
+
qu'au décorateur) ; `create service` n'en génère pas, il s'écrit à la main. Un singleton qui en
|
|
87
|
+
dépend est refusé au démarrage. Pour **lire** une donnée de la requête depuis un singleton, pas
|
|
88
|
+
besoin de portée : `RequestContext.getScope()` ou `RequestContext.get()` au moment de l'appel.
|
|
89
|
+
Détail : `node_modules/nodefony/docs/service.md`.
|
|
90
|
+
|
|
72
91
|
## Obtenir un service depuis un autre — deux voies, un choix
|
|
73
92
|
|
|
74
93
|
| Voie | Quand |
|
|
@@ -167,8 +167,8 @@ await page.addInitScript(() => {
|
|
|
167
167
|
if (window.__nfCsp.length < 20)
|
|
168
168
|
window.__nfCsp.push({
|
|
169
169
|
directive: e.violatedDirective,
|
|
170
|
-
blocked:
|
|
171
|
-
source:
|
|
170
|
+
blocked: (e.blockedURI ?? "").slice(0, 140),
|
|
171
|
+
source: (e.sourceFile ?? "").slice(0, 140),
|
|
172
172
|
line: e.lineNumber,
|
|
173
173
|
});
|
|
174
174
|
});
|
|
@@ -355,7 +355,7 @@ function effectiveBackground(el) {
|
|
|
355
355
|
let opaqueLayer = null;
|
|
356
356
|
for (let n = el; n; n = n.parentElement) {
|
|
357
357
|
const bg = getComputedStyle(n).backgroundColor;
|
|
358
|
-
if (!bg ||
|
|
358
|
+
if (!bg || bg.includes("transparent")) continue;
|
|
359
359
|
const { a } = parseColor(bg);
|
|
360
360
|
if (a === 0) continue;
|
|
361
361
|
if (a >= 1) {
|
|
@@ -397,7 +397,7 @@ function probeA11y() {
|
|
|
397
397
|
// simplifié suffit à attraper un bouton-icône muet — le cas réel.
|
|
398
398
|
const accessibleName = (el) => {
|
|
399
399
|
const aria = el.getAttribute("aria-label");
|
|
400
|
-
if (aria
|
|
400
|
+
if (aria?.trim()) return aria.trim();
|
|
401
401
|
const refs = el.getAttribute("aria-labelledby");
|
|
402
402
|
if (refs) {
|
|
403
403
|
const t = refs
|
|
@@ -682,14 +682,14 @@ async function measurePage(args) {
|
|
|
682
682
|
// CSP de l'application refuserait à bon droit) : c'est le pilote qui évalue.
|
|
683
683
|
const expression = `((args) => {
|
|
684
684
|
${sourceWcag()}
|
|
685
|
-
${describeElement}
|
|
686
|
-
${isVisible}
|
|
687
|
-
${effectiveBackground}
|
|
688
|
-
${probeA11y}
|
|
689
|
-
${probeRendering}
|
|
690
|
-
${probeWebStorage}
|
|
691
|
-
${readPerf}
|
|
692
|
-
return (${measurePage})(args);
|
|
685
|
+
${describeElement.toString()}
|
|
686
|
+
${isVisible.toString()}
|
|
687
|
+
${effectiveBackground.toString()}
|
|
688
|
+
${probeA11y.toString()}
|
|
689
|
+
${probeRendering.toString()}
|
|
690
|
+
${probeWebStorage.toString()}
|
|
691
|
+
${readPerf.toString()}
|
|
692
|
+
return (${measurePage.toString()})(args);
|
|
693
693
|
})(${JSON.stringify({ probes: PROBES, families: [...active] })})`;
|
|
694
694
|
|
|
695
695
|
const measured = await page.evaluate(expression);
|
|
@@ -711,7 +711,9 @@ if (active.has("axe")) {
|
|
|
711
711
|
try {
|
|
712
712
|
const axeCode = await axeSource();
|
|
713
713
|
const report = await page.evaluate(async (source) => {
|
|
714
|
-
//
|
|
714
|
+
// Le code d'axe-core est une CHAÎNE à exécuter dans la page : c'est le seul
|
|
715
|
+
// moyen de l'y charger sans `<script>` injecté, que la CSP refuserait.
|
|
716
|
+
// oxlint-disable-next-line no-new-func, typescript/no-implied-eval -- évalué par le pilote, hors CSP
|
|
715
717
|
new Function(source)();
|
|
716
718
|
return await window.axe.run(document, {
|
|
717
719
|
resultTypes: ["violations", "incomplete"],
|
|
@@ -39,7 +39,7 @@ export const FAMILIES = Object.freeze({
|
|
|
39
39
|
* et celles qui n'existent pas — à refuser, jamais à ignorer.
|
|
40
40
|
*/
|
|
41
41
|
export function parseFamilies(raw, fallback = []) {
|
|
42
|
-
const requested =
|
|
42
|
+
const requested = (raw ?? "").trim();
|
|
43
43
|
if (!requested) return { kept: [...fallback], unknown: [] };
|
|
44
44
|
if (requested === "toutes") {
|
|
45
45
|
return { kept: Object.keys(FAMILIES), unknown: [] };
|
|
@@ -71,7 +71,7 @@ export function parseFamilies(raw, fallback = []) {
|
|
|
71
71
|
export function parseProbes(raw) {
|
|
72
72
|
const probes = [];
|
|
73
73
|
const rejected = [];
|
|
74
|
-
for (const chunk of
|
|
74
|
+
for (const chunk of (raw ?? "")
|
|
75
75
|
.split(",")
|
|
76
76
|
.map((p) => p.trim())
|
|
77
77
|
.filter(Boolean)) {
|
|
@@ -171,9 +171,7 @@ export function parseActions(raw) {
|
|
|
171
171
|
* `schema` à null quand rien n'est demandé (on ne force rien).
|
|
172
172
|
*/
|
|
173
173
|
export function parseColorScheme(raw) {
|
|
174
|
-
const v =
|
|
175
|
-
.trim()
|
|
176
|
-
.toLowerCase();
|
|
174
|
+
const v = (raw ?? "").trim().toLowerCase();
|
|
177
175
|
if (!v) return { schema: null, invalid: null };
|
|
178
176
|
if (v === "light" || v === "dark" || v === "no-preference")
|
|
179
177
|
return { schema: v, invalid: null };
|
|
@@ -200,7 +198,7 @@ export function parseColorScheme(raw) {
|
|
|
200
198
|
export function parseStorage(raw) {
|
|
201
199
|
const entries = [];
|
|
202
200
|
const rejected = [];
|
|
203
|
-
for (const chunk of
|
|
201
|
+
for (const chunk of (raw ?? "")
|
|
204
202
|
.split(",")
|
|
205
203
|
.map((p) => p.trim())
|
|
206
204
|
.filter(Boolean)) {
|
|
@@ -261,7 +259,7 @@ export function environmentDefaults({ inContainer, base, out } = {}) {
|
|
|
261
259
|
* @returns {string} le nom de fichier, sans dossier.
|
|
262
260
|
*/
|
|
263
261
|
export function authStateName(login) {
|
|
264
|
-
const raw =
|
|
262
|
+
const raw = login ?? "";
|
|
265
263
|
const readable =
|
|
266
264
|
raw.replace(/[^A-Za-z0-9._-]/gu, "_").slice(0, 40) || "anonyme";
|
|
267
265
|
const fingerprint = createHash("sha256")
|
|
@@ -284,7 +282,7 @@ export function authStateName(login) {
|
|
|
284
282
|
export function parseWidths(raw) {
|
|
285
283
|
const widths = [];
|
|
286
284
|
const invalidWidths = [];
|
|
287
|
-
for (const chunk of
|
|
285
|
+
for (const chunk of (raw ?? "")
|
|
288
286
|
.split(",")
|
|
289
287
|
.map((p) => p.trim())
|
|
290
288
|
.filter(Boolean)) {
|
|
@@ -427,7 +425,7 @@ export function summarizeLighthouse(lhr, threshold = 0.9) {
|
|
|
427
425
|
for (const c of categories) {
|
|
428
426
|
for (const ref of c.auditRefs ?? []) {
|
|
429
427
|
const a = audits[ref.id];
|
|
430
|
-
if (
|
|
428
|
+
if (a?.score == null) continue;
|
|
431
429
|
if (a.score >= threshold) continue;
|
|
432
430
|
failed.push({
|
|
433
431
|
category: c.id,
|
|
@@ -495,7 +493,7 @@ export function summarizeLighthouse(lhr, threshold = 0.9) {
|
|
|
495
493
|
* @returns {string[]} les navigateurs à essayer, dans l'ordre.
|
|
496
494
|
*/
|
|
497
495
|
export function browserOrder(explicit) {
|
|
498
|
-
const v =
|
|
496
|
+
const v = (explicit ?? "").trim();
|
|
499
497
|
if (v) return [v];
|
|
500
498
|
return ["chromium", "chrome", "msedge"];
|
|
501
499
|
}
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* @throws {Error} si la notation n'est pas reconnue.
|
|
40
40
|
*/
|
|
41
41
|
export function parseColor(color) {
|
|
42
|
-
const s =
|
|
42
|
+
const s = color.trim();
|
|
43
43
|
// `transparent` est le SEUL mot-clé que les navigateurs rendent parfois tel
|
|
44
44
|
// quel, et il a une valeur définie : `rgba(0, 0, 0, 0)`. Le refuser serait
|
|
45
45
|
// faux — un fond non peint est un cas courant, pas une erreur de mesure.
|
|
@@ -51,7 +51,7 @@ export function parseColor(color) {
|
|
|
51
51
|
if (hex) {
|
|
52
52
|
const d = hex[1];
|
|
53
53
|
if (d.length === 3 || d.length === 4) {
|
|
54
|
-
const q =
|
|
54
|
+
const q = Array.from(d, (c) => parseInt(c + c, 16));
|
|
55
55
|
return {
|
|
56
56
|
r: q[0],
|
|
57
57
|
g: q[1],
|
|
@@ -73,7 +73,7 @@ export function parseColor(color) {
|
|
|
73
73
|
const numbers = s.match(/-?\d*\.?\d+(?:e-?\d+)?%?/gi);
|
|
74
74
|
if (!numbers || numbers.length < 3)
|
|
75
75
|
throw new Error(
|
|
76
|
-
`notation de couleur non reconnue : ${JSON.stringify(
|
|
76
|
+
`notation de couleur non reconnue : ${JSON.stringify(color)} — ` +
|
|
77
77
|
"attendu rgb()/rgba(), color(srgb …) ou #rrggbb",
|
|
78
78
|
);
|
|
79
79
|
// `color(srgb …)` et `color(display-p3 …)` : canaux en 0–1. Le pourcentage
|
|
@@ -164,7 +164,7 @@ export function contrastRatio(a, b) {
|
|
|
164
164
|
* @returns {boolean} vrai si les seuils « texte large » s'appliquent.
|
|
165
165
|
*/
|
|
166
166
|
export function isLargeText(px, bold) {
|
|
167
|
-
return px >= 24 || (bold
|
|
167
|
+
return px >= 24 || (bold && px >= 18.66);
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
/**
|
|
@@ -320,7 +320,7 @@ async function scenarioSocket(conf) {
|
|
|
320
320
|
} catch (e) {
|
|
321
321
|
result.reconnection = {
|
|
322
322
|
closedAt,
|
|
323
|
-
verdict: `ÉCHEC — ${String(e
|
|
323
|
+
verdict: `ÉCHEC — ${String(e?.message ? e.message : e).slice(0, 140)}`,
|
|
324
324
|
};
|
|
325
325
|
}
|
|
326
326
|
result.closes = closes;
|
|
@@ -337,7 +337,7 @@ try {
|
|
|
337
337
|
// L'accueil qui ne vient jamais est LE symptôme à diagnostiquer en premier :
|
|
338
338
|
// endpoint faux, page non authentifiée, ou Origin refusé par le serveur.
|
|
339
339
|
console.error(
|
|
340
|
-
`Scénario interrompu : ${String(e
|
|
340
|
+
`Scénario interrompu : ${String(e?.message ? e.message : e).slice(0, 300)}\n` +
|
|
341
341
|
`→ vérifier le chemin du endpoint, l'authentification (NF_BROWSER_USER + NF_BROWSER_LOGIN), et que la page ${process.env.NF_BROWSER_PAGE ?? "/"} appartient bien à la même origine.`,
|
|
342
342
|
);
|
|
343
343
|
await browser.close();
|
|
@@ -306,6 +306,8 @@ Chacun a déjà coûté au moins une heure à quelqu'un. Les quatre premiers son
|
|
|
306
306
|
- **Une clé de configuration mal orthographiée est retirée en silence.** `satisfies` sur le
|
|
307
307
|
fragment n'est pas décoratif : sans lui, la faute compile, puis la validation écarte la clé et
|
|
308
308
|
le module démarre sur son défaut. Personne ne le voit.
|
|
309
|
+
- **La config d'un module est FIGÉE après le démarrage** : l'écrire lève `TypeError`. Une variante
|
|
310
|
+
par requête = `overlayConfig()` (clés de la liste blanche du module), lue par `useConfig()`.
|
|
309
311
|
- **Tu lis une liste sans la BORNER.** Un `find` sans limite matérialise la table entière —
|
|
310
312
|
indolore sur les quelques lignes du poste de développement, fatal sur les dizaines de milliers
|
|
311
313
|
de la production. Le service d'une entité hérite `findPage({ limit: 25 })`.
|
|
@@ -370,12 +372,11 @@ Chacun a déjà coûté au moins une heure à quelqu'un. Les quatre premiers son
|
|
|
370
372
|
|
|
371
373
|
## 8. Données et fichiers — les façades qu'on ne recompose pas
|
|
372
374
|
|
|
373
|
-
- **Un
|
|
374
|
-
_stores_ qu'il sait tenir (`nodefony.stores` de son `package.json`) — `drizzle` les
|
|
375
|
-
`
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
chaque donnée.
|
|
375
|
+
- **Un cache ne remplace pas une base : ils se COMPLÈTENT.** Chaque adaptateur déclare les
|
|
376
|
+
_stores_ qu'il sait tenir (`nodefony.stores` de son `package.json`) — `drizzle` et `mongoose` les
|
|
377
|
+
huit, `redis` quatre (`session`, `tokens`, `passkeys`, `idempotency`) : un cache ne garde ni
|
|
378
|
+
comptes ni journal d'audit. Ne promets jamais une parité qui n'existe pas — lis ce champ, et
|
|
379
|
+
`npx nodefony inspect stores` dit où atterrit chaque donnée.
|
|
379
380
|
- **Les violations de contrainte sont DÉJÀ traduites en HTTP — ne les rattrape pas.** Un doublon
|
|
380
381
|
sur une colonne unique ressort en **409**, une donnée qui viole le schéma Zod en **422**, chacun
|
|
381
382
|
avec son corps JSON : le rendu d'erreur lit le code du pilote (`23505`, `ER_DUP_ENTRY`,
|
|
@@ -307,18 +307,19 @@ nodefony orm:migrate:status
|
|
|
307
307
|
Un refus n'est pas une panne : c'est le produit qui s'arrête devant une décision qui t'appartient.
|
|
308
308
|
Le `code` est stable — **lis-le, il désigne le geste**.
|
|
309
309
|
|
|
310
|
-
| Code | Ce qui s'est passé
|
|
311
|
-
| ---------------------------------- |
|
|
312
|
-
| `NF_MIGRATE_BASELINE_REQUIRED` | la base porte déjà les tables, sans aucun historique
|
|
313
|
-
| `NF_GENERATE_DATABASE_NOT_ADOPTED` | aucune migration n'existe, et la base porte déjà ces tables
|
|
314
|
-
| `NF_MIGRATE_BASELINE_NOT_EMPTY` | `--from-database` demandé alors que des migrations existent
|
|
315
|
-
| `
|
|
316
|
-
| `
|
|
317
|
-
| `
|
|
318
|
-
| `
|
|
319
|
-
| `
|
|
320
|
-
| `
|
|
321
|
-
| `
|
|
310
|
+
| Code | Ce qui s'est passé | Le geste |
|
|
311
|
+
| ---------------------------------- | ------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- |
|
|
312
|
+
| `NF_MIGRATE_BASELINE_REQUIRED` | la base porte déjà les tables, sans aucun historique | `orm:migrate:baseline` (l'adopter) |
|
|
313
|
+
| `NF_GENERATE_DATABASE_NOT_ADOPTED` | aucune migration n'existe, et la base porte déjà ces tables | `orm:migrate:baseline --from-database`, PUIS regénérer |
|
|
314
|
+
| `NF_MIGRATE_BASELINE_NOT_EMPTY` | `--from-database` demandé alors que des migrations existent | `orm:migrate:baseline` sans option |
|
|
315
|
+
| `NF_MIGRATE_SECONDARY_CONNECTOR` | un connecteur secondaire porte le nom d'un dossier de dialecte (`sqlite`, `postgres`, `mysql`, `meta`) | renommer le connecteur dans `connectors` — un secondaire range ses migrations sous `migrations/<connecteur>/` |
|
|
316
|
+
| `NF_MIGRATE_FAILED_MARKER` | une migration a échoué, ou n'a jamais fini | LIRE l'erreur, puis `orm:migrate:repair` |
|
|
317
|
+
| `NF_MIGRATE_HASH_MISMATCH` | un fichier déjà appliqué a été modifié | RÉTABLIR le fichier (1er geste) ; `--update-hashes` seulement si la modification était sans effet |
|
|
318
|
+
| `NF_MIGRATE_OUT_OF_ORDER` | une migration en attente se range avant la dernière posée | renommer la nouvelle après la dernière appliquée |
|
|
319
|
+
| `NF_MIGRATE_MISSING_FILE` | une migration appliquée n'a plus de fichier | rétablir le fichier — il fait partie de l'historique |
|
|
320
|
+
| `NF_GENERATE_DATABASE_BEHIND` | rien à écrire, et pourtant la base ne porte pas le schéma | l'historique affirme une migration jamais exécutée : `orm:migrate:repair --forget <source>/<tag>` puis `orm:migrate` |
|
|
321
|
+
| `NF_MIGRATE_LOCK_TIMEOUT` | un autre travail de migration tient le verrou | ATTENDRE puis rejouer — ce n'est pas une panne, et le verbe est idempotent |
|
|
322
|
+
| `NF_MIGRATE_DESTRUCTIVE` | les migrations en attente SUPPRIMENT des données | lire le SQL (`--dry-run`), puis assumer avec `--allow-destructive` |
|
|
322
323
|
|
|
323
324
|
Les codes exhaustifs, avec un exemple de charge utile pour chacun :
|
|
324
325
|
[`references/verdicts.md`](references/verdicts.md).
|
|
@@ -113,24 +113,25 @@ n'a jamais les deux — c'est le discriminant à tester.
|
|
|
113
113
|
|
|
114
114
|
### Refus d'usage — la demande elle-même
|
|
115
115
|
|
|
116
|
-
| Code
|
|
117
|
-
|
|
|
118
|
-
| `NF_MIGRATE_UNKNOWN_CONNECTOR`
|
|
119
|
-
| `NF_MIGRATE_UNKNOWN_TAG`
|
|
120
|
-
| `NF_MIGRATE_UNKNOWN_SOURCE`
|
|
121
|
-
| `NF_MIGRATE_URL_MISMATCH`
|
|
122
|
-
| `NF_MIGRATE_NOT_CONFIGURED`
|
|
123
|
-
| `
|
|
124
|
-
| `
|
|
125
|
-
| `
|
|
126
|
-
| `
|
|
127
|
-
| `
|
|
128
|
-
| `
|
|
129
|
-
| `
|
|
130
|
-
| `
|
|
131
|
-
| `
|
|
132
|
-
| `
|
|
133
|
-
| `
|
|
116
|
+
| Code | Ce qui s'est passé | Le geste |
|
|
117
|
+
| -------------------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------- |
|
|
118
|
+
| `NF_MIGRATE_UNKNOWN_CONNECTOR` | aucun connecteur de ce nom | le message liste ceux que l'application déclare |
|
|
119
|
+
| `NF_MIGRATE_UNKNOWN_TAG` | `--up-to` désigne une migration inconnue | relire `sources[].pendingTags` |
|
|
120
|
+
| `NF_MIGRATE_UNKNOWN_SOURCE` | `--source` n'est pas déclarée par cette application | relire `sources[].name` |
|
|
121
|
+
| `NF_MIGRATE_URL_MISMATCH` | la variable de migration désigne une base d'un AUTRE dialecte | corriger la variable, ou choisir le bon connecteur |
|
|
122
|
+
| `NF_MIGRATE_NOT_CONFIGURED` | connecteur SQL non déclaré dans la configuration | le déclarer pour pouvoir le suivre |
|
|
123
|
+
| `NF_MIGRATE_SECONDARY_CONNECTOR` | un connecteur secondaire porte le nom d'un dossier de dialecte (`sqlite`, `postgres`, `mysql`, `meta`) | renommer le connecteur dans `connectors` — un secondaire range ses migrations sous `migrations/<connecteur>/` |
|
|
124
|
+
| `NF_MIGRATE_NO_MIGRATIONS` | ce connecteur est porté par une base qui ne se migre pas ainsi | rien à migrer ici — ce n'est pas une panne |
|
|
125
|
+
| `NF_MIGRATE_NOT_DEVELOPMENT` | geste réservé au développement, demandé ailleurs | passer par le travail de déploiement |
|
|
126
|
+
| `NF_MIGRATE_DESTRUCTIVE` | des migrations en attente SUPPRIMENT des données | relire le fichier produit, puis assumer explicitement |
|
|
127
|
+
| `NF_MIGRATE_UNAVAILABLE` | la commande s'est arrêtée sans pouvoir nommer la cause | constater l'état (`orm:migrate:status`) avant de reprendre |
|
|
128
|
+
| `NF_MIGRATE_CONFIRM_REQUIRED` | `orm:reset` demandé hors terminal, ou en sortie machine | relancer avec `--yes` si l'effacement est voulu |
|
|
129
|
+
| `NF_GENERATE_NAME` | le nom de migration demandé n'est pas utilisable | reprendre la suggestion que le message donne |
|
|
130
|
+
| `NF_GENERATE_MISSING_ENTITY` | une entité est enregistrée sans fichier qui la fournisse | `nodefony inspect entities --json` pour la situer |
|
|
131
|
+
| `NF_GENERATE_FRAMEWORK_TABLE` | une entité de l'application usurpe une table du framework | la renommer, ou écrire une migration libre (`--custom`) |
|
|
132
|
+
| `NF_GENERATE_DESTRUCTIVE` | la migration ÉCRITE supprime des données | relire le fichier, puis annuler (`git checkout`) ou appliquer — la mise en service a sa propre garde |
|
|
133
|
+
| `NF_GENERATE_DATABASE_BEHIND` | rien à écrire, et pourtant la base ne porte pas le schéma | `repair --forget <source>/<tag>` : l'historique affirme une migration jamais exécutée |
|
|
134
|
+
| `NF_GENERATE_TOOL_MISSING` | l'outil de génération n'est pas installé | l'installer en dépendance de développement |
|
|
134
135
|
|
|
135
136
|
## Un contrat qui ne bougera pas
|
|
136
137
|
|