@nodefony/drizzle 10.0.0-alpha.8 → 10.0.0-alpha.9

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 (50) hide show
  1. package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorate.js +1 -1
  2. package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorateMetadata.js +1 -1
  3. package/dist/index.js +9 -6
  4. package/dist/nodefony/command/migrateShared.js +17 -1
  5. package/dist/nodefony/command/orm-generate.js +11 -7
  6. package/dist/nodefony/command/orm-migrate-baseline.js +5 -2
  7. package/dist/nodefony/command/orm-reset.js +1 -0
  8. package/dist/nodefony/config/defineModuleConfig.js +21 -5
  9. package/dist/nodefony/entity/userTable.js +3 -0
  10. package/dist/nodefony/registerStores.js +2 -26
  11. package/dist/nodefony/service/DrizzleService.js +19 -7
  12. package/dist/nodefony/src/DrizzleAuditStore.js +13 -1
  13. package/dist/nodefony/src/DrizzleIdempotencyStore.js +14 -2
  14. package/dist/nodefony/src/DrizzleTokenStore.js +13 -1
  15. package/dist/nodefony/src/DrizzleTotpSecretStore.js +13 -1
  16. package/dist/nodefony/src/DrizzleWebAuthnCredentialStore.js +13 -1
  17. package/dist/nodefony/src/DrizzleWebhookStore.js +13 -1
  18. package/dist/nodefony/src/SessionStorage.js +8 -0
  19. package/dist/nodefony/src/frameworkConnector.js +29 -0
  20. package/dist/nodefony/src/memoryDatabase.js +13 -0
  21. package/dist/nodefony/src/migrator/appSchema.js +44 -1
  22. package/dist/nodefony/src/migrator/drivers/sqliteDriver.js +1 -0
  23. package/dist/nodefony/src/migrator/kit.js +23 -2
  24. package/dist/nodefony/src/migrator/paths.js +55 -1
  25. package/dist/nodefony/src/migrator/refusals.js +50 -1
  26. package/dist/nodefony/src/migrator/resolve.js +55 -5
  27. package/dist/nodefony/src/orm-core/DrizzleOrm.js +2 -1
  28. package/dist/nodefony/src/safeTarget.js +1 -0
  29. package/dist/types/nodefony/command/migrateShared.d.ts +10 -0
  30. package/dist/types/nodefony/config/defineModuleConfig.d.ts +5 -1
  31. package/dist/types/nodefony/entity/userTable.d.ts +46 -3
  32. package/dist/types/nodefony/registerStores.d.ts +4 -3
  33. package/dist/types/nodefony/src/DrizzleAuditStore.d.ts +6 -0
  34. package/dist/types/nodefony/src/DrizzleIdempotencyStore.d.ts +8 -1
  35. package/dist/types/nodefony/src/DrizzleTokenStore.d.ts +6 -0
  36. package/dist/types/nodefony/src/DrizzleTotpSecretStore.d.ts +6 -0
  37. package/dist/types/nodefony/src/DrizzleWebAuthnCredentialStore.d.ts +6 -0
  38. package/dist/types/nodefony/src/DrizzleWebhookStore.d.ts +6 -0
  39. package/dist/types/nodefony/src/SessionStorage.d.ts +6 -0
  40. package/dist/types/nodefony/src/frameworkConnector.d.ts +24 -0
  41. package/dist/types/nodefony/src/memoryDatabase.d.ts +10 -0
  42. package/dist/types/nodefony/src/migrator/appSchema.d.ts +25 -0
  43. package/dist/types/nodefony/src/migrator/kit.d.ts +15 -0
  44. package/dist/types/nodefony/src/migrator/paths.d.ts +35 -0
  45. package/dist/types/nodefony/src/migrator/refusals.d.ts +36 -1
  46. package/dist/types/nodefony/src/migrator/resolve.d.ts +44 -1
  47. package/dist/types/nodefony/src/orm-core/DrizzleOrm.d.ts +11 -0
  48. package/docs/index.md +28 -25
  49. package/docs/migrations.md +39 -3
  50. package/package.json +18 -19
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Nom de fichier d'une base SQLite en MÉMOIRE — volatile, elle repart vide à
3
+ * chaque démarrage.
4
+ *
5
+ * Module FEUILLE (aucun import) : l'adapter, le migrateur, les commandes et le
6
+ * service le consultent tous, et le placer dans l'un d'eux ferait importer le
7
+ * migrateur par l'adapter. Une seule écriture du littéral : c'est d'elle que
8
+ * dépend la règle « une base volatile dérive son schéma » (`resolveDdlMode`).
9
+ */
10
+ export declare const MEMORY_DATABASE = ":memory:";
@@ -221,3 +221,28 @@ export declare function usurpedTables(tables: readonly IDiscoveredTable[], frame
221
221
  * @returns les entités et leur table, dans l'ordre du registre.
222
222
  */
223
223
  export declare function registeredTables(connector: string): IExpectedEntity[];
224
+ /**
225
+ * Tables d'un connecteur parmi celles que les fichiers fournissent — le
226
+ * partage qui fait qu'une migration ne décrit que SA base.
227
+ *
228
+ * Une table appartient au connecteur de l'entité qui la déclare (le REGISTRE
229
+ * le sait, un fichier non). Une table qu'aucune entité ne réclame reste au
230
+ * connecteur du framework, comme avant que les connecteurs secondaires aient
231
+ * leurs migrations. Sans ce partage, la table d'une entité secondaire entrait
232
+ * AUSSI dans les migrations du framework : créée dans une base qui ne s'en sert
233
+ * jamais, et proposée à la suppression le jour où elle n'y figure plus.
234
+ *
235
+ * @param tables - tables fournies par les fichiers de l'application.
236
+ * @param connector - connecteur visé.
237
+ * @param owners - `table → connecteurs` qui la déclarent ; par défaut, le registre.
238
+ * @returns les tables de ce connecteur, dans l'ordre reçu.
239
+ */
240
+ export declare function tablesOfConnector<T extends {
241
+ tableName: string;
242
+ }>(tables: readonly T[], connector: string, owners?: ReadonlyMap<string, ReadonlySet<string>>): T[];
243
+ /**
244
+ * Pour chaque table enregistrée, les connecteurs dont une entité la déclare.
245
+ *
246
+ * @returns la table de correspondance, lue dans le registre des entités.
247
+ */
248
+ export declare function registeredOwners(): Map<string, Set<string>>;
@@ -129,6 +129,21 @@ export declare function auditMigrationSql(sql: string, dialect: SqlDialect): IMi
129
129
  * @returns `true` si l'échec vient d'une question restée sans terminal.
130
130
  */
131
131
  export declare function isInteractivePromptFailure(output: string): boolean;
132
+ /**
133
+ * Reconnaît le refus de l'outil devant un `await` de premier niveau.
134
+ *
135
+ * C'est l'esbuild de `drizzle-kit` qui le détecte, en transpilant le fichier en
136
+ * CommonJS : sa ligne d'erreur nomme le fichier et la position. On la lit plutôt
137
+ * que d'analyser nous-mêmes les sources — l'outil qui refuse est aussi celui qui
138
+ * sait exactement où, sans faux positif. Fonction PURE, éprouvée sans process.
139
+ *
140
+ * @param output - sortie complète de l'outil.
141
+ * @returns le fichier et la ligne, ou `null` si l'échec est d'une autre nature.
142
+ */
143
+ export declare function topLevelAwaitFailure(output: string): {
144
+ file: string;
145
+ line: number;
146
+ } | null;
132
147
  /**
133
148
  * Pose le marqueur de format en tête des `.sql` d'un dossier qui ne l'ont pas.
134
149
  *
@@ -12,6 +12,37 @@ export declare const FRAMEWORK_RANK = 0;
12
12
  * celles des modules ; l'inverse n'arrive jamais.
13
13
  */
14
14
  export declare const APP_RANK = 1000000;
15
+ /**
16
+ * Noms qui ne peuvent PAS désigner le dossier d'un connecteur secondaire : ce
17
+ * sont ceux des dossiers de dialecte (et du journal) que le connecteur du
18
+ * framework range déjà sous `migrations/`. Un connecteur `postgres` y lirait les
19
+ * migrations de `default` comme les siennes.
20
+ */
21
+ export declare const RESERVED_MIGRATION_DIRS: readonly string[];
22
+ /**
23
+ * Le nom de ce connecteur collisionne-t-il avec un dossier de dialecte ?
24
+ *
25
+ * @param connector - nom du connecteur (clé de `connectors`).
26
+ * @returns `true` pour un connecteur SECONDAIRE nommé comme un dossier réservé.
27
+ */
28
+ export declare function isReservedConnectorName(connector: string): boolean;
29
+ /**
30
+ * Dossier des migrations PROPRES à un connecteur — la seule règle qui dit où
31
+ * elles vivent.
32
+ *
33
+ * Le connecteur du framework garde la racine (`migrations/<dialecte>`) : ses
34
+ * migrations décrivent la base du framework et des entités de l'application.
35
+ * Un connecteur secondaire reçoit son sous-dossier (`migrations/<connecteur>/
36
+ * <dialecte>`) : sa base ne reçoit que ce qui y est écrit, et celle du
37
+ * framework n'en voit rien — le dossier de dialecte qu'elle lit n'est pas le
38
+ * même.
39
+ *
40
+ * @param appDir - dossier de migrations de l'application (`appMigrationsDir`).
41
+ * @param connector - nom du connecteur.
42
+ * @returns le dossier parent des sous-dossiers de dialecte, ou `undefined` sans
43
+ * racine d'application, ou pour un nom réservé ({@link isReservedConnectorName}).
44
+ */
45
+ export declare function connectorMigrationsDir(appDir: string | undefined, connector: string): string | undefined;
15
46
  /**
16
47
  * Dossier des migrations livrées par ce paquet.
17
48
  *
@@ -47,8 +78,12 @@ export declare function frameworkMigrationsDir(): Promise<string>;
47
78
  *
48
79
  * @param appDir - dossier de migrations de l'application, s'il y en a un.
49
80
  * @param options.framework - `false` quand le module est data-only.
81
+ * @param options.connector - connecteur visé ; un connecteur secondaire ne
82
+ * reçoit que SES migrations ({@link connectorMigrationsDir}), jamais celles
83
+ * du framework. Omis : le registre complet (usage direct).
50
84
  * @returns le registre, prêt pour l'applicateur.
51
85
  */
52
86
  export declare function defaultMigrationSources(appDir?: string, options?: {
53
87
  framework?: boolean;
88
+ connector?: string;
54
89
  }): Promise<IMigrationSource[]>;
@@ -37,6 +37,8 @@ export type CommandFailureCode =
37
37
  | "NF_GENERATE_TOOL_MISSING"
38
38
  /** L'outil de génération pose une question, et il n'y a pas de terminal. */
39
39
  | "NF_GENERATE_NEEDS_ANSWER"
40
+ /** Un fichier d'entité porte un `await` de premier niveau, que l'outil ne sait pas lire. */
41
+ | "NF_GENERATE_TOP_LEVEL_AWAIT"
40
42
  /** L'outil de génération s'est arrêté ; ce qu'il a dit est remonté tel quel. */
41
43
  | "NF_GENERATE_TOOL_FAILED"
42
44
  /** La lecture du schéma d'une base existante a échoué. */
@@ -46,7 +48,9 @@ export type CommandFailureCode =
46
48
  /** L'adoption par lecture de la base, demandée alors qu'il existe déjà des migrations. */
47
49
  | "NF_MIGRATE_BASELINE_NOT_EMPTY"
48
50
  /** La table d'historique existe, mais ce n'est pas celle du framework. */
49
- | "NF_MIGRATE_HISTORY_FOREIGN";
51
+ | "NF_MIGRATE_HISTORY_FOREIGN"
52
+ /** Écrire une migration pour un connecteur secondaire au nom réservé (`postgres`…). */
53
+ | "NF_MIGRATE_SECONDARY_CONNECTOR";
50
54
  /**
51
55
  * Ce que la découverte des entités a VU, au moment où la commande a refusé.
52
56
  *
@@ -157,6 +161,22 @@ export declare function formatToolOutput(output: string, maxLines?: number): str
157
161
  * @returns le refus, prêt pour la ligne de commande comme pour l'écran.
158
162
  */
159
163
  export declare function generationNeedsAnswer(label: string, replay: string): IResolutionRefusal;
164
+ /**
165
+ * Un fichier lu par l'outil de génération porte un `await` de premier niveau.
166
+ *
167
+ * Nodefony est ESM, et l'application démarre très bien avec ce fichier. Mais
168
+ * l'outil qui écrit les migrations (`drizzle-kit`) relit les fichiers de schéma
169
+ * HORS de la chaîne de l'application, en les transpilant en CommonJS — où un
170
+ * `await` de premier niveau n'existe pas. Sans ce refus, l'utilisateur recevait
171
+ * une pile d'appels d'esbuild qui ne nomme ni l'outil ni le remède.
172
+ *
173
+ * @param label - ce qui était généré, tel qu'on le cite à l'utilisateur.
174
+ * @param file - fichier fautif, tel que l'outil l'a nommé.
175
+ * @param line - ligne de l'`await`.
176
+ * @param replay - la commande à relancer une fois le fichier corrigé.
177
+ * @returns le refus, prêt pour la ligne de commande comme pour l'écran.
178
+ */
179
+ export declare function generationTopLevelAwait(label: string, file: string, line: number, replay: string): IResolutionRefusal;
160
180
  /**
161
181
  * L'outil de génération s'est arrêté, et ce qu'il a dit est remonté TEL QUEL.
162
182
  *
@@ -218,6 +238,21 @@ export declare function moduleAbsent(): IResolutionRefusal;
218
238
  * @returns le refus, prêt pour la ligne de commande comme pour l'écran.
219
239
  */
220
240
  export declare function notConfigured(connector: string, driver: string): IResolutionRefusal;
241
+ /**
242
+ * Écrire une migration pour un connecteur secondaire dont le NOM est celui d'un
243
+ * dossier de dialecte — refusé.
244
+ *
245
+ * Un connecteur secondaire range ses migrations sous `migrations/<connecteur>/`,
246
+ * à côté des dossiers de dialecte du connecteur du framework. Nommé `postgres`,
247
+ * son dossier SERAIT celui des migrations PostgreSQL du framework : il les
248
+ * lirait comme les siennes, et les siennes seraient appliquées à la base du
249
+ * framework. On le refuse en nommant la collision, plutôt que de le découvrir
250
+ * au premier déploiement.
251
+ *
252
+ * @param connector - connecteur demandé.
253
+ * @returns le refus, prêt pour la ligne de commande comme pour l'écran.
254
+ */
255
+ export declare function reservedConnectorName(connector: string): IResolutionRefusal;
221
256
  /**
222
257
  * Traduit une résolution qui n'est PAS `ready` en refus lisible.
223
258
  *
@@ -67,6 +67,8 @@ export declare function resetAllowed(env: IMigrationEnv): boolean;
67
67
  * @returns `true` si l'enchaînement est recevable.
68
68
  */
69
69
  export declare function generateApplyAllowed(env: IMigrationEnv): boolean;
70
+ import { MEMORY_DATABASE } from "../memoryDatabase.js";
71
+ export { MEMORY_DATABASE };
70
72
  /**
71
73
  * Le mode de schéma qui s'applique à un connecteur.
72
74
  *
@@ -99,10 +101,18 @@ export declare function generateApplyAllowed(env: IMigrationEnv): boolean;
99
101
  *
100
102
  * @param explicit - valeur écrite dans la configuration du connecteur, si elle l'est.
101
103
  * @param env - environnement constaté.
104
+ * 🔴 **Une base VOLATILE (`:memory:`) dérive toujours son schéma**, en
105
+ * production comprise. Rien d'autre ne peut la peupler : elle repart vide à
106
+ * chaque démarrage, et une migration appliquée ne survit pas au processus. En
107
+ * `none`, elle restait vide, le verdict de schéma tombait à `divergent` et la
108
+ * sonde de disponibilité retenait la mise en service (vécu en CI : le connecteur
109
+ * d'un module de banc, ouvert en production sous dérogation).
110
+ *
102
111
  * @param appHasMigrations - l'application versionne au moins une migration.
112
+ * @param volatile - la base est en mémoire (`:memory:`).
103
113
  * @returns le mode effectif.
104
114
  */
105
- export declare function resolveDdlMode(explicit: DdlMode | undefined, env: IMigrationEnv, appHasMigrations?: boolean): DdlMode;
115
+ export declare function resolveDdlMode(explicit: DdlMode | undefined, env: IMigrationEnv, appHasMigrations?: boolean, volatile?: boolean): DdlMode;
106
116
  /**
107
117
  * L'application versionne-t-elle au moins une migration ?
108
118
  *
@@ -120,6 +130,21 @@ export declare function resolveDdlMode(explicit: DdlMode | undefined, env: IMigr
120
130
  * ou sous un sous-dossier de dialecte (`migrations/postgres/0001_x.sql`).
121
131
  */
122
132
  export declare function appVersionsMigrations(dir: string | undefined): boolean;
133
+ /**
134
+ * Des migrations versionnées CONCERNENT-elles ce connecteur ?
135
+ *
136
+ * C'est la question que pose la bascule `auto` → `migrate`, et elle n'est pas
137
+ * « l'application versionne-t-elle des migrations ? » : celles de la racine
138
+ * décrivent la base du framework. Posée telle quelle, elle faisait basculer en
139
+ * `migrate` un connecteur secondaire qui n'en recevra jamais une — sa base
140
+ * restait alors sans schéma, ni dérivé ni migré. On regarde donc le dossier
141
+ * DU connecteur ({@link connectorMigrationsDir}).
142
+ *
143
+ * @param connector - nom du connecteur.
144
+ * @param dir - dossier de migrations de l'application ({@link appMigrationsDir}).
145
+ * @returns `true` si le dossier propre au connecteur porte au moins un `.sql`.
146
+ */
147
+ export declare function connectorVersionsMigrations(connector: string, dir: string | undefined): boolean;
123
148
  /**
124
149
  * La conduite de la sonde de disponibilité face à un schéma en retard.
125
150
  *
@@ -133,6 +158,24 @@ export declare function appVersionsMigrations(dir: string | undefined): boolean;
133
158
  * @returns la conduite effective.
134
159
  */
135
160
  export declare function resolveCheckMode(explicit: MigrationCheckMode | undefined, env: IMigrationEnv): MigrationCheckMode;
161
+ /**
162
+ * En schéma `auto`, faut-il ÉNONCER au démarrage les migrations enregistrées
163
+ * que personne n'a appliquées ?
164
+ *
165
+ * Oui en développement, sauf conduite `off` — c'est ce conseil qui évite de
166
+ * conclure à une base incohérente devant une colonne inconnue. Jamais sur une
167
+ * base `:memory:` : elle repart VIDE à chaque démarrage, son historique aussi,
168
+ * si bien que tout y paraît « à appliquer » à chaque fois — un avertissement
169
+ * permanent qui n'apprend rien (vécu : le connecteur de banc `mediasoup`,
170
+ * une fois déclaré, réclamait à chaque démarrage les migrations du framework
171
+ * et de l'application, qui ne le concernent pas).
172
+ *
173
+ * @param check - conduite résolue ({@link resolveCheckMode}).
174
+ * @param env - environnement constaté.
175
+ * @param filename - fichier sqlite du connecteur, s'il en a un.
176
+ * @returns `true` si le conseil doit être rendu.
177
+ */
178
+ export declare function adviseMigrations(check: MigrationCheckMode, env: IMigrationEnv, filename: string | undefined): boolean;
136
179
  /** Ce qu'un connecteur est, du point de vue des migrations. */
137
180
  export type IConnectorResolution = {
138
181
  /** Le connecteur existe et son propriétaire sait migrer. */
@@ -1,3 +1,4 @@
1
+ import type { Container } from "nodefony";
1
2
  import { Orm } from "@nodefony/orm-core";
2
3
  import type { IOrmMigrationApplyReply, IOrmMigrationPlanReply, IOrmMigrationReply } from "@nodefony/orm-core";
3
4
  import { type ISchemaComparison } from "../migrator/schemaDiff.js";
@@ -18,6 +19,16 @@ export interface DrizzleOrmOptions {
18
19
  * `mysql`. Requise pour ces dialectes.
19
20
  */
20
21
  url?: string;
22
+ /**
23
+ * Container du service qui construit l'ORM — il porte le `syslog` du kernel.
24
+ *
25
+ * Sans lui, l'ORM se fabrique un container et un journal à lui, que rien ne
26
+ * relie à la sortie du serveur : « connexion perdue » et « connexion
27
+ * rétablie » partaient dans le vide, et l'exploitant ne voyait que les
28
+ * échecs des sondes, jamais le diagnostic de la reprise. Omis en usage
29
+ * direct (bancs de test) : l'ORM reste alors autonome, comme avant.
30
+ */
31
+ container?: Container;
21
32
  /**
22
33
  * Le schéma doit-il être DÉRIVÉ du code à la connexion ?
23
34
  *
package/docs/index.md CHANGED
@@ -342,10 +342,10 @@ de drizzle qui sert de modèle. Deux fichiers, mêmes noms partout, aucune quest
342
342
  | `nodefony/config/config.ts` | **le QUOI** | schéma Zod commenté = source **unique** des défauts, matérialisés `parse({})` |
343
343
  | `nodefony/config/defineModuleConfig.ts` | **le COMMENT** | builder **pur** : parse → surcharge d'environnement → gel |
344
344
 
345
- Concrètement : `drizzleConfigSchema` (`config.ts:79`) porte chaque `.default()` et chaque
345
+ Concrètement : `drizzleConfigSchema` (`config.ts:136`) porte chaque `.default()` et chaque
346
346
  `.describe()` — changer un défaut du module, c'est éditer **là et nulle part ailleurs**. Le builder
347
- `defineDrizzleConfig()` (`defineModuleConfig.ts:58`) ne retape jamais une valeur : il valide, applique
348
- l'environnement, gèle. Et `drizzleConfigJsonSchema()` (`defineModuleConfig.ts:69`) expose le tout en
347
+ `defineDrizzleConfig()` (`defineModuleConfig.ts:81`) ne retape jamais une valeur : il valide, applique
348
+ l'environnement, gèle. Et `drizzleConfigJsonSchema()` (`defineModuleConfig.ts:99`) expose le tout en
349
349
  JSON Schema pour l'écran de configuration de Studio.
350
350
 
351
351
  Le schéma reste **pur** : il ne lit ni `process.env` ni le kernel. C'est ce qui rend le module
@@ -361,11 +361,11 @@ importable et testable sans serveur.
361
361
  | `connectors.<n>.url` | `string` | — | Chaîne de connexion `postgres://…` / `mysql://…`. **Porte un secret.** |
362
362
  | `frameworkEntities` | `boolean` | `true` | Déclare (ou non) le schéma des huit briques durables sur `default`. |
363
363
 
364
- Table dérivée de `drizzleConfigSchema` (`config.ts:79`) et de `SQL_DIALECTS` (`config.ts:37`).
364
+ Table dérivée de `drizzleConfigSchema` (`config.ts:136`) et de `SQL_DIALECTS` (`config.ts:37`).
365
365
 
366
366
  **`filename` est volontairement sans défaut.** Le chemin dépend du kernel, qui n'existe pas quand le
367
367
  schéma est évalué. Il est résolu **au démarrage** par `DrizzleService.#defaultFilename()`
368
- (`DrizzleService.ts:95`) vers `<app>/var/databases/nodefony-<connecteur>.db` — sous `var/`, le dossier
368
+ (`DrizzleService.ts:202`) vers `<app>/var/databases/nodefony-<connecteur>.db` — sous `var/`, le dossier
369
369
  commun des données runtime : « où sont mes données ? » a une réponse unique, un seul chemin à
370
370
  sauvegarder et à ignorer dans git.
371
371
 
@@ -385,9 +385,12 @@ NF_DATABASE_URL=mysql://app:secret@db:3306/prod
385
385
  NF_DATABASE_URL=sqlite:/var/lib/app/prod.db
386
386
  ```
387
387
 
388
- `applyEnvOverrides()` (`defineModuleConfig.ts:20`) lit l'infra, en déduit le dialecte depuis le
389
- _scheme_, et pose `filename` ou `url` sur le connecteur primaire. Une URL `mongodb://` est **ignorée
390
- ici** : elle appartient alors à `@nodefony/mongoose`.
388
+ `applyEnvOverrides()` (`defineModuleConfig.ts:32`) lit l'infra, en déduit le dialecte depuis le
389
+ _scheme_, et pose `filename` ou `url` sur le connecteur primaire. Une URL `mongodb://` n'est **pas
390
+ lue ici** : elle appartient à `@nodefony/mongoose`. Drizzle en tire aussi la conséquence — il ne crée
391
+ **pas** le connecteur `default` que ses défauts posent, donc aucune base SQLite parallèle et aucun
392
+ schéma du framework en double. Un `default` que l'application écrit elle-même dans
393
+ `use("@nodefony/drizzle", { connectors: { default: … } })` est gardé, comme tout autre connecteur.
391
394
 
392
395
  **Situation 3 — plusieurs bases.** Un connecteur par base. Seul `default` porte le schéma du framework ;
393
396
  les autres sont à toi.
@@ -403,13 +406,13 @@ use("@nodefony/drizzle", {
403
406
 
404
407
  > [!WARNING]
405
408
  > Une URL de connexion **porte un mot de passe**. Le module ne la journalise jamais telle quelle :
406
- > `redactUrl()` (`DrizzleService.ts:68`) remplace le mot de passe par `***` avant tout log de
409
+ > `redactUrl()` (`DrizzleService.ts:86`) remplace le mot de passe par `***` avant tout log de
407
410
  > démarrage, et la sonde d'administration applique la même règle.
408
411
 
409
412
  ### Quand la connexion échoue, le démarrage échoue
410
413
 
411
414
  Un connecteur **déclaré** qui ne se connecte pas lève une `BootConfigurationError`
412
- (`DrizzleService.ts:244`) — en développement **comme** en production. Ce n'est pas une sévérité
415
+ (`DrizzleService.ts:340`) — en développement **comme** en production. Ce n'est pas une sévérité
413
416
  gratuite : une infrastructure déclarée mais injoignable ne se répare pas en continuant. Un serveur qui
414
417
  démarrerait « vivant » avec ses stores morts accepterait des requêtes pour échouer plus tard, la cause
415
418
  noyée dans un avertissement. Le message d'erreur nomme le connecteur, le dialecte, la cible rédigée et
@@ -417,7 +420,7 @@ la piste à vérifier.
417
420
 
418
421
  Pour les dialectes réseau, la connexion fait un **ping réel** au démarrage : les pools `pg` et `mysql2`
419
422
  sont paresseux, sans ce `SELECT 1` une base morte « se connecterait » et n'échouerait qu'à la première
420
- requête métier (`#connectPostgres()`, `DrizzleOrm.ts:998` · `#connectMysql()`, `DrizzleOrm.ts:1296`).
423
+ requête métier (`#connectPostgres()`, `DrizzleOrm.ts:1193` · `#connectMysql()`, `DrizzleOrm.ts:1504`).
421
424
 
422
425
  ## Dialectes — une base par déploiement, un seul code
423
426
 
@@ -464,7 +467,7 @@ En MySQL, les verbes « qui rendent la ligne écrite » (`create`, `updateOne`,
464
467
  `findOneAndDelete`) se décomposent en sélection de la cible → mutation bornée par la clé primaire **avec
465
468
  le critère revérifié dans le `WHERE`** → relecture. Deux à trois allers-retours au lieu d'un : c'est le
466
469
  prix du dialecte, payé **uniquement** en MySQL. Une course perdue rend `null`, jamais une mutation hors
467
- critère (`#mysqlInsertReturning()`, `DrizzleRepository.ts:1138`).
470
+ critère (`#mysqlInsertReturning()`, `DrizzleRepository.ts:1149`).
468
471
 
469
472
  Le SQL brut nécessaire aux entités du framework est lui aussi routé par dialecte, dans un seul fichier
470
473
  (`queryKit.ts`) : recherche dans une colonne JSON (`findUserIdBySocialProvider()`, `queryKit.ts:76`),
@@ -504,7 +507,7 @@ connexion**, donc leurs tables sont créées au moment où l'ORM s'ouvre.
504
507
  | Pièce | Rôle | Ancre |
505
508
  | ---------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------ |
506
509
  | `Drizzle` (le module) | valide la config, déclare le schéma framework, monte le data plane | `index.ts` du module |
507
- | `DrizzleService` | ouvre un ORM par connecteur au boot, ferme tout à l'arrêt | `connectAll()`, `DrizzleService.ts:148` |
510
+ | `DrizzleService` | ouvre un ORM par connecteur au boot, ferme tout à l'arrêt | `connectAll()`, `DrizzleService.ts:186` |
508
511
  | `DrizzleOrm` | la connexion : DDL dérivé, repositories, transactions, sonde | `DrizzleOrm.ts:214` |
509
512
  | `DrizzleRepository<T>` | le CRUD portable, les opérateurs riches, l'eager-load | `DrizzleRepository.ts:146` |
510
513
  | `DrizzleTransaction` | `BEGIN`/`COMMIT`/`ROLLBACK` pilotés à la main, sur les trois dialectes | `DrizzleTransaction.ts:70` |
@@ -515,7 +518,7 @@ connexion**, donc leurs tables sont créées au moment où l'ORM s'ouvre.
515
518
  ### Le DDL dérivé — comment les tables apparaissent
516
519
 
517
520
  Drizzle ne « synchronise » pas un schéma. L'adapter dérive lui-même un `CREATE TABLE IF NOT EXISTS`
518
- depuis chaque table déclarée (`#buildCreateTable()`, `DrizzleOrm.ts:400`) et l'exécute à la connexion.
521
+ depuis chaque table déclarée (`#buildCreateTable()`, `DrizzleOrm.ts:509`) et l'exécute à la connexion.
519
522
  Trois conséquences à connaître **avant** de dépendre de ce mécanisme :
520
523
 
521
524
  1. il **crée**, il ne **modifie** pas — aucun `ALTER` n'est émis ;
@@ -571,7 +574,7 @@ Deux points de comportement qui évitent des surprises :
571
574
  `UPDATE` (`#pickOne()`, `DrizzleRepository.ts:259`). C'est ce qui rend ces verbes portables — MySQL
572
575
  interdit la forme naïve.
573
576
  - **l'eager-load est manuel** : une requête `IN (…)` par relation déclarée, puis regroupement en
574
- mémoire (`#populate()`, `DrizzleRepository.ts:683`). Choix assumé — pas de couche de relations à
577
+ mémoire (`#populate()`, `DrizzleRepository.ts:694`). Choix assumé — pas de couche de relations à
575
578
  déclarer une seconde fois, et le comportement est le même sur les trois dialectes.
576
579
 
577
580
  ### Transactions — une connexion dédiée, jamais le pool
@@ -600,7 +603,7 @@ Le mécanisme diffère par dialecte, sans que ton code le voie : en PostgreSQL/M
600
603
  emprunte une connexion **dédiée** au pool, rendue au commit — et **détruite** si celui-ci échoue,
601
604
  jamais recyclée dans un état inconnu. En SQLite la connexion est unique, donc c'est un pool de taille 1 :
602
605
  les transactions concurrentes sont **sérialisées par une file d'attente** (`#sqliteTxGate`,
603
- `DrizzleOrm.ts:267`), sinon deux requêtes HTTP simultanées émettraient deux `BEGIN` sur la même
606
+ `DrizzleOrm.ts:360`), sinon deux requêtes HTTP simultanées émettraient deux `BEGIN` sur la même
604
607
  connexion et la seconde échouerait.
605
608
 
606
609
  Les points de sauvegarde sont disponibles (`savepoint()`, `DrizzleTransaction.ts:123`) ; le nom est
@@ -624,7 +627,7 @@ const rows = await db.all(sql`
624
627
  `);
625
628
  ```
626
629
 
627
- C'est l'**anti-blocage** du modèle Repository (`getNativeConnection()`, `DrizzleOrm.ts:1524`) : CTE,
630
+ C'est l'**anti-blocage** du modèle Repository (`getNativeConnection()`, `DrizzleOrm.ts:1745`) : CTE,
628
631
  fonctions de fenêtre, sous-requêtes corrélées, jointures arbitraires. Deux contreparties assumées :
629
632
  ce SQL n'est plus portable entre dialectes, et il **ne passe pas** par la sonde de profilage des
630
633
  requêtes.
@@ -653,7 +656,7 @@ ou l'idempotence (`createIdempotencyTable`, `idempotencyEntity.ts:85`).
653
656
  ### Comment ça s'active — la réponse est : tout seul
654
657
 
655
658
  Chaque brique choisit son backend par une option `store`, dont le **défaut est `"auto"`**. La
656
- résolution automatique (`resolveAutoStore()`, `infra.ts:241`) applique cette préférence :
659
+ résolution automatique (`resolveAutoStore()`, `infra.ts:297`) applique cette préférence :
657
660
 
658
661
  1. une infra `database` déclarée (`NF_DATABASE_URL`) → **`drizzle`** (ou `mongoose` si l'URL est Mongo) ;
659
662
  2. sinon, un backend local persistant réellement chargé → **`drizzle`**, c'est-à-dire SQLite ;
@@ -714,8 +717,8 @@ construction : ils la résolvent à chaque appel. C'est nécessaire parce que l'
714
717
  le framework résout ses stores avant que l'ORM ne soit connecté — et parce que l'ORM se **déconnecte à
715
718
  l'arrêt** avant que les serveurs HTTP n'aient fini de vider leurs requêtes en vol. Handle absent =
716
719
  dégradation annoncée, pas un plantage : le `SessionStorage` rend une session vide et ignore les
717
- écritures (`#repo()`, `SessionStorage.ts:65`), le store d'idempotence laisse passer la mutation sans
718
- dédup (`begin()`, `DrizzleIdempotencyStore.ts:200`).
720
+ écritures (`#repo()`, `SessionStorage.ts:86`), le store d'idempotence laisse passer la mutation sans
721
+ dédup (`begin()`, `DrizzleIdempotencyStore.ts:220`).
719
722
 
720
723
  **SQL n'a pas de TTL.** Contrairement à Redis, rien n'expire tout seul : chaque store expose un `gc()`
721
724
  applicatif qui supprime les lignes échues, déclenché par un minuteur hors du chemin chaud —
@@ -732,7 +735,7 @@ c'est ce qui interdit de conclure « nouvelle mutation » hors d'une réservatio
732
735
  en deux instructions au verdict non ambigu (`reserveIdempotencyKeyMysql()`, `queryKit.ts:152`).
733
736
 
734
737
  **Le journal d'audit** pagine par un curseur **composite auto-portant** `<horodatage>:<id>` sur un ordre
735
- total, plutôt que par un identifiant seul (`listPage()`, `DrizzleAuditStore.ts:154`). Deux gains : plus
738
+ total, plutôt que par un identifiant seul (`listPage()`, `DrizzleAuditStore.ts:169`). Deux gains : plus
736
739
  d'aller-retour pour résoudre le curseur, et une pagination qui ne rembobine pas à la première page si
737
740
  l'événement de référence a été purgé entre deux appels.
738
741
 
@@ -772,8 +775,8 @@ entités que le DDL dérivé, et un banc vérifie sur les trois dialectes qu'une
772
775
  identique à une base **dérivée** — colonne par colonne, index compris. Sans cette preuve, les deux
773
776
  chemins divergeraient en silence.
774
777
 
775
- Les appliquer, les adopter, les réparer et surveiller leur état est le travail de cinq commandes
776
- `nodefony orm:migrate*` — avec leur verrou, leur historique et leur branchement sur la sonde de
778
+ Les écrire, les appliquer, les adopter, les réparer, surveiller leur état et repartir d'une base
779
+ vide est le travail de six commandes — `orm:generate`, les quatre `orm:migrate*` et `orm:reset` — avec leur verrou, leur historique et leur branchement sur la sonde de
777
780
  disponibilité. Tout est dans **[Migrations de schéma](migrations.md)**, qui porte aussi le patron de
778
781
  déploiement sans interruption et les droits du compte qui migre.
779
782
 
@@ -831,7 +834,7 @@ faire lui-même).
831
834
  Côté écrans : **Database**, **ORM (vue d'ensemble et par entité)** et **Stores** — ce dernier répond à
832
835
  la question « où sont écrites mes données ? » pour chaque brique.
833
836
 
834
- La sonde d'un connecteur s'adapte au dialecte (`probe()`, `DrizzleOrm.ts:1614`) :
837
+ La sonde d'un connecteur s'adapte au dialecte (`probe()`, `DrizzleOrm.ts:1835`) :
835
838
 
836
839
  - **SQLite** → `storage` : taille du fichier, mode de journal, pages libres (lus par `PRAGMA`) ;
837
840
  - **PostgreSQL / MySQL** → `pool` : taille, connexions libres, empruntées, en attente — **compteurs en
@@ -843,7 +846,7 @@ que promettre en silence — c'est le principe « superviser sans peser sur la p
843
846
 
844
847
  Chaque store expose aussi son **emplacement physique** pour l'écran Stores : le chemin du fichier
845
848
  SQLite, relativisé (anti-fuite d'information), et `undefined` pour un backend réseau — dont
846
- l'emplacement **est** l'infra déclarée, déjà affichée ailleurs (`location`, `DrizzleOrm.ts:415`).
849
+ l'emplacement **est** l'infra déclarée, déjà affichée ailleurs (`location`, `DrizzleOrm.ts:481`).
847
850
 
848
851
  ## ⚡ Performance & mémoire
849
852
 
@@ -80,7 +80,7 @@ D'où **trois modes**, un par connecteur, réglés par la clé `ddl` :
80
80
 
81
81
  ## 🚀 Démarrage rapide
82
82
 
83
- Cinq verbes, et un seul à retenir pour le quotidien du développement.
83
+ Six verbes, et un seul à retenir pour le quotidien du développement.
84
84
 
85
85
  ```bash
86
86
  # Ce que la base a reçu, ce qui reste, et ce qu'il faut taper. N'écrit rien.
@@ -117,6 +117,34 @@ Toutes acceptent `--connector <nom>` (défaut : `default`) et `--json`. Le flux
117
117
  Les migrations du **framework** sont livrées dans le paquet : vous n'avez pas à les produire. Celles
118
118
  de votre **application**, vous les écrivez avec `orm:generate` — voir la section suivante.
119
119
 
120
+ Ces deux familles de migrations appartiennent au **seul connecteur `default`**, celui qui porte les
121
+ briques du framework : le dossier `migrations/<dialecte>` décrit sa base, et aucune autre.
122
+
123
+ Un **connecteur secondaire** (une base d'analyse, la base d'un module, un second PostgreSQL) a ses
124
+ **propres** migrations, dans son sous-dossier :
125
+
126
+ ```text
127
+ migrations/
128
+ ├── sqlite/ ← connecteur « default » (framework + application)
129
+ └── analytics/
130
+ └── postgres/ ← connecteur « analytics », et lui seul
131
+ ```
132
+
133
+ ```bash
134
+ nodefony orm:generate --connector analytics --name init # écrit sous migrations/analytics/<dialecte>
135
+ nodefony orm:migrate --connector analytics # l'applique à la base d'« analytics »
136
+ ```
137
+
138
+ Chaque base ne reçoit que son dossier : `default` ne voit pas les migrations d'`analytics`, et
139
+ inversement. `orm:generate --connector analytics` ne décrit que les tables des entités déclarées sur
140
+ ce connecteur (`@entity({ connector: "analytics" })`) ; une table qu'aucune entité ne réclame reste
141
+ à `default`. Tant qu'un secondaire n'a rien versionné, rien ne change : en développement son schéma
142
+ est dérivé du code. Dès son premier fichier, il bascule en `migrate` comme `default`.
143
+
144
+ Un connecteur secondaire ne peut pas s'appeler `sqlite`, `postgres`, `mysql` ni `meta` : son dossier
145
+ serait celui d'un dialecte de `default`. Les commandes qui écriraient une migration le refusent
146
+ (`NF_MIGRATE_SECONDARY_CONNECTOR`) — renommez le connecteur, son nom ne sert qu'à le désigner.
147
+
120
148
  Côté configuration, il n'y a rien à écrire pour le cas courant : le mode se résout par
121
149
  environnement. Ne le déclarer que pour s'en écarter — un serveur unique qui migre au démarrage :
122
150
 
@@ -224,6 +252,14 @@ dépendances de développement (`--omit=dev`, une image de production). Le refus
224
252
  geste ; il ne parle pas de votre base, qui n'y est pour rien — appliquer des migrations, lui, ne
225
253
  réclame aucun outil tiers.
226
254
 
255
+ Un cinquième tient à la forme d'un fichier d'entité : **un `await` de premier niveau**
256
+ (`NF_GENERATE_TOP_LEVEL_AWAIT`). Votre application est ESM et démarre très bien avec lui, mais
257
+ l'outil qui écrit les migrations relit les fichiers de schéma de son côté, en CommonJS, où cette
258
+ construction n'existe pas. Le refus nomme le fichier et la ligne. Un fichier d'entité **déclare**
259
+ une table : ce qui se décide à l'exécution — choisir une entité selon l'infrastructure, importer
260
+ un module à la demande — se place dans l'`index.ts` du module, dont `@entities([...])` accepte une
261
+ liste calculée.
262
+
227
263
  ### Une entité qui pointe vers une table du framework
228
264
 
229
265
  Déclarer une référence vers une table du framework (`session`, `audit_event`…) est légitime : ce
@@ -390,7 +426,7 @@ Trois propriétés en découlent, et elles valent d'être nommées :
390
426
  - **Rien n'est appliqué deux fois.** L'historique (`nodefony_migrations`, `types.ts:23`) est écrit
391
427
  dans la même transaction que le DDL, là où le moteur le permet.
392
428
  - **Un pod en retard ne reçoit pas de trafic.** En `none` comme en `migrate`, l'état du schéma est
393
- publié à la sonde de disponibilité (`#publishReadiness()`, `DrizzleService.ts:445`) : `/readyz`
429
+ publié à la sonde de disponibilité (`#publishReadiness()`, `DrizzleService.ts:527`) : `/readyz`
394
430
  répond 503, l'orchestrateur sort l'exemplaire du répartiteur de charge, et l'ancien continue de
395
431
  servir. `/livez` n'est jamais touché — un schéma en retard n'est pas un processus malade, et le
396
432
  redémarrer ne réparerait rien. La vérification est rejouée toutes les 15 secondes : dès que le
@@ -454,7 +490,7 @@ qui la prend. Un outil qui prétendrait la préparer tout seul inviterait à ne
454
490
  Ce que l'outil fait à la place, et qui vaut mieux :
455
491
 
456
492
  - **il refuse d'appliquer sans que vous sachiez.** Le SQL en attente est examiné avant la moindre
457
- écriture (`scanDestructive()`, `destructive.ts:184`) : une suppression de données est refusée hors
493
+ écriture (`scanDestructive()`, `destructive.ts:282`) : une suppression de données est refusée hors
458
494
  développement, en nommant l'instruction et ce qui disparaît. Il faut `--allow-destructive` pour
459
495
  passer outre, et **au démarrage il n'y a aucun drapeau pour lever le refus** — un exemplaire qui
460
496
  redémarre ne supprime jamais de colonne de lui-même, parce que personne ne regarde à ce
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodefony/drizzle",
3
- "version": "10.0.0-alpha.8",
3
+ "version": "10.0.0-alpha.9",
4
4
  "description": "Moteur SQL de Nodefony sur Drizzle ORM (SQLite, PostgreSQL, MySQL) : dépôts typés, migrations de schéma et adoption d'une base existante",
5
5
  "nodefony": {
6
6
  "storeKind": "durable",
@@ -28,8 +28,8 @@
28
28
  },
29
29
  "scripts": {
30
30
  "start": "node dist/index.js",
31
- "build": "rimraf dist && rolldown -c rolldown.config.ts && tsgo -p tsconfig.declarations.json",
32
- "clean": "rimraf dist",
31
+ "build": "rolldown -c rolldown.config.ts && tsgo -p tsconfig.declarations.json",
32
+ "clean": "node --eval \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
33
33
  "generate:migrations": "tsx scripts/generate-migrations.ts",
34
34
  "check:migrations": "tsx scripts/check-migrations-drift.ts",
35
35
  "dev": "rolldown -c rolldown.config.ts --watch",
@@ -55,35 +55,34 @@
55
55
  "esm"
56
56
  ],
57
57
  "dependencies": {
58
- "drizzle-orm": "0.45.2",
58
+ "drizzle-orm": "0.45.3",
59
59
  "tslib": "2.8.1",
60
60
  "zod": "^4.6.5"
61
61
  },
62
62
  "devDependencies": {
63
- "@nodefony/framework": "^10.0.0-alpha.8",
64
- "@nodefony/http": "^10.0.0-alpha.8",
65
- "@nodefony/orm-core": "^10.0.0-alpha.8",
66
- "@nodefony/security": "^10.0.0-alpha.8",
67
- "@nodefony/user": "^10.0.0-alpha.8",
63
+ "@nodefony/framework": "^10.0.0-alpha.9",
64
+ "@nodefony/http": "^10.0.0-alpha.9",
65
+ "@nodefony/orm-core": "^10.0.0-alpha.9",
66
+ "@nodefony/security": "^10.0.0-alpha.9",
67
+ "@nodefony/user": "^10.0.0-alpha.9",
68
68
  "@types/better-sqlite3": "9.6.0",
69
69
  "@types/node": "26.6.2",
70
70
  "@types/pg": "8.23.1",
71
71
  "better-sqlite3": "13.0.3",
72
- "drizzle-kit": "0.31.10",
72
+ "drizzle-kit": "0.31.11",
73
73
  "mysql2": "3.24.4",
74
- "nodefony": "^10.0.0-alpha.8",
75
- "pg": "8.23.0",
76
- "rimraf": "6.1.3"
74
+ "nodefony": "^10.0.0-alpha.9",
75
+ "pg": "8.23.0"
77
76
  },
78
77
  "peerDependencies": {
79
- "@nodefony/framework": "^10.0.0-alpha.8",
80
- "@nodefony/http": "^10.0.0-alpha.8",
81
- "@nodefony/orm-core": "^10.0.0-alpha.8",
82
- "@nodefony/security": "^10.0.0-alpha.8",
83
- "@nodefony/user": "^10.0.0-alpha.8",
78
+ "@nodefony/framework": "^10.0.0-alpha.9",
79
+ "@nodefony/http": "^10.0.0-alpha.9",
80
+ "@nodefony/orm-core": "^10.0.0-alpha.9",
81
+ "@nodefony/security": "^10.0.0-alpha.9",
82
+ "@nodefony/user": "^10.0.0-alpha.9",
84
83
  "better-sqlite3": "^13.0.0",
85
84
  "mysql2": "^3.24.0",
86
- "nodefony": "^10.0.0-alpha.8",
85
+ "nodefony": "^10.0.0-alpha.9",
87
86
  "pg": "^8.23.0"
88
87
  },
89
88
  "repository": {