@nodefony/drizzle 10.0.0-alpha.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.
Files changed (143) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +162 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/index.js +105 -0
  6. package/dist/nodefony/command/migrateShared.js +247 -0
  7. package/dist/nodefony/command/orm-generate.js +356 -0
  8. package/dist/nodefony/command/orm-migrate-baseline.js +208 -0
  9. package/dist/nodefony/command/orm-migrate-repair.js +114 -0
  10. package/dist/nodefony/command/orm-migrate-status.js +67 -0
  11. package/dist/nodefony/command/orm-migrate.js +141 -0
  12. package/dist/nodefony/command/orm-reset.js +166 -0
  13. package/dist/nodefony/config/config.js +107 -0
  14. package/dist/nodefony/config/defineModuleConfig.js +63 -0
  15. package/dist/nodefony/entity/auditEventEntity.js +93 -0
  16. package/dist/nodefony/entity/colKit.js +260 -0
  17. package/dist/nodefony/entity/idempotencyEntity.js +74 -0
  18. package/dist/nodefony/entity/sessionEntity.js +75 -0
  19. package/dist/nodefony/entity/tokenEntity.js +198 -0
  20. package/dist/nodefony/entity/totpSecretEntity.js +98 -0
  21. package/dist/nodefony/entity/userTable.js +141 -0
  22. package/dist/nodefony/entity/webAuthnCredentialEntity.js +114 -0
  23. package/dist/nodefony/entity/webhookEndpointEntity.js +106 -0
  24. package/dist/nodefony/interfaces/IDrizzleConfig.js +1 -0
  25. package/dist/nodefony/interfaces/index.js +1 -0
  26. package/dist/nodefony/migrations-schema/mysql.js +48 -0
  27. package/dist/nodefony/migrations-schema/postgres.js +48 -0
  28. package/dist/nodefony/migrations-schema/sqlite.js +48 -0
  29. package/dist/nodefony/registerStores.js +218 -0
  30. package/dist/nodefony/service/DrizzleService.js +282 -0
  31. package/dist/nodefony/src/DrizzleAuditStore.js +203 -0
  32. package/dist/nodefony/src/DrizzleIdempotencyStore.js +278 -0
  33. package/dist/nodefony/src/DrizzleTokenStore.js +244 -0
  34. package/dist/nodefony/src/DrizzleTotpSecretStore.js +151 -0
  35. package/dist/nodefony/src/DrizzleUserRepository.js +217 -0
  36. package/dist/nodefony/src/DrizzleWebAuthnCredentialStore.js +159 -0
  37. package/dist/nodefony/src/DrizzleWebhookStore.js +169 -0
  38. package/dist/nodefony/src/SessionStorage.js +259 -0
  39. package/dist/nodefony/src/connectorTarget.js +59 -0
  40. package/dist/nodefony/src/likeSql.js +50 -0
  41. package/dist/nodefony/src/migrator/DrizzleMigrator.js +775 -0
  42. package/dist/nodefony/src/migrator/adopt.js +553 -0
  43. package/dist/nodefony/src/migrator/appSchema.js +414 -0
  44. package/dist/nodefony/src/migrator/catalog.js +76 -0
  45. package/dist/nodefony/src/migrator/destructive.js +213 -0
  46. package/dist/nodefony/src/migrator/divergence.js +84 -0
  47. package/dist/nodefony/src/migrator/drivers/index.js +39 -0
  48. package/dist/nodefony/src/migrator/drivers/mysqlDriver.js +147 -0
  49. package/dist/nodefony/src/migrator/drivers/postgresDriver.js +151 -0
  50. package/dist/nodefony/src/migrator/drivers/sqliteDriver.js +121 -0
  51. package/dist/nodefony/src/migrator/explain.js +565 -0
  52. package/dist/nodefony/src/migrator/hash.js +47 -0
  53. package/dist/nodefony/src/migrator/history.js +219 -0
  54. package/dist/nodefony/src/migrator/index.js +16 -0
  55. package/dist/nodefony/src/migrator/kit.js +296 -0
  56. package/dist/nodefony/src/migrator/name.js +68 -0
  57. package/dist/nodefony/src/migrator/paths.js +88 -0
  58. package/dist/nodefony/src/migrator/refusals.js +143 -0
  59. package/dist/nodefony/src/migrator/resolve.js +281 -0
  60. package/dist/nodefony/src/migrator/schemaDiff.js +86 -0
  61. package/dist/nodefony/src/migrator/sources.js +419 -0
  62. package/dist/nodefony/src/migrator/status.js +231 -0
  63. package/dist/nodefony/src/migrator/types.js +91 -0
  64. package/dist/nodefony/src/orm-core/DrizzleOrm.js +1154 -0
  65. package/dist/nodefony/src/orm-core/DrizzleRepository.js +610 -0
  66. package/dist/nodefony/src/orm-core/DrizzleTransaction.js +106 -0
  67. package/dist/nodefony/src/orm-core/index.js +4 -0
  68. package/dist/nodefony/src/queryKit.js +318 -0
  69. package/dist/nodefony/src/safeTarget.js +55 -0
  70. package/dist/types/index.d.ts +76 -0
  71. package/dist/types/nodefony/command/migrateShared.d.ts +137 -0
  72. package/dist/types/nodefony/command/orm-generate.d.ts +53 -0
  73. package/dist/types/nodefony/command/orm-migrate-baseline.d.ts +87 -0
  74. package/dist/types/nodefony/command/orm-migrate-repair.d.ts +66 -0
  75. package/dist/types/nodefony/command/orm-migrate-status.d.ts +38 -0
  76. package/dist/types/nodefony/command/orm-migrate.d.ts +65 -0
  77. package/dist/types/nodefony/command/orm-reset.d.ts +55 -0
  78. package/dist/types/nodefony/config/config.d.ts +110 -0
  79. package/dist/types/nodefony/config/defineModuleConfig.d.ts +24 -0
  80. package/dist/types/nodefony/entity/auditEventEntity.d.ts +56 -0
  81. package/dist/types/nodefony/entity/colKit.d.ts +130 -0
  82. package/dist/types/nodefony/entity/idempotencyEntity.d.ts +52 -0
  83. package/dist/types/nodefony/entity/sessionEntity.d.ts +50 -0
  84. package/dist/types/nodefony/entity/tokenEntity.d.ts +53 -0
  85. package/dist/types/nodefony/entity/totpSecretEntity.d.ts +61 -0
  86. package/dist/types/nodefony/entity/userTable.d.ts +65 -0
  87. package/dist/types/nodefony/entity/webAuthnCredentialEntity.d.ts +57 -0
  88. package/dist/types/nodefony/entity/webhookEndpointEntity.d.ts +58 -0
  89. package/dist/types/nodefony/interfaces/IDrizzleConfig.d.ts +17 -0
  90. package/dist/types/nodefony/interfaces/index.d.ts +1 -0
  91. package/dist/types/nodefony/migrations-schema/mysql.d.ts +9 -0
  92. package/dist/types/nodefony/migrations-schema/postgres.d.ts +9 -0
  93. package/dist/types/nodefony/migrations-schema/sqlite.d.ts +9 -0
  94. package/dist/types/nodefony/registerStores.d.ts +52 -0
  95. package/dist/types/nodefony/service/DrizzleService.d.ts +27 -0
  96. package/dist/types/nodefony/src/DrizzleAuditStore.d.ts +65 -0
  97. package/dist/types/nodefony/src/DrizzleIdempotencyStore.d.ts +129 -0
  98. package/dist/types/nodefony/src/DrizzleTokenStore.d.ts +146 -0
  99. package/dist/types/nodefony/src/DrizzleTotpSecretStore.d.ts +60 -0
  100. package/dist/types/nodefony/src/DrizzleUserRepository.d.ts +67 -0
  101. package/dist/types/nodefony/src/DrizzleWebAuthnCredentialStore.d.ts +62 -0
  102. package/dist/types/nodefony/src/DrizzleWebhookStore.d.ts +79 -0
  103. package/dist/types/nodefony/src/SessionStorage.d.ts +72 -0
  104. package/dist/types/nodefony/src/connectorTarget.d.ts +48 -0
  105. package/dist/types/nodefony/src/likeSql.d.ts +29 -0
  106. package/dist/types/nodefony/src/migrator/DrizzleMigrator.d.ts +94 -0
  107. package/dist/types/nodefony/src/migrator/adopt.d.ts +280 -0
  108. package/dist/types/nodefony/src/migrator/appSchema.d.ts +223 -0
  109. package/dist/types/nodefony/src/migrator/catalog.d.ts +100 -0
  110. package/dist/types/nodefony/src/migrator/destructive.d.ts +123 -0
  111. package/dist/types/nodefony/src/migrator/divergence.d.ts +61 -0
  112. package/dist/types/nodefony/src/migrator/drivers/index.d.ts +26 -0
  113. package/dist/types/nodefony/src/migrator/drivers/mysqlDriver.d.ts +83 -0
  114. package/dist/types/nodefony/src/migrator/drivers/postgresDriver.d.ts +81 -0
  115. package/dist/types/nodefony/src/migrator/drivers/sqliteDriver.d.ts +53 -0
  116. package/dist/types/nodefony/src/migrator/explain.d.ts +424 -0
  117. package/dist/types/nodefony/src/migrator/hash.d.ts +39 -0
  118. package/dist/types/nodefony/src/migrator/history.d.ts +139 -0
  119. package/dist/types/nodefony/src/migrator/index.d.ts +20 -0
  120. package/dist/types/nodefony/src/migrator/kit.d.ts +141 -0
  121. package/dist/types/nodefony/src/migrator/name.d.ts +52 -0
  122. package/dist/types/nodefony/src/migrator/paths.d.ts +54 -0
  123. package/dist/types/nodefony/src/migrator/refusals.d.ts +170 -0
  124. package/dist/types/nodefony/src/migrator/resolve.d.ts +201 -0
  125. package/dist/types/nodefony/src/migrator/schemaDiff.d.ts +112 -0
  126. package/dist/types/nodefony/src/migrator/sources.d.ts +119 -0
  127. package/dist/types/nodefony/src/migrator/status.d.ts +132 -0
  128. package/dist/types/nodefony/src/migrator/types.d.ts +273 -0
  129. package/dist/types/nodefony/src/orm-core/DrizzleOrm.d.ts +241 -0
  130. package/dist/types/nodefony/src/orm-core/DrizzleRepository.d.ts +98 -0
  131. package/dist/types/nodefony/src/orm-core/DrizzleTransaction.d.ts +71 -0
  132. package/dist/types/nodefony/src/orm-core/index.d.ts +11 -0
  133. package/dist/types/nodefony/src/queryKit.d.ts +136 -0
  134. package/dist/types/nodefony/src/safeTarget.d.ts +42 -0
  135. package/docs/index.md +954 -0
  136. package/docs/migrations.md +691 -0
  137. package/migrations/mysql/0000_framework_init.sql +137 -0
  138. package/migrations/mysql/meta/_journal.json +13 -0
  139. package/migrations/postgres/0000_framework_init.sql +128 -0
  140. package/migrations/postgres/meta/_journal.json +13 -0
  141. package/migrations/sqlite/0000_framework_init.sql +127 -0
  142. package/migrations/sqlite/meta/_journal.json +13 -0
  143. package/package.json +126 -0
