@nodefony/drizzle 10.0.0-alpha.7 → 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.
- 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/index.js +11 -7
- package/dist/nodefony/command/migrateShared.js +17 -1
- package/dist/nodefony/command/orm-generate.js +81 -13
- package/dist/nodefony/command/orm-migrate-baseline.js +5 -2
- package/dist/nodefony/command/orm-reset.js +1 -0
- package/dist/nodefony/config/defineModuleConfig.js +21 -5
- package/dist/nodefony/entity/userTable.js +3 -0
- package/dist/nodefony/registerStores.js +2 -26
- package/dist/nodefony/service/DrizzleService.js +19 -7
- package/dist/nodefony/src/DrizzleAuditStore.js +13 -1
- package/dist/nodefony/src/DrizzleIdempotencyStore.js +14 -2
- package/dist/nodefony/src/DrizzleTokenStore.js +13 -1
- package/dist/nodefony/src/DrizzleTotpSecretStore.js +13 -1
- package/dist/nodefony/src/DrizzleWebAuthnCredentialStore.js +13 -1
- package/dist/nodefony/src/DrizzleWebhookStore.js +13 -1
- package/dist/nodefony/src/SessionStorage.js +8 -0
- package/dist/nodefony/src/frameworkConnector.js +29 -0
- package/dist/nodefony/src/memoryDatabase.js +13 -0
- package/dist/nodefony/src/migrator/DrizzleMigrator.js +17 -1
- package/dist/nodefony/src/migrator/appSchema.js +44 -1
- package/dist/nodefony/src/migrator/drivers/sqliteDriver.js +1 -0
- package/dist/nodefony/src/migrator/explain.js +42 -1
- package/dist/nodefony/src/migrator/kit.js +23 -2
- package/dist/nodefony/src/migrator/paths.js +55 -1
- package/dist/nodefony/src/migrator/refusals.js +50 -1
- package/dist/nodefony/src/migrator/resolve.js +126 -6
- package/dist/nodefony/src/orm-core/DrizzleOrm.js +123 -22
- package/dist/nodefony/src/orm-core/ddlPlan.js +95 -0
- package/dist/nodefony/src/orm-core/index.js +2 -1
- package/dist/nodefony/src/orm-core/seedRow.js +65 -0
- package/dist/nodefony/src/safeTarget.js +1 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/nodefony/command/migrateShared.d.ts +10 -0
- package/dist/types/nodefony/command/orm-generate.d.ts +12 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +5 -1
- package/dist/types/nodefony/entity/userTable.d.ts +46 -3
- package/dist/types/nodefony/registerStores.d.ts +4 -3
- package/dist/types/nodefony/src/DrizzleAuditStore.d.ts +6 -0
- package/dist/types/nodefony/src/DrizzleIdempotencyStore.d.ts +8 -1
- package/dist/types/nodefony/src/DrizzleTokenStore.d.ts +6 -0
- package/dist/types/nodefony/src/DrizzleTotpSecretStore.d.ts +6 -0
- package/dist/types/nodefony/src/DrizzleWebAuthnCredentialStore.d.ts +6 -0
- package/dist/types/nodefony/src/DrizzleWebhookStore.d.ts +6 -0
- package/dist/types/nodefony/src/SessionStorage.d.ts +6 -0
- package/dist/types/nodefony/src/frameworkConnector.d.ts +24 -0
- package/dist/types/nodefony/src/memoryDatabase.d.ts +10 -0
- package/dist/types/nodefony/src/migrator/appSchema.d.ts +25 -0
- package/dist/types/nodefony/src/migrator/explain.d.ts +19 -0
- package/dist/types/nodefony/src/migrator/kit.d.ts +15 -0
- package/dist/types/nodefony/src/migrator/paths.d.ts +35 -0
- package/dist/types/nodefony/src/migrator/refusals.d.ts +36 -1
- package/dist/types/nodefony/src/migrator/resolve.d.ts +97 -1
- package/dist/types/nodefony/src/orm-core/DrizzleOrm.d.ts +11 -0
- package/dist/types/nodefony/src/orm-core/ddlPlan.d.ts +96 -0
- package/dist/types/nodefony/src/orm-core/index.d.ts +1 -0
- package/dist/types/nodefony/src/orm-core/seedRow.d.ts +23 -0
- package/docs/index.md +28 -25
- package/docs/migrations.md +69 -4
- package/package.json +19 -20
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ordre de création des tables, et contraintes qu'il faut renoncer à poser.
|
|
3
|
+
*
|
|
4
|
+
* Module **pur** (aucune base, aucun Drizzle) : c'est la seule partie du DDL
|
|
5
|
+
* dérivé qui demande un raisonnement, donc la seule qui mérite d'être éprouvée
|
|
6
|
+
* sans serveur — un cycle entre entités ne se reproduit pas à la demande sur une
|
|
7
|
+
* base réelle.
|
|
8
|
+
*
|
|
9
|
+
* Une clé étrangère se déclare **dans** le `CREATE TABLE` : la table visée doit
|
|
10
|
+
* donc exister AVANT celle qui la désigne. Un ordre quelconque échoue sur la
|
|
11
|
+
* première relation rencontrée, et l'application ne démarre pas.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Une clé étrangère RÉSOLUE en noms SQL — prête à être écrite dans un
|
|
15
|
+
* `CREATE TABLE`, sans plus aucune référence à un objet Drizzle.
|
|
16
|
+
*/
|
|
17
|
+
export interface IResolvedForeignKey {
|
|
18
|
+
/** Nom de la contrainte — explicite, car MySQL l'exige unique pour la base. */
|
|
19
|
+
readonly name: string;
|
|
20
|
+
/** Colonnes porteuses, côté table courante. */
|
|
21
|
+
readonly columns: readonly string[];
|
|
22
|
+
/** Table visée. */
|
|
23
|
+
readonly foreignTable: string;
|
|
24
|
+
/** Colonnes visées, dans le même ordre que {@link columns}. */
|
|
25
|
+
readonly foreignColumns: readonly string[];
|
|
26
|
+
/** Politique d'effacement du parent (`restrict`, `set null`, `cascade`…). */
|
|
27
|
+
readonly onDelete?: string;
|
|
28
|
+
/** Politique de mise à jour de la clé du parent. */
|
|
29
|
+
readonly onUpdate?: string;
|
|
30
|
+
}
|
|
31
|
+
/** Une table à créer, et les tables que ses clés étrangères désignent. */
|
|
32
|
+
export interface ITableDependency {
|
|
33
|
+
/** Nom SQL de la table. */
|
|
34
|
+
readonly name: string;
|
|
35
|
+
/**
|
|
36
|
+
* Tables visées par ses clés étrangères, dans l'ordre de déclaration.
|
|
37
|
+
*
|
|
38
|
+
* Une table peut y figurer plusieurs fois (deux colonnes vers la même cible) ;
|
|
39
|
+
* elle peut aussi s'y désigner elle-même, ce qui ne crée aucune dépendance.
|
|
40
|
+
*/
|
|
41
|
+
readonly references: readonly string[];
|
|
42
|
+
}
|
|
43
|
+
/** Une contrainte qu'on renonce à poser, et la raison qui le commande. */
|
|
44
|
+
export interface IOmittedForeignKey {
|
|
45
|
+
/** Table qui porte la clé étrangère. */
|
|
46
|
+
readonly table: string;
|
|
47
|
+
/** Table visée. */
|
|
48
|
+
readonly target: string;
|
|
49
|
+
/**
|
|
50
|
+
* `cycle` : la cible dépend à son tour de cette table, directement ou non —
|
|
51
|
+
* aucun ordre de création ne satisfait les deux.
|
|
52
|
+
* `absent` : la cible n'appartient pas à ce connecteur, donc à cette base.
|
|
53
|
+
*/
|
|
54
|
+
readonly reason: "cycle" | "absent";
|
|
55
|
+
}
|
|
56
|
+
/** Ordre de création, et ce qu'il a fallu abandonner en chemin. */
|
|
57
|
+
export interface IDdlPlan {
|
|
58
|
+
/** Noms des tables, dans l'ordre où les créer. */
|
|
59
|
+
readonly order: readonly string[];
|
|
60
|
+
/** Clés étrangères non émises — jamais silencieuses, l'appelant les journalise. */
|
|
61
|
+
readonly omitted: readonly IOmittedForeignKey[];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Trie les tables pour que chaque cible naisse avant qui la désigne.
|
|
65
|
+
*
|
|
66
|
+
* Tri topologique (Kahn) sur le graphe des dépendances, avec deux écarts
|
|
67
|
+
* assumés au manuel :
|
|
68
|
+
*
|
|
69
|
+
* - **l'auto-référence ne compte pas** (`parent:ref:Category` dans `Category`) :
|
|
70
|
+
* les trois moteurs acceptent une contrainte qui vise la table en cours de
|
|
71
|
+
* création, dans le même `CREATE TABLE` ;
|
|
72
|
+
* - **un cycle ne fait pas échouer le démarrage**. Il n'y a aucun ordre correct,
|
|
73
|
+
* mais refuser de démarrer pour ça serait hors de proportion : on crée les
|
|
74
|
+
* tables du cycle dans un ordre stable, et on OMET les contraintes qui
|
|
75
|
+
* regardent en arrière. Elles sont rendues à l'appelant pour qu'il les dise —
|
|
76
|
+
* une garantie qui disparaît sans un mot est pire que son absence.
|
|
77
|
+
*
|
|
78
|
+
* À ordre de sortie stable : les tables prêtes sont prises par ordre
|
|
79
|
+
* alphabétique, jamais dans l'ordre d'arrivée. Deux démarrages de la même
|
|
80
|
+
* application produisent donc le même DDL, ce qui rend un journal comparable.
|
|
81
|
+
*
|
|
82
|
+
* @param tables - les tables du connecteur et leurs cibles.
|
|
83
|
+
* @returns l'ordre de création et les contraintes abandonnées.
|
|
84
|
+
*/
|
|
85
|
+
export declare function planTableCreation(tables: readonly ITableDependency[]): IDdlPlan;
|
|
86
|
+
/**
|
|
87
|
+
* Met en mots les contraintes abandonnées — le message que l'appelant journalise.
|
|
88
|
+
*
|
|
89
|
+
* Écrit pour être lu par celui qui découvre le problème dans ses journaux : il
|
|
90
|
+
* nomme les tables, la cause, et ce qu'il reste à faire. Un avertissement qui ne
|
|
91
|
+
* dit pas quoi faire se relit deux fois puis s'ignore.
|
|
92
|
+
*
|
|
93
|
+
* @param omitted - les contraintes non émises.
|
|
94
|
+
* @returns le texte à journaliser, ou `null` s'il n'y a rien à dire.
|
|
95
|
+
*/
|
|
96
|
+
export declare function explainOmittedForeignKeys(omitted: readonly IOmittedForeignKey[]): string | null;
|
|
@@ -9,3 +9,4 @@ export type { DrizzleOrmOptions } from "./DrizzleOrm.js";
|
|
|
9
9
|
export { DrizzleRepository } from "./DrizzleRepository.js";
|
|
10
10
|
export type { DrizzleDb, DrizzleTable, DrizzleColumn, DrizzleResolvedRelation, } from "./DrizzleRepository.js";
|
|
11
11
|
export { DrizzleTransaction } from "./DrizzleTransaction.js";
|
|
12
|
+
export { seedEntityRow } from "./seedRow.js";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { IEntity } from "@nodefony/orm-core";
|
|
2
|
+
import type { DrizzleOrm } from "./DrizzleOrm.js";
|
|
3
|
+
/**
|
|
4
|
+
* Insère une ligne minimale dans la table d'une entité, et rend son identifiant.
|
|
5
|
+
*
|
|
6
|
+
* Destiné aux **tests** : dès qu'une entité porte une relation (`author:ref:Author`),
|
|
7
|
+
* sa colonne est une vraie clé étrangère, et la base REFUSE un identifiant
|
|
8
|
+
* inventé. La ligne parente doit donc exister avant toute insertion — et c'est
|
|
9
|
+
* la seule façon d'éprouver la contrainte au lieu de la contourner (débrancher
|
|
10
|
+
* `foreign_keys` rendrait le test vert sans rien prouver).
|
|
11
|
+
*
|
|
12
|
+
* Ce qui est rempli : les colonnes **obligatoires sans valeur par défaut**, avec
|
|
13
|
+
* une valeur du type que Drizzle déclare. Le reste est laissé au schéma — une
|
|
14
|
+
* clé primaire auto-générée, un horodatage, un défaut applicatif se posent seuls.
|
|
15
|
+
*
|
|
16
|
+
* @param orm - la connexion sur laquelle semer.
|
|
17
|
+
* @param entity - l'entité cible ; son `schema` est la table Drizzle.
|
|
18
|
+
* @returns l'identifiant de la ligne créée, à poser dans la colonne qui la désigne.
|
|
19
|
+
* @throws Si la cible porte elle-même une clé étrangère obligatoire — le semis à
|
|
20
|
+
* un seul étage ne sait pas la satisfaire, et le message le dit plutôt que de
|
|
21
|
+
* laisser remonter un « FOREIGN KEY constraint failed » sans contexte.
|
|
22
|
+
*/
|
|
23
|
+
export declare function seedEntityRow(orm: DrizzleOrm, entity: IEntity): Promise<string | number>;
|
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:
|
|
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:
|
|
348
|
-
l'environnement, gèle. Et `drizzleConfigJsonSchema()` (`defineModuleConfig.ts:
|
|
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:
|
|
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:
|
|
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:
|
|
389
|
-
_scheme_, et pose `filename` ou `url` sur le connecteur primaire. Une URL `mongodb://` est **
|
|
390
|
-
ici** : elle appartient
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
718
|
-
dédup (`begin()`, `DrizzleIdempotencyStore.ts:
|
|
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:
|
|
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
|
|
776
|
-
`
|
|
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:
|
|
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:
|
|
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
|
|
package/docs/migrations.md
CHANGED
|
@@ -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
|
-
|
|
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
|
|
|
@@ -162,6 +190,34 @@ Le dialecte est celui de votre connecteur : vos entités sont du Drizzle **natif
|
|
|
162
190
|
un moteur. C'est ce qui vous laisse toute la puissance du moteur dans une entité — et ce qui fait
|
|
163
191
|
qu'une migration vaut pour lui seul.
|
|
164
192
|
|
|
193
|
+
### En développement : écrire et appliquer d'un seul geste — `--apply`
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
nodefony orm:generate --name ajout_du_titre --apply
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Vous éditez votre entité, vous tapez **une** commande, la colonne est en base. C'est le geste
|
|
200
|
+
quotidien du développement, et il ne vous demande pas de retenir un second verbe.
|
|
201
|
+
|
|
202
|
+
`--apply` n'invente rien : il **enchaîne `orm:migrate`**. Toutes ses gardes valent donc à
|
|
203
|
+
l'identique — une migration qui supprime des données reste refusée sans `--allow-destructive`, le
|
|
204
|
+
verrou est pris, l'historique est écrit. En sortie machine (`--json`), un seul document est produit,
|
|
205
|
+
celui de l'application : c'est lui qui porte le verdict.
|
|
206
|
+
|
|
207
|
+
> **Le drapeau n'existe qu'en développement**, et aucun autre drapeau ne le déverrouille. Écrire une
|
|
208
|
+
> migration et l'appliquer dans le même souffle suppose qu'on relit le SQL produit entre les deux —
|
|
209
|
+
> ce que personne ne fait sur un serveur. Ailleurs, la migration a été écrite, relue et versionnée
|
|
210
|
+
> ailleurs : c'est `orm:migrate` qui l'applique, et lui seul.
|
|
211
|
+
|
|
212
|
+
> **Et le mode de schéma bascule tout seul.** Dès que votre application versionne sa première
|
|
213
|
+
> migration, le démarrage cesse de dériver les tables depuis le code (`ddl: "auto"`) et applique vos
|
|
214
|
+
> migrations (`ddl: "migrate"`) — même en développement. Deux fabricants du même schéma ne
|
|
215
|
+
> s'accordent pas : le DDL dérivé pose une unicité **collée à la colonne** (`content text NOT NULL
|
|
216
|
+
UNIQUE`), le générateur de migrations raisonne sur un **index nommé** (`messages_content_unique`),
|
|
217
|
+
> et la première migration qui tente de retirer la seconde échoue sur un objet qui n'a jamais existé.
|
|
218
|
+
> Un `ddl` écrit dans votre configuration gagne toujours ; et sous `NODE_ENV=test`, rien ne change
|
|
219
|
+
> (une suite lance un exemplaire par worker, et ses bases partent vides).
|
|
220
|
+
|
|
165
221
|
### Ce que la commande refuse, et pourquoi c'est une bonne nouvelle
|
|
166
222
|
|
|
167
223
|
**Une migration est immuable dès qu'une base l'a reçue.** Une migration à laquelle il manque une
|
|
@@ -196,12 +252,21 @@ dépendances de développement (`--omit=dev`, une image de production). Le refus
|
|
|
196
252
|
geste ; il ne parle pas de votre base, qui n'y est pour rien — appliquer des migrations, lui, ne
|
|
197
253
|
réclame aucun outil tiers.
|
|
198
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
|
+
|
|
199
263
|
### Une entité qui pointe vers une table du framework
|
|
200
264
|
|
|
201
265
|
Déclarer une référence vers une table du framework (`session`, `audit_event`…) est légitime : ce
|
|
202
266
|
qui est refusé, c'est de **ré-exporter** cette table depuis vos fichiers. Les tables du framework
|
|
203
267
|
sont exclues du plan de votre application — elles ont leurs propres migrations, appliquées avant les
|
|
204
|
-
vôtres.
|
|
268
|
+
vôtres. La contrainte, elle, est bien écrite dans votre migration : elle désigne une table que le
|
|
269
|
+
framework a déjà créée.
|
|
205
270
|
|
|
206
271
|
> [!IMPORTANT]
|
|
207
272
|
> **`User` n'en fait PAS partie.** L'identité est du domaine : la table `User` appartient à votre
|
|
@@ -361,7 +426,7 @@ Trois propriétés en découlent, et elles valent d'être nommées :
|
|
|
361
426
|
- **Rien n'est appliqué deux fois.** L'historique (`nodefony_migrations`, `types.ts:23`) est écrit
|
|
362
427
|
dans la même transaction que le DDL, là où le moteur le permet.
|
|
363
428
|
- **Un pod en retard ne reçoit pas de trafic.** En `none` comme en `migrate`, l'état du schéma est
|
|
364
|
-
publié à la sonde de disponibilité (`#publishReadiness()`, `DrizzleService.ts:
|
|
429
|
+
publié à la sonde de disponibilité (`#publishReadiness()`, `DrizzleService.ts:527`) : `/readyz`
|
|
365
430
|
répond 503, l'orchestrateur sort l'exemplaire du répartiteur de charge, et l'ancien continue de
|
|
366
431
|
servir. `/livez` n'est jamais touché — un schéma en retard n'est pas un processus malade, et le
|
|
367
432
|
redémarrer ne réparerait rien. La vérification est rejouée toutes les 15 secondes : dès que le
|
|
@@ -425,7 +490,7 @@ qui la prend. Un outil qui prétendrait la préparer tout seul inviterait à ne
|
|
|
425
490
|
Ce que l'outil fait à la place, et qui vaut mieux :
|
|
426
491
|
|
|
427
492
|
- **il refuse d'appliquer sans que vous sachiez.** Le SQL en attente est examiné avant la moindre
|
|
428
|
-
écriture (`scanDestructive()`, `destructive.ts:
|
|
493
|
+
écriture (`scanDestructive()`, `destructive.ts:282`) : une suppression de données est refusée hors
|
|
429
494
|
développement, en nommant l'instruction et ce qui disparaît. Il faut `--allow-destructive` pour
|
|
430
495
|
passer outre, et **au démarrage il n'y a aucun drapeau pour lever le refus** — un exemplaire qui
|
|
431
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.
|
|
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": "
|
|
32
|
-
"clean": "
|
|
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.
|
|
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.
|
|
64
|
-
"@nodefony/http": "^10.0.0-alpha.
|
|
65
|
-
"@nodefony/orm-core": "^10.0.0-alpha.
|
|
66
|
-
"@nodefony/security": "^10.0.0-alpha.
|
|
67
|
-
"@nodefony/user": "^10.0.0-alpha.
|
|
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
|
-
"@types/node": "26.6.
|
|
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.
|
|
72
|
+
"drizzle-kit": "0.31.11",
|
|
73
73
|
"mysql2": "3.24.4",
|
|
74
|
-
"nodefony": "^10.0.0-alpha.
|
|
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.
|
|
80
|
-
"@nodefony/http": "^10.0.0-alpha.
|
|
81
|
-
"@nodefony/orm-core": "^10.0.0-alpha.
|
|
82
|
-
"@nodefony/security": "^10.0.0-alpha.
|
|
83
|
-
"@nodefony/user": "^10.0.0-alpha.
|
|
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.
|
|
85
|
+
"nodefony": "^10.0.0-alpha.9",
|
|
87
86
|
"pg": "^8.23.0"
|
|
88
87
|
},
|
|
89
88
|
"repository": {
|