@@ -0,0 +1,218 @@
1
+ import { DrizzleOrm } from "./src/orm-core/DrizzleOrm.js";
2
+ import "./src/orm-core/index.js";
3
+ import { TOKEN_ENTITY_NAMES, registerTokenEntities } from "./entity/tokenEntity.js";
4
+ import { AUDIT_ENTITY_NAMES, registerAuditEntities } from "./entity/auditEventEntity.js";
5
+ import { WEBAUTHN_CREDENTIAL_ENTITY, registerWebAuthnCredentialEntity } from "./entity/webAuthnCredentialEntity.js";
6
+ import { TOTP_SECRET_ENTITY, registerTotpSecretEntity } from "./entity/totpSecretEntity.js";
7
+ import { WEBHOOK_ENDPOINT_ENTITY, registerWebhookEndpointEntity } from "./entity/webhookEndpointEntity.js";
8
+ import { IDEMPOTENCY_ENTITY_NAME, createIdempotencyTable, registerIdempotencyEntities } from "./entity/idempotencyEntity.js";
9
+ import { SESSION_CONNECTOR, SESSION_ENTITY_NAME, registerSessionEntity } from "./entity/sessionEntity.js";
10
+ import { registerUserEntity, userTableColumns } from "./entity/userTable.js";
11
+ import { DrizzleTokenStore } from "./src/DrizzleTokenStore.js";
12
+ import { DrizzleAuditStore } from "./src/DrizzleAuditStore.js";
13
+ import { DrizzleWebAuthnCredentialStore } from "./src/DrizzleWebAuthnCredentialStore.js";
14
+ import { DrizzleTotpSecretStore } from "./src/DrizzleTotpSecretStore.js";
15
+ import { DrizzleWebhookStore } from "./src/DrizzleWebhookStore.js";
16
+ import { DrizzleIdempotencyStore } from "./src/DrizzleIdempotencyStore.js";
17
+ import { entityRegistry, ormRegistry } from "@nodefony/orm-core";
18
+ import { assertUserContract } from "@nodefony/user";
19
+ import { getAuditStoreFactory, getTokenStoreFactory, getTotpStoreFactory, getWebAuthnStoreFactory, getWebhookStoreFactory, registerAuditStore, registerTokenStore, registerTotpStore, registerWebAuthnStore, registerWebhookStore } from "@nodefony/security";
20
+ import { getIdempotencyStoreFactory, registerIdempotencyStore } from "@nodefony/framework";
21
+ //#region nodefony/registerStores.ts
22
+ /**
23
+ * AUTO-ENREGISTREMENT des backends framework portés par Drizzle — « charger le
24
+ * module = ses backends deviennent sélectionnables par simple nom ».
25
+ *
26
+ * Appelé par `Drizzle.onKernelRegister` (config validée → dialecte du connecteur
27
+ * `default` connu, AVANT le connect de `onBoot` → les tables sont créées au
28
+ * connect). Remplace l'« approche B » historique où chaque application devait
29
+ * câbler `registerXStore(...)` + `registerXEntities(...)` à la main.
30
+ *
31
+ * Deux garde-fous préservent la main de l'application (customisation) :
32
+ * - **entité** : `entityRegistry.has(name, orm)` → une entité déjà enregistrée
33
+ * par l'app est respectée (jamais de doublon-throw) ;
34
+ * - **fabrique** : `getXStoreFactory("drizzle")` → une fabrique déjà posée par
35
+ * l'app garde la main (le registre est premier-arrivé-premier-servi ici).
36
+ *
37
+ * Une brique dont l'entité n'est PAS portée sur le dialecte configuré n'est NI
38
+ * déclarée NI fabricable : les registres reflètent le RÉEL (`listXStores()` ne
39
+ * promet jamais un backend qui échouerait), et la sélectionner échoue franc au
40
+ * boot (« store inconnu ») — jamais de table fantôme ni d'erreur SQL différée.
41
+ */
42
+ /**
43
+ * Connecteur conventionnel qui héberge le schéma framework (même convention que
44
+ * `SESSION_CONNECTOR` : `"default"` pour Drizzle, `"nodefony"` pour Mongoose).
45
+ */
46
+ const FRAMEWORK_CONNECTOR = "default";
47
+ /** Portage par entité (chantier multi-dialecte — Ph.2.1 allume les cases). */
48
+ const ALL_DIALECTS = [
49
+ "sqlite",
50
+ "postgres",
51
+ "mysql"
52
+ ];
53
+ const IDEMPOTENCY_PORTED = ALL_DIALECTS;
54
+ const SESSION_PORTED = ALL_DIALECTS;
55
+ const TOKEN_PORTED = ALL_DIALECTS;
56
+ const WEBAUTHN_PORTED = ALL_DIALECTS;
57
+ const TOTP_PORTED = ALL_DIALECTS;
58
+ const USER_PORTED = ALL_DIALECTS;
59
+ const AUDIT_PORTED = ALL_DIALECTS;
60
+ const WEBHOOK_PORTED = ALL_DIALECTS;
61
+ /**
62
+ * Les entités que le REPLI a posées, par connecteur — `<connecteur>:<entité>`.
63
+ *
64
+ * Elle existe pour une question qu'aucun autre objet ne sait trancher : cette
65
+ * entité vient-elle de l'APPLICATION, ou le framework l'a-t-il posée faute de
66
+ * mieux ? Le registre d'entités, lui, ne retient pas qui a écrit. Or la réponse
67
+ * décide d'un refus de démarrage : une entité de repli n'est dans AUCUNE chaîne
68
+ * de migration, donc sa table n'existe nulle part hors développement.
69
+ */
70
+ const fallbackEntities = /* @__PURE__ */ new Set();
71
+ /** Clé de {@link fallbackEntities} — une entité vit par connecteur. */
72
+ function fallbackKey(entityName, connector) {
73
+ return `${connector}:${entityName}`;
74
+ }
75
+ /**
76
+ * Cette entité a-t-elle été posée par le repli du framework ?
77
+ *
78
+ * @param entityName - nom de l'entité (`"User"`…).
79
+ * @param connector - connecteur porteur.
80
+ * @returns `true` si le framework l'a enregistrée lui-même, faute d'entité d'app.
81
+ */
82
+ function isFrameworkFallbackEntity(entityName, connector = FRAMEWORK_CONNECTOR) {
83
+ return fallbackEntities.has(fallbackKey(entityName, connector));
84
+ }
85
+ /**
86
+ * Résout l'ORM `default` CONNECTÉ pour une fabrique de store — échec FRANC avec
87
+ * la cause exacte (module absent / ordre de boot / dialecte) : principe « pas de
88
+ * dégradation silencieuse ».
89
+ */
90
+ function resolveConnectedOrm(store, dialect) {
91
+ let orm;
92
+ try {
93
+ orm = ormRegistry.get(FRAMEWORK_CONNECTOR);
94
+ } catch {
95
+ throw new Error(`${store} : ORM "${FRAMEWORK_CONNECTOR}" introuvable — charger @nodefony/drizzle AVANT @nodefony/security dans le manifeste "modules".`);
96
+ }
97
+ if (!(orm instanceof DrizzleOrm)) throw new Error(`${store} : l'ORM "${FRAMEWORK_CONNECTOR}" n'est pas un DrizzleOrm (connecteur homonyme d'un autre driver ?).`);
98
+ if (!orm.isConnected()) throw new Error(`${store} : ORM "${FRAMEWORK_CONNECTOR}" non connecté au montage du store (ordre de boot).`);
99
+ if (orm.dialect !== dialect) throw new Error(`${store} : ORM "${FRAMEWORK_CONNECTOR}" en "${orm.dialect}" mais le schéma framework a été déclaré en "${dialect}" (incohérence de config).`);
100
+ return orm;
101
+ }
102
+ /**
103
+ * Déclare les entités framework portées sur `dialect` (connecteur `default`) et
104
+ * enregistre leurs fabriques de stores dans les registres de `@nodefony/security`
105
+ * et `@nodefony/framework`. Idempotent (guards) — rejouable sans effet.
106
+ *
107
+ * @param dialect - dialecte du connecteur `default` (config validée du module)
108
+ * @returns bilan à logger (registered / appOwned / unported)
109
+ */
110
+ function registerDrizzleFrameworkStores(dialect) {
111
+ const report = {
112
+ registered: [],
113
+ appOwned: [],
114
+ unported: []
115
+ };
116
+ fallbackEntities.clear();
117
+ const wire = (entityName, ported, registerEntity, registerFactory) => {
118
+ if (!ported.includes(dialect)) {
119
+ report.unported.push(entityName);
120
+ return;
121
+ }
122
+ if (entityRegistry.has(entityName, "default")) report.appOwned.push(entityName);
123
+ else {
124
+ registerEntity();
125
+ report.registered.push(entityName);
126
+ fallbackEntities.add(fallbackKey(entityName, FRAMEWORK_CONNECTOR));
127
+ }
128
+ registerFactory();
129
+ };
130
+ wire(SESSION_ENTITY_NAME, SESSION_PORTED, () => registerSessionEntity(SESSION_CONNECTOR, dialect), () => {});
131
+ wire("User", USER_PORTED, () => registerUserEntity(FRAMEWORK_CONNECTOR, dialect), () => {});
132
+ wire(TOKEN_ENTITY_NAMES.records, TOKEN_PORTED, () => registerTokenEntities(FRAMEWORK_CONNECTOR, dialect), () => {
133
+ if (getTokenStoreFactory("drizzle")) return;
134
+ registerTokenStore("drizzle", (ctx) => {
135
+ const orm = resolveConnectedOrm(`tokenStore "drizzle"`, dialect);
136
+ const days = ctx?.config?.tokenStore?.retentionRevokedDays;
137
+ return DrizzleTokenStore.from(orm, void 0, typeof days === "number" ? days * 864e5 : void 0);
138
+ });
139
+ });
140
+ wire(AUDIT_ENTITY_NAMES.events, AUDIT_PORTED, () => registerAuditEntities(FRAMEWORK_CONNECTOR, dialect), () => {
141
+ if (getAuditStoreFactory("drizzle")) return;
142
+ registerAuditStore("drizzle", (ctx) => {
143
+ const orm = resolveConnectedOrm(`audit.store "drizzle"`, dialect);
144
+ const days = ctx?.config?.audit?.retentionDays;
145
+ return DrizzleAuditStore.from(orm, void 0, typeof days === "number" ? days * 864e5 : void 0);
146
+ });
147
+ });
148
+ wire(WEBAUTHN_CREDENTIAL_ENTITY, WEBAUTHN_PORTED, () => registerWebAuthnCredentialEntity(FRAMEWORK_CONNECTOR, dialect), () => {
149
+ if (getWebAuthnStoreFactory("drizzle")) return;
150
+ registerWebAuthnStore("drizzle", () => DrizzleWebAuthnCredentialStore.from(resolveConnectedOrm(`passkeys.store "drizzle"`, dialect)));
151
+ });
152
+ wire(TOTP_SECRET_ENTITY, TOTP_PORTED, () => registerTotpSecretEntity(FRAMEWORK_CONNECTOR, dialect), () => {
153
+ if (getTotpStoreFactory("drizzle")) return;
154
+ registerTotpStore("drizzle", () => DrizzleTotpSecretStore.from(resolveConnectedOrm(`totp.store "drizzle"`, dialect)));
155
+ });
156
+ wire(WEBHOOK_ENDPOINT_ENTITY, WEBHOOK_PORTED, () => registerWebhookEndpointEntity(FRAMEWORK_CONNECTOR, dialect), () => {
157
+ if (getWebhookStoreFactory("drizzle")) return;
158
+ registerWebhookStore("drizzle", () => DrizzleWebhookStore.from(resolveConnectedOrm(`webhooks.store "drizzle"`, dialect)));
159
+ });
160
+ wire(IDEMPOTENCY_ENTITY_NAME, IDEMPOTENCY_PORTED, () => registerIdempotencyEntities(FRAMEWORK_CONNECTOR, dialect), () => {
161
+ if (getIdempotencyStoreFactory("drizzle")) return;
162
+ registerIdempotencyStore("drizzle", () => {
163
+ const resolveDb = () => {
164
+ let orm;
165
+ try {
166
+ orm = ormRegistry.get(FRAMEWORK_CONNECTOR);
167
+ } catch {
168
+ return null;
169
+ }
170
+ if (!(orm instanceof DrizzleOrm) || !orm.isConnected()) return null;
171
+ return orm.getNativeConnection();
172
+ };
173
+ return new DrizzleIdempotencyStore(resolveDb, void 0, void 0, void 0, createIdempotencyTable(dialect), () => {
174
+ try {
175
+ const orm = ormRegistry.get(FRAMEWORK_CONNECTOR);
176
+ return orm instanceof DrizzleOrm ? orm.location : void 0;
177
+ } catch {
178
+ return;
179
+ }
180
+ });
181
+ });
182
+ });
183
+ if (report.appOwned.includes("User")) assertAppUserEntityHonoursContract(dialect);
184
+ return report;
185
+ }
186
+ /**
187
+ * Confronte l'entité `User` déclarée par l'application au contrat de colonnes.
188
+ *
189
+ * Appelé au moment où l'auto-register CONSTATE que l'application possède son
190
+ * entité — donc au démarrage, avant qu'un seul lecteur n'ait tenté quoi que ce
191
+ * soit. Le message vient de {@link assertUserContract} : une règle, un texte,
192
+ * partagés avec l'adaptateur document.
193
+ *
194
+ * Le silence en cas de schéma illisible est délibéré : une entité dont la table
195
+ * ne se laisse pas inspecter (driver tiers, forme inattendue, table déclarée
196
+ * dans la grammaire d'un AUTRE dialecte — {@link userTableColumns} lève alors)
197
+ * ne prouve PAS qu'une colonne manque. Refuser sur cette base transformerait un
198
+ * contrôle en panne de démarrage pour une application parfaitement correcte —
199
+ * et un refus faux apprend surtout à passer outre les refus.
200
+ *
201
+ * @param dialect - dialecte du connecteur framework, qui décide de la grammaire
202
+ * de lecture de la table.
203
+ * @throws Error si une colonne du contrat manque à l'entité de l'application.
204
+ */
205
+ function assertAppUserEntityHonoursContract(dialect) {
206
+ let present;
207
+ try {
208
+ const entity = entityRegistry.get("User", FRAMEWORK_CONNECTOR);
209
+ const columns = userTableColumns(entity.schema, dialect);
210
+ if (columns.size === 0) return;
211
+ present = columns.keys();
212
+ } catch {
213
+ return;
214
+ }
215
+ assertUserContract(present, `L'entité « User » de cette application (connecteur « ${FRAMEWORK_CONNECTOR} », ${dialect})`);
216
+ }
217
+ //#endregion
218
+ export { FRAMEWORK_CONNECTOR, isFrameworkFallbackEntity, registerDrizzleFrameworkStores };
@@ -0,0 +1,282 @@
1
+ import { buildReport, isAheadOnly, meaningOf } from "../src/migrator/explain.js";
2
+ import { defaultConnectorFilename } from "../src/connectorTarget.js";
3
+ import { defaultMigrationSources } from "../src/migrator/paths.js";
4
+ import { DrizzleMigrator } from "../src/migrator/DrizzleMigrator.js";
5
+ import { appMigrationsDir, readMigrationEnv, resetAllowed, resolveCheckMode, resolveDdlMode } from "../src/migrator/resolve.js";
6
+ import { describeDivergence } from "../src/migrator/divergence.js";
7
+ import { DrizzleOrm } from "../src/orm-core/DrizzleOrm.js";
8
+ import "../src/orm-core/index.js";
9
+ import { dataLoss, renderDestructive, scanDestructive, summarizeDestructive } from "../src/migrator/destructive.js";
10
+ import { BootConfigurationError, Service } from "nodefony";
11
+ import { queryFlowMonitor, resolveOrmFlowEnabled } from "@nodefony/orm-core";
12
+ import fs from "node:fs";
13
+ import path from "node:path";
14
+ //#region nodefony/service/DrizzleService.ts
15
+ const serviceName = "drizzle";
16
+ /**
17
+ * Période de re-vérification du schéma quand la mise en service est retenue.
18
+ *
19
+ * C'est ce minuteur, et lui seul, qui donne au processus sa propriété la plus
20
+ * précieuse : **il redevient disponible TOUT SEUL** dès que les migrations sont
21
+ * appliquées par ailleurs — aucun redéploiement, aucune intervention. Sans lui,
22
+ * un exemplaire retenu le resterait jusqu'à ce qu'on pense à le relancer.
23
+ *
24
+ * Quinze secondes : assez court pour qu'un déploiement ne perde pas de temps,
25
+ * assez long pour être invisible (une requête sur une table minuscule). Le
26
+ * minuteur n'existe QUE lorsque le schéma n'est pas dérivé du code — en
27
+ * développement, il n'est jamais créé.
28
+ */
29
+ const READINESS_POLL_MS = 15e3;
30
+ /**
31
+ * Ce que chaque mode veut dire, en clair, dans le journal de démarrage.
32
+ *
33
+ * Une ligne de journal qui dit `ddl: none` n'apprend rien à qui n'a pas lu la
34
+ * documentation — et personne ne la lit AVANT l'incident. La phrase, elle,
35
+ * suffit.
36
+ */
37
+ const DDL_EXPLAINED = {
38
+ auto: "(dérivé du code au démarrage — développement ; les colonnes manquantes qui acceptent le vide sont ajoutées)",
39
+ migrate: "(migrations appliquées au démarrage, sous verrou — un seul exemplaire assumé)",
40
+ none: "(personne ne touche au schéma ici — un travail externe lance « nodefony orm:migrate »)"
41
+ };
42
+ /** Nom du contributeur de disponibilité, tel qu'il apparaît au diagnostic. */
43
+ function readinessName(connector) {
44
+ return `drizzle:schema:${connector}`;
45
+ }
46
+ /** URL de connexion sans credentials — un log de boot ne porte jamais de secret. */
47
+ function redactUrl(url) {
48
+ if (!url) return "no url";
49
+ try {
50
+ const parsed = new URL(url);
51
+ if (parsed.password) parsed.password = "***";
52
+ return parsed.toString();
53
+ } catch {
54
+ return "invalid url";
55
+ }
56
+ }
57
+ /**
58
+ * Service bootable du module `@nodefony/drizzle`.
59
+ *
60
+ * Au boot du kernel (`onBoot`), instancie un {@link DrizzleOrm} (adapter
61
+ * orm-core) **par connecteur** déclaré dans la config et le connecte ; chaque
62
+ * ORM s'auto-enregistre dans le `ormRegistry` (accessible ensuite via DI ou
63
+ * `OrmRegistry.get(name)`). Ferme proprement les connexions à `onTerminate`.
64
+ *
65
+ * C'est le point d'entrée « ORM par défaut » de l'app : il rend Drizzle utilisable
66
+ * sans logique métier — les entités (`@entity`) ciblant un connecteur sont
67
+ * compilées à la connexion (aucune au départ = base connectée mais vide).
68
+ */
69
+ var DrizzleService = class extends Service {
70
+ module;
71
+ /** ORM connectés, indexés par nom de connecteur. */
72
+ #orms = /* @__PURE__ */ new Map();
73
+ /**
74
+ * Minuteurs de re-vérification du schéma, par connecteur.
75
+ *
76
+ * `null` tant que personne n'en a besoin — c'est le cas de TOUT le
77
+ * développement, où le schéma est dérivé du code : aucun objet alloué, aucun
78
+ * minuteur armé, aucune requête périodique. Ils ne naissent que pour les
79
+ * connecteurs dont le schéma appartient aux migrations.
80
+ */
81
+ #watchers = null;
82
+ constructor(module) {
83
+ super(serviceName, module.container, module.notificationsCenter, module.options ?? {});
84
+ this.module = module;
85
+ this.module.hookKernel("onBoot", async () => {
86
+ queryFlowMonitor.setEnabled(resolveOrmFlowEnabled(this.kernel));
87
+ await this.connectAll().catch((e) => {
88
+ this.log(e, "ERROR");
89
+ throw e;
90
+ });
91
+ });
92
+ this.kernel?.once("onTerminate", async () => {
93
+ this.#stopWatchers();
94
+ await this.disconnectAll().catch(() => {});
95
+ });
96
+ }
97
+ /** Config validée (Zod) exposée par le Module (`this.module.config`). */
98
+ #config() {
99
+ return this.module.config;
100
+ }
101
+ /** Connecte tous les connecteurs déclarés en config (validée Zod). */
102
+ async connectAll() {
103
+ const connectors = this.#config()?.connectors ?? {};
104
+ for (const [name, cfg] of Object.entries(connectors)) await this.#connectOne(name, cfg);
105
+ }
106
+ /**
107
+ * Chemin SQLite par défaut d'un connecteur, résolu AU BOOT (kernel présent —
108
+ * jamais au top-level d'un import) : `<app>/var/databases/nodefony-<x>.db`.
109
+ *
110
+ * Sous `kernel.varDir` (= `<app>/var`) = la base COMMUNE des données runtime
111
+ * persistées (stores fichier + bases SQLite, lot 1 « varDir ») → un seul
112
+ * répertoire à sauvegarder/gitignorer, et « où sont mes données » a une réponse
113
+ * unique. Fallback `<root>/var` si le kernel n'a pas encore matérialisé `varDir`.
114
+ */
115
+ #defaultFilename(name) {
116
+ return defaultConnectorFilename(this.kernel, name);
117
+ }
118
+ /** Connecte un connecteur (crée le dossier de la base SQLite si nécessaire). */
119
+ async #connectOne(name, cfg) {
120
+ const dialect = cfg.dialect ?? "sqlite";
121
+ const ddl = resolveDdlMode(cfg.ddl, readMigrationEnv(this.kernel));
122
+ let filename;
123
+ if (dialect === "sqlite") {
124
+ filename = cfg.filename ?? this.#defaultFilename(name);
125
+ if (filename !== ":memory:") fs.mkdirSync(path.dirname(filename), { recursive: true });
126
+ }
127
+ const orm = new DrizzleOrm(name, {
128
+ dialect,
129
+ filename,
130
+ url: cfg.url,
131
+ deriveSchema: ddl === "auto",
132
+ migrationStatus: async () => {
133
+ const { migrationStatusFor } = await import("../src/migrator/status.js");
134
+ const result = await migrationStatusFor(name, this.#config(), this.kernel);
135
+ return result.ok ? result.report : result.failure;
136
+ },
137
+ migrationPlan: async () => {
138
+ const { migrationPlanFor } = await import("../src/migrator/status.js");
139
+ const result = await migrationPlanFor(name, this.#config(), this.kernel);
140
+ return result.ok ? result.plan : result.failure;
141
+ },
142
+ applyMigrations: async () => {
143
+ const { applyMigrationsFor } = await import("../src/migrator/status.js");
144
+ const result = await applyMigrationsFor(name, this.#config(), this.kernel);
145
+ return result.ok ? result.run : result.failure;
146
+ }
147
+ });
148
+ const target = dialect === "sqlite" ? filename : redactUrl(cfg.url);
149
+ try {
150
+ await orm.connect();
151
+ } catch (e) {
152
+ const cause = e instanceof Error ? e.message : String(e);
153
+ throw new BootConfigurationError(`Drizzle : le connecteur "${name}" (${dialect}: ${target}) n'a pas pu se connecter — corriger la configuration (infra déclarée NF_DATABASE_URL/connectors, base démarrée ?, entités portées sur ce dialecte ?) ou la retirer. Cause : ${cause}`, { cause: e });
154
+ }
155
+ this.#orms.set(name, orm);
156
+ this.log(`Drizzle « ${name} » connecté (${dialect}: ${target}) — schéma : ${ddl} ${DDL_EXPLAINED[ddl]}`, "INFO");
157
+ await this.#applySchemaPolicy(name, ddl, cfg, dialect, filename);
158
+ }
159
+ /**
160
+ * Fait ce que le mode de schéma demande, et l'ÉNONCE.
161
+ *
162
+ * - `auto` : le schéma vient d'être dérivé du code, il n'y a rien de plus à
163
+ * faire et rien à surveiller ;
164
+ * - `migrate` : les migrations sont appliquées ici, sous verrou ;
165
+ * - `none` : personne ne touche au schéma, on se contente de constater.
166
+ *
167
+ * Dans les deux derniers cas, l'état est publié à la sonde de disponibilité
168
+ * puis re-vérifié périodiquement.
169
+ *
170
+ * **Aucune de ces situations ne tue le processus.** Un schéma en retard est un
171
+ * état EXTÉRIEUR au processus : le redémarrer ne répare rien, et un
172
+ * redéploiement forcé coûterait plus cher que d'attendre. Le processus reste
173
+ * donc vivant, refuse le trafic, dit pourquoi — et repart tout seul.
174
+ */
175
+ async #applySchemaPolicy(name, ddl, cfg, dialect, filename) {
176
+ if (ddl === "auto") return;
177
+ const kernel = this.kernel;
178
+ const config = this.#config();
179
+ const check = resolveCheckMode(config.migrations?.check, readMigrationEnv(kernel));
180
+ const target = {
181
+ dialect,
182
+ filename,
183
+ url: cfg.url
184
+ };
185
+ const sources = await defaultMigrationSources(appMigrationsDir(kernel, config.migrations?.dir ?? "migrations"), { framework: config.frameworkEntities !== false });
186
+ const migrator = new DrizzleMigrator({
187
+ connector: name,
188
+ ...target,
189
+ sources,
190
+ lockTimeoutMs: config.migrations?.lockTimeoutMs
191
+ });
192
+ if (ddl === "migrate") try {
193
+ const prevu = await migrator.status();
194
+ const losses = dataLoss(scanDestructive(prevu.pending));
195
+ if (losses.length > 0) {
196
+ this.log(`${summarizeDestructive(losses, name)}\n Le démarrage N'APPLIQUE PAS ces migrations : un exemplaire qui redémarre ne supprime jamais de données de lui-même.\n` + renderDestructive(losses, true) + ` À faire, une fois la décision prise : nodefony orm:migrate --connector ${name} --allow-destructive`, "CRITIC");
197
+ await this.#publishReadiness(name, migrator, ddl, check);
198
+ this.#watch(name, migrator, ddl, check);
199
+ return;
200
+ }
201
+ const run = await migrator.migrate();
202
+ if (run.applied.length > 0) this.log(`Drizzle « ${name} » : ${run.applied.length} migration(s) appliquée(s) au démarrage — ` + run.applied.map((a) => `${a.source}/${a.tag}`).join(", "), "INFO");
203
+ } catch (e) {
204
+ this.log(`Drizzle « ${name} » : les migrations n'ont pas pu être appliquées au démarrage — ${e.message}\n Le processus reste vivant mais NE reçoit PAS de trafic. Il redeviendra disponible seul dès que le schéma sera à jour.\n Diagnostic : nodefony orm:migrate:status --connector ${name} --json`, "CRITIC");
205
+ }
206
+ await this.#publishReadiness(name, migrator, ddl, check);
207
+ this.#watch(name, migrator, ddl, check);
208
+ }
209
+ /**
210
+ * Calcule l'état du schéma et le publie à la sonde de disponibilité.
211
+ *
212
+ * Le verdict est **déjà calculé** quand la sonde le lit : `/readyz` ne fait
213
+ * qu'une comparaison d'entier, sans `await` ni allocation. C'est ce qui la
214
+ * rend insensible à une base qui tombe — une sonde qui interrogerait la base
215
+ * tomberait avec elle, et l'orchestrateur conclurait que le processus est mort
216
+ * alors que c'est la base qui l'est.
217
+ *
218
+ * ⚠️ Elle ne touche JAMAIS `/livez`. Un schéma en retard n'est pas un
219
+ * processus malade : le tuer et le relancer ne changerait rien, et ferait
220
+ * boucler l'orchestrateur sur des redémarrages inutiles.
221
+ */
222
+ async #publishReadiness(name, migrator, ddl, check) {
223
+ if (check === "off") return;
224
+ const kernel = this.kernel;
225
+ if (!kernel) return;
226
+ try {
227
+ const plan = await migrator.status();
228
+ const mode = this.#config().migrations?.divergence ?? "report";
229
+ const report = buildReport(plan, {
230
+ ddl,
231
+ divergence: mode === "off" ? null : await describeDivergence(plan),
232
+ divergenceMode: mode,
233
+ canReset: resetAllowed(readMigrationEnv(kernel))
234
+ });
235
+ const enAvance = isAheadOnly(plan);
236
+ const ok = report.exitCode === 0 || enAvance;
237
+ kernel.setReadiness(readinessName(name), ok, report.summary, check === "fail");
238
+ if (enAvance) this.log(`Drizzle « ${name} » : la base porte ${plan.missing.length} migration(s) que ce code ne connaît pas — elle est EN AVANCE. C'est attendu pendant un déploiement progressif ou après un retour arrière ; rien à appliquer, le processus peut servir.`, "INFO");
239
+ else if (!ok) this.log(`Drizzle « ${name} » : ${report.summary}\n ${meaningOf(report.verdict)}\n` + (check === "fail" ? " → le trafic est RETENU (/readyz répond 503) jusqu'à ce que ce soit réglé ; /livez reste vert, ce processus n'est pas malade.\n" : ` → le trafic passe quand même (migrations.check: "warn").\n`) + report.nextActions.map((a) => ` À faire : ${a.command}`).join("\n"), check === "fail" ? "CRITIC" : "WARNING");
240
+ else if (check === "fail") this.log(`Drizzle « ${name} » : schéma à jour — le processus peut servir.`, "INFO");
241
+ } catch (e) {
242
+ const cause = e.message;
243
+ if (check === "fail") kernel.setReadiness(readinessName(name), false, `état du schéma inconnu : ${cause}`);
244
+ this.log(`Drizzle « ${name} » : impossible de lire l'état du schéma — ${cause}\n Ce n'est pas un schéma en retard : la base n'a pas répondu. Nouvelle tentative dans ${Math.round(READINESS_POLL_MS / 1e3)} s.\n Diagnostic : nodefony orm:migrate:status --connector ${name} --json`, "CRITIC");
245
+ }
246
+ }
247
+ /**
248
+ * Arme la re-vérification périodique — le mécanisme qui rend le processus
249
+ * capable de redevenir disponible sans qu'on y touche.
250
+ */
251
+ #watch(name, migrator, ddl, check) {
252
+ if (check === "off") return;
253
+ if (this.#watchers === null) this.#watchers = /* @__PURE__ */ new Map();
254
+ const timer = setInterval(() => {
255
+ this.#publishReadiness(name, migrator, ddl, check).catch(() => void 0);
256
+ }, READINESS_POLL_MS);
257
+ timer.unref();
258
+ this.#watchers.set(name, timer);
259
+ }
260
+ /** Éteint toutes les surveillances et libère le registre. */
261
+ #stopWatchers() {
262
+ if (this.#watchers === null) return;
263
+ const kernel = this.kernel;
264
+ for (const [name, timer] of this.#watchers) {
265
+ clearInterval(timer);
266
+ kernel?.clearReadiness(readinessName(name));
267
+ }
268
+ this.#watchers.clear();
269
+ this.#watchers = null;
270
+ }
271
+ /** Ferme toutes les connexions. */
272
+ async disconnectAll() {
273
+ for (const orm of this.#orms.values()) await orm.disconnect();
274
+ this.#orms.clear();
275
+ }
276
+ /** Retourne l'ORM Drizzle d'un connecteur (défaut : `"default"`). */
277
+ getOrm(name = "default") {
278
+ return this.#orms.get(name);
279
+ }
280
+ };
281
+ //#endregion
282
+ export { DrizzleService as default };