@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
@@ -1,4 +1,4 @@
1
- //#region \0@oxc-project+runtime@0.150.0/helpers/esm/decorate.js
1
+ //#region \0@oxc-project+runtime@0.151.0/helpers/esm/decorate.js
2
2
  function __decorate(decorators, target, key, desc) {
3
3
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
4
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1,4 +1,4 @@
1
- //#region \0@oxc-project+runtime@0.150.0/helpers/esm/decorateMetadata.js
1
+ //#region \0@oxc-project+runtime@0.151.0/helpers/esm/decorateMetadata.js
2
2
  function __decorateMetadata(k, v) {
3
3
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
4
4
  }
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import { EXIT, MIGRATION_FORMAT_VERSION, action, buildReport, exitCodeOf, meanin
5
5
  import { MYSQL_LOCK_NAME_SQL } from "./nodefony/src/migrator/drivers/mysqlDriver.js";
6
6
  import { PG_LOCK_KEY } from "./nodefony/src/migrator/drivers/postgresDriver.js";
7
7
  import { openMigrationDriver } from "./nodefony/src/migrator/drivers/index.js";
8
+ import { FRAMEWORK_CONNECTOR } from "./nodefony/src/frameworkConnector.js";
8
9
  import { APP_RANK, APP_SOURCE, FRAMEWORK_RANK, FRAMEWORK_SOURCE, defaultMigrationSources, frameworkMigrationsDir } from "./nodefony/src/migrator/paths.js";
9
10
  import { migrationHash } from "./nodefony/src/migrator/hash.js";
10
11
  import { loadSources, orderSources } from "./nodefony/src/migrator/sources.js";
@@ -30,15 +31,15 @@ import { DrizzleWebAuthnCredentialStore } from "./nodefony/src/DrizzleWebAuthnCr
30
31
  import { DrizzleTotpSecretStore } from "./nodefony/src/DrizzleTotpSecretStore.js";
31
32
  import { DrizzleWebhookStore } from "./nodefony/src/DrizzleWebhookStore.js";
32
33
  import { DrizzleIdempotencyStore } from "./nodefony/src/DrizzleIdempotencyStore.js";
33
- import { FRAMEWORK_CONNECTOR, isFrameworkFallbackEntity, registerDrizzleFrameworkStores } from "./nodefony/registerStores.js";
34
+ import { isFrameworkFallbackEntity, registerDrizzleFrameworkStores } from "./nodefony/registerStores.js";
34
35
  import OrmGenerate from "./nodefony/command/orm-generate.js";
35
36
  import OrmMigrate from "./nodefony/command/orm-migrate.js";
36
37
  import OrmMigrateStatus from "./nodefony/command/orm-migrate-status.js";
37
38
  import OrmMigrateBaseline from "./nodefony/command/orm-migrate-baseline.js";
38
39
  import OrmMigrateRepair from "./nodefony/command/orm-migrate-repair.js";
39
40
  import OrmReset from "./nodefony/command/orm-reset.js";
40
- import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorateMetadata.js";
41
- import __decorate from "./_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js";
41
+ import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
42
+ import __decorate from "./_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
42
43
  import SessionStorage from "./nodefony/src/SessionStorage.js";
43
44
  import "./nodefony/src/migrator/index.js";
44
45
  import { DrizzleUserRepository } from "./nodefony/src/DrizzleUserRepository.js";
@@ -76,11 +77,13 @@ let Drizzle = class Drizzle extends Module {
76
77
  * (convention Zod, alignée sur `@nodefony/mongoose` — audit config ORM 2026-06).
77
78
  */
78
79
  async onKernelRegister() {
79
- const validated = defineDrizzleConfig(this.options ?? {});
80
+ const validated = defineDrizzleConfig(this.options ?? {}, this.appOptions ?? {});
80
81
  this.options = validated;
81
82
  registerUserStore("drizzle");
82
- if (validated.frameworkEntities !== false) {
83
- const dialect = validated.connectors?.default?.dialect ?? "sqlite";
83
+ const frameworkHost = validated.connectors.default;
84
+ if (validated.frameworkEntities !== false && frameworkHost === void 0) this.log(`pas de connecteur "${FRAMEWORK_CONNECTOR}" (infrastructure déclarée non SQL) : schéma framework non déclaré sur drizzle — les stores durables vont au backend de cette infrastructure`, "INFO");
85
+ if (validated.frameworkEntities !== false && frameworkHost !== void 0) {
86
+ const dialect = frameworkHost.dialect;
84
87
  const report = registerDrizzleFrameworkStores(dialect);
85
88
  if (report.unported.length) this.log(`schéma framework : entités non portées sur "${dialect}" → stores drizzle indisponibles pour [${report.unported.join(", ")}] (chantier multi-dialecte Ph.2.1)`, "WARNING");
86
89
  if (report.appOwned.length) this.log(`schéma framework : entités déjà enregistrées par l'app [${report.appOwned.join(", ")}] — auto-register respecte l'app`, "DEBUG");
@@ -1,8 +1,9 @@
1
1
  import { HISTORY_TABLE, MigrationVerdictError } from "../src/migrator/types.js";
2
2
  import { EXIT, action, describeDiscovery, refusalInMode, renderRefusal, styleFor } from "../src/migrator/explain.js";
3
3
  import { missingHistoryColumn } from "../src/migrator/history.js";
4
+ import { isReservedConnectorName } from "../src/migrator/paths.js";
4
5
  import { buildMigrator, readMigrationEnv, resolveConnector } from "../src/migrator/resolve.js";
5
- import { MigrationToolError, describeResolutionRefusal, moduleAbsent } from "../src/migrator/refusals.js";
6
+ import { MigrationToolError, describeResolutionRefusal, moduleAbsent, reservedConnectorName } from "../src/migrator/refusals.js";
6
7
  import { composeReport } from "../src/migrator/status.js";
7
8
  import { CONSOLE_DATA_RUN_PROFILE, Command, resolveColorEnabled } from "nodefony";
8
9
  //#region nodefony/command/migrateShared.ts
@@ -180,6 +181,21 @@ var OrmMigrateCommand = class extends Command {
180
181
  };
181
182
  }
182
183
  /**
184
+ * Arrête une commande qui ÉCRIRAIT une migration pour un connecteur
185
+ * secondaire dont le nom est celui d'un dossier de dialecte — son dossier
186
+ * propre se confondrait avec celui des migrations du framework.
187
+ *
188
+ * @param connector - connecteur résolu.
189
+ * @param json - mode machine.
190
+ * @returns `true` si la commande est arrêtée.
191
+ */
192
+ refuseReservedConnector(connector, json) {
193
+ if (!isReservedConnectorName(connector)) return false;
194
+ const refusal = reservedConnectorName(connector);
195
+ this.fail(connector, refusal.code, refusal.summary, refusal.meaning, refusal.nextActions, json, refusal.exitCode);
196
+ return true;
197
+ }
198
+ /**
183
199
  * Construit l'applicateur d'un connecteur résolu.
184
200
  *
185
201
  * @param resolution - connecteur prêt.
@@ -1,13 +1,14 @@
1
1
  import { HISTORY_TABLE } from "../src/migrator/types.js";
2
2
  import { EXIT, action } from "../src/migrator/explain.js";
3
3
  import { openMigrationDriver } from "../src/migrator/drivers/index.js";
4
- import { frameworkMigrationsDir } from "../src/migrator/paths.js";
4
+ import { ownsSharedMigrations } from "../src/frameworkConnector.js";
5
+ import { connectorMigrationsDir, frameworkMigrationsDir } from "../src/migrator/paths.js";
5
6
  import { createdTables, frameworkTables, loadSources } from "../src/migrator/sources.js";
6
7
  import { appMigrationsDir, generateApplyAllowed, readMigrationEnv } from "../src/migrator/resolve.js";
7
8
  import { summarizeGap } from "../src/migrator/schemaDiff.js";
8
9
  import { gapAgainstDeclared } from "../src/migrator/divergence.js";
9
10
  import { auditMigrationSql, runGenerate, stampFormatMarker } from "../src/migrator/kit.js";
10
- import { collectTables, entityFilesOf, missingProviders, registeredTables, usurpedTables, writeCustomMigration, writeKitConfig, writeSchemaModule } from "../src/migrator/appSchema.js";
11
+ import { collectTables, entityFilesOf, missingProviders, registeredTables, tablesOfConnector, usurpedTables, writeCustomMigration, writeKitConfig, writeSchemaModule } from "../src/migrator/appSchema.js";
11
12
  import { checkMigrationName } from "../src/migrator/name.js";
12
13
  import { repairRebuildCopy } from "../src/migrator/rebuildCopy.js";
13
14
  import { readJournal, tablesPresentIn } from "../src/migrator/adopt.js";
@@ -113,6 +114,7 @@ var OrmGenerate = class extends OrmMigrateCommand {
113
114
  if (!resolved) return this;
114
115
  const { resolution, config } = resolved;
115
116
  const connector = resolution.connector;
117
+ if (this.refuseReservedConnector(connector, opts.json)) return this;
116
118
  const name = this.#nameOrFail(opts, connector);
117
119
  if (name === null) return this;
118
120
  const root = this.#projectRoot();
@@ -121,7 +123,8 @@ var OrmGenerate = class extends OrmMigrateCommand {
121
123
  this.fail(connector, "NF_MIGRATE_UNAVAILABLE", "La racine de l'application n'a pas pu être résolue : impossible de savoir où écrire les migrations.", "Le dossier des migrations se résout depuis la racine de l'application, jamais depuis le répertoire courant — sans quoi la commande serait juste ou fausse selon l'endroit d'où on la tape.", [action("nodefony inspect config --json")], opts.json);
122
124
  return this;
123
125
  }
124
- const outDir = path.join(migrationsDir, resolution.dialect);
126
+ const connectorDir = connectorMigrationsDir(migrationsDir, connector) ?? migrationsDir;
127
+ const outDir = path.join(connectorDir, resolution.dialect);
125
128
  const relative = (p) => path.relative(root, p).split(path.sep).join("/");
126
129
  try {
127
130
  if (opts.custom === true) {
@@ -169,8 +172,9 @@ var OrmGenerate = class extends OrmMigrateCommand {
169
172
  const frameworkRoot = path.dirname(await frameworkMigrationsDir());
170
173
  const belongsToFramework = (file) => !path.relative(frameworkRoot, file).startsWith("..");
171
174
  const mine = found.filter((t) => !belongsToFramework(t.file));
172
- const tables = mine.filter((t) => t.dialect === dialect);
173
- const otherDialect = mine.filter((t) => t.dialect !== dialect).map((t) => ({
175
+ const ofConnector = tablesOfConnector(mine, connector);
176
+ const tables = ofConnector.filter((t) => t.dialect === dialect);
177
+ const otherDialect = ofConnector.filter((t) => t.dialect !== dialect).map((t) => ({
174
178
  table: t.tableName,
175
179
  dialect: t.dialect ?? "inconnu",
176
180
  file: relative(t.file)
@@ -181,7 +185,7 @@ var OrmGenerate = class extends OrmMigrateCommand {
181
185
  otherDialect,
182
186
  unreadable
183
187
  };
184
- const framework = new Set(await frameworkTables(dialect));
188
+ const framework = new Set(ownsSharedMigrations(connector) ? await frameworkTables(dialect) : []);
185
189
  const usurped = usurpedTables(tables, framework);
186
190
  if (usurped.length > 0) {
187
191
  const list = usurped.map((t) => ` • « ${t.tableName} », exportée par ${relative(t.file)} (${t.exportName})`).join("\n");
@@ -223,7 +227,7 @@ var OrmGenerate = class extends OrmMigrateCommand {
223
227
  configRel: relative(configFile),
224
228
  name,
225
229
  label: `le connecteur « ${connector} » (${dialect})`,
226
- regenerateCommand: `nodefony orm:generate --name ${name}`
230
+ regenerateCommand: ownsSharedMigrations(connector) ? `nodefony orm:generate --name ${name}` : `nodefony orm:generate --connector ${connector} --name ${name}`
227
231
  });
228
232
  } finally {
229
233
  await fs.rm(work, {
@@ -1,5 +1,7 @@
1
1
  import { HISTORY_TABLE } from "../src/migrator/types.js";
2
2
  import { EXIT, MIGRATION_FORMAT_VERSION, action, renderStatus } from "../src/migrator/explain.js";
3
+ import { ownsSharedMigrations } from "../src/frameworkConnector.js";
4
+ import { connectorMigrationsDir } from "../src/migrator/paths.js";
3
5
  import { frameworkTables } from "../src/migrator/sources.js";
4
6
  import { appMigrationsDir } from "../src/migrator/resolve.js";
5
7
  import { summarizeGap } from "../src/migrator/schemaDiff.js";
@@ -119,13 +121,14 @@ var OrmMigrateBaseline = class extends OrmMigrateCommand {
119
121
  * @returns ce qui a été écrit, ou `null` si la commande a déjà refusé.
120
122
  */
121
123
  async #fromDatabase(opts, resolution, config) {
124
+ if (this.refuseReservedConnector(resolution.connector, opts.json)) return null;
122
125
  const root = this.kernel.path;
123
126
  const dir = appMigrationsDir(this.kernel, config.migrations.dir);
124
127
  if (dir === void 0) {
125
128
  this.fail(resolution.connector, "NF_MIGRATE_UNAVAILABLE", "La racine de l'application n'a pas pu être résolue : impossible de savoir où écrire la migration de référence.", "Le dossier des migrations se résout depuis la racine de l'application, jamais depuis le répertoire courant — sans quoi la commande serait juste ou fausse selon l'endroit d'où on la tape.", [action("nodefony inspect config --json")], opts.json);
126
129
  return null;
127
130
  }
128
- const outDir = path.join(dir, resolution.dialect);
131
+ const outDir = path.join(connectorMigrationsDir(dir, resolution.connector) ?? dir, resolution.dialect);
129
132
  const journal = await readJournal(outDir);
130
133
  if (journal !== null && journal.entries.length > 0) {
131
134
  this.fail(resolution.connector, "NF_MIGRATE_BASELINE_NOT_EMPTY", `Rien n'a été écrit : ce dossier porte déjà ${journal.entries.length} migration(s) (${outDir}).`, "La lecture de la base ne sert qu'à DÉMARRER un historique qui n'existe pas encore. Ici il en existe un : c'est lui qui fait foi, et la base s'adopte alors telle quelle. Si ces migrations décrivent bien l'état de la base, `orm:migrate:baseline` sans option les inscrit ; si elles décrivent autre chose, c'est un écart à regarder, pas une référence à réécrire.", [action(`nodefony orm:migrate:status --connector ${resolution.connector} --json`), action(`nodefony orm:migrate:baseline --connector ${resolution.connector}`)], opts.json, EXIT.actionRequired);
@@ -141,7 +144,7 @@ var OrmMigrateBaseline = class extends OrmMigrateCommand {
141
144
  outDir,
142
145
  dialect: resolution.dialect,
143
146
  target: resolution.target,
144
- excludedTables: [...await frameworkTables(resolution.dialect), HISTORY_TABLE],
147
+ excludedTables: [...ownsSharedMigrations(resolution.connector) ? await frameworkTables(resolution.dialect) : [], HISTORY_TABLE],
145
148
  declaredTables: registeredTables(resolution.connector).map((t) => t.table),
146
149
  name: verdict.name,
147
150
  workDir: path.join(root, "node_modules", ".cache", "nodefony", "orm-adopt")
@@ -1,4 +1,5 @@
1
1
  import { action } from "../src/migrator/explain.js";
2
+ import "../src/memoryDatabase.js";
2
3
  import { openMigrationDriver } from "../src/migrator/drivers/index.js";
3
4
  import { readHistory } from "../src/migrator/history.js";
4
5
  import { readMigrationEnv, resetAllowed } from "../src/migrator/resolve.js";
@@ -10,11 +10,23 @@ import { z } from "zod";
10
10
  *
11
11
  * - Infra `database` (`NF_DATABASE_URL`, alias `DATABASE_URL`) de famille SQL →
12
12
  * dialecte + cible du connecteur primaire (`default`, sinon le premier) :
13
- * `sqlite:…` → `filename` ; `postgres://…`/`mysql://…` → `url`. Une URL
14
- * `mongodb://` est IGNORÉE ici (l'infra appartient alors à `@nodefony/mongoose`).
13
+ * `sqlite:…` → `filename` ; `postgres://…`/`mysql://…` → `url`.
14
+ * - Infra `database` d'une AUTRE famille (`mongodb://…`) : l'URL appartient à
15
+ * `@nodefony/mongoose`, Drizzle ne la lit pas — et il ne crée pas non plus le
16
+ * connecteur `default` que son schéma pose par défaut. Ce connecteur-là, personne
17
+ * ne l'a demandé : il ouvrait une base SQLite parallèle où le schéma du
18
+ * framework se déclarait en double. Un `default` ÉCRIT par l'application, lui,
19
+ * est gardé — c'est ce qui sépare « par défaut » de « demandé ».
20
+ *
21
+ * @param config - config validée (défauts du schéma posés)
22
+ * @param declaredDefault - l'application a-t-elle écrit un connecteur `default` ?
15
23
  */
16
- function applyEnvOverrides(config) {
24
+ function applyEnvOverrides(config, declaredDefault) {
17
25
  const database = resolveInfra(process.env).database;
26
+ if (database && database.family !== "sql" && !declaredDefault) {
27
+ delete config.connectors.default;
28
+ return config;
29
+ }
18
30
  if (database && database.family === "sql" && database.dialect) {
19
31
  const target = config.connectors.default ? "default" : Object.keys(config.connectors)[0];
20
32
  const connector = target ? config.connectors[target] : void 0;
@@ -45,12 +57,16 @@ function applyEnvOverrides(config) {
45
57
  * au boot — cf audit config ORM 2026-06 §3.2.
46
58
  *
47
59
  * @param config - configuration brute (sections omises = défauts sûrs).
60
+ * @param declared - ce que l'APPLICATION a écrit elle-même. Au boot, `config`
61
+ * arrive déjà fusionné avec les défauts du module (`config.ts`) : on ne peut
62
+ * plus y distinguer un `default` demandé d'un `default` posé par défaut. Omis,
63
+ * `config` fait foi (appel direct, tests).
48
64
  * @returns config validée, surchargée par l'env, et gelée.
49
65
  * @throws ZodError si la config est invalide.
50
66
  */
51
- function defineDrizzleConfig(config = {}) {
67
+ function defineDrizzleConfig(config = {}, declared = config) {
52
68
  const parsed = parseModuleConfig(drizzleConfigSchema, config, "@nodefony/drizzle");
53
- return Object.freeze(applyEnvOverrides(parsed));
69
+ return Object.freeze(applyEnvOverrides(parsed, declared.connectors?.default !== void 0));
54
70
  }
55
71
  /**
56
72
  * JSON Schema introspectable de la config Drizzle — destiné au formulaire
@@ -81,6 +81,9 @@ const createUserTable = createFrameworkTableFactory(USER_TABLE_SPEC);
81
81
  /**
82
82
  * Variante SQLite de la table `User` (dialecte par défaut) — export conservé
83
83
  * pour l'usage direct/banc-test (ex. module mediasoup).
84
+ *
85
+ * Son type n'est PAS réécrit en `SQLiteTable` : l'annoter ainsi effaçait la
86
+ * clé primaire que la factory vient d'exposer, et c'était la moitié du défaut.
84
87
  */
85
88
  const userTable = createUserTable("sqlite");
86
89
  /**
@@ -1,3 +1,4 @@
1
+ import { FRAMEWORK_CONNECTOR } from "./src/frameworkConnector.js";
1
2
  import { DrizzleOrm } from "./src/orm-core/DrizzleOrm.js";
2
3
  import "./src/orm-core/index.js";
3
4
  import { TOKEN_ENTITY_NAMES, registerTokenEntities } from "./entity/tokenEntity.js";
@@ -19,31 +20,6 @@ import { assertUserContract } from "@nodefony/user";
19
20
  import { getAuditStoreFactory, getTokenStoreFactory, getTotpStoreFactory, getWebAuthnStoreFactory, getWebhookStoreFactory, registerAuditStore, registerTokenStore, registerTotpStore, registerWebAuthnStore, registerWebhookStore } from "@nodefony/security";
20
21
  import { getIdempotencyStoreFactory, registerIdempotencyStore } from "@nodefony/framework";
21
22
  //#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
23
  /** Portage par entité (chantier multi-dialecte — Ph.2.1 allume les cases). */
48
24
  const ALL_DIALECTS = [
49
25
  "sqlite",
@@ -177,7 +153,7 @@ function registerDrizzleFrameworkStores(dialect) {
177
153
  } catch {
178
154
  return;
179
155
  }
180
- });
156
+ }, FRAMEWORK_CONNECTOR);
181
157
  });
182
158
  });
183
159
  if (report.appOwned.includes("User")) assertAppUserEntityHonoursContract(dialect);
@@ -1,14 +1,16 @@
1
1
  import { buildReport, isAheadOnly, meaningOf } from "../src/migrator/explain.js";
2
2
  import { defaultConnectorFilename } from "../src/connectorTarget.js";
3
+ import { MEMORY_DATABASE } from "../src/memoryDatabase.js";
4
+ import { FRAMEWORK_CONNECTOR, ownsSharedMigrations } from "../src/frameworkConnector.js";
3
5
  import { defaultMigrationSources } from "../src/migrator/paths.js";
4
6
  import { DrizzleMigrator } from "../src/migrator/DrizzleMigrator.js";
5
- import { appMigrationsDir, appVersionsMigrations, readMigrationEnv, resetAllowed, resolveCheckMode, resolveDdlMode } from "../src/migrator/resolve.js";
7
+ import { adviseMigrations, appMigrationsDir, connectorVersionsMigrations, readMigrationEnv, resetAllowed, resolveCheckMode, resolveDdlMode } from "../src/migrator/resolve.js";
6
8
  import { describeDivergence } from "../src/migrator/divergence.js";
7
9
  import { DrizzleOrm } from "../src/orm-core/DrizzleOrm.js";
8
10
  import "../src/orm-core/index.js";
9
11
  import { dataLoss, renderDestructive, scanDestructive, summarizeDestructive } from "../src/migrator/destructive.js";
10
12
  import { BootConfigurationError, Service, runNeedsExternalServices } from "nodefony";
11
- import { diagnoseConnectionFailure, parseConnectionTarget, queryFlowMonitor, resolveOrmFlowEnabled } from "@nodefony/orm-core";
13
+ import { describeConnectFailure, diagnoseConnectionFailure, parseConnectionTarget, queryFlowMonitor, reportOrphanEntities, resolveOrmFlowEnabled } from "@nodefony/orm-core";
12
14
  import fs from "node:fs";
13
15
  import path from "node:path";
14
16
  //#region nodefony/service/DrizzleService.ts
@@ -35,7 +37,7 @@ const READINESS_POLL_MS = 15e3;
35
37
  * suffit.
36
38
  */
37
39
  const DDL_EXPLAINED = {
38
- auto: "(dérivé du code au démarrage — développement ; les colonnes manquantes qui acceptent le vide sont ajoutées)",
40
+ auto: "(dérivé du code au démarrage ; les colonnes manquantes qui acceptent le vide sont ajoutées)",
39
41
  migrate: "(migrations appliquées au démarrage, sous verrou — un seul exemplaire assumé)",
40
42
  none: "(personne ne touche au schéma ici — un travail externe lance « nodefony orm:migrate »)"
41
43
  };
@@ -86,6 +88,9 @@ var DrizzleService = class extends Service {
86
88
  queryFlowMonitor.setEnabled(resolveOrmFlowEnabled(this.kernel));
87
89
  await this.connectAll(runNeedsExternalServices(this.kernel));
88
90
  });
91
+ this.module.hookKernel("onReady", () => {
92
+ reportOrphanEntities((message, severity) => this.log(message, severity), this.kernel ?? this, Object.keys(this.#config()?.connectors ?? {}));
93
+ });
89
94
  this.kernel?.once("onTerminate", async () => {
90
95
  this.#stopWatchers();
91
96
  await this.disconnectAll().catch(() => {});
@@ -122,13 +127,14 @@ var DrizzleService = class extends Service {
122
127
  /** Connecte un connecteur (crée le dossier de la base SQLite si nécessaire). */
123
128
  async #connectOne(name, cfg, connect = true) {
124
129
  const dialect = cfg.dialect ?? "sqlite";
125
- const ddl = resolveDdlMode(cfg.ddl, readMigrationEnv(this.kernel), appVersionsMigrations(appMigrationsDir(this.kernel, this.#config().migrations?.dir ?? "migrations")));
130
+ const ddl = resolveDdlMode(cfg.ddl, readMigrationEnv(this.kernel), connectorVersionsMigrations(name, appMigrationsDir(this.kernel, this.#config().migrations?.dir ?? "migrations")), cfg.filename === MEMORY_DATABASE);
126
131
  let filename;
127
132
  if (dialect === "sqlite") {
128
133
  filename = cfg.filename ?? this.#defaultFilename(name);
129
134
  if (filename !== ":memory:") fs.mkdirSync(path.dirname(filename), { recursive: true });
130
135
  }
131
136
  const orm = new DrizzleOrm(name, {
137
+ container: this.container ?? void 0,
132
138
  dialect,
133
139
  filename,
134
140
  url: cfg.url,
@@ -163,7 +169,7 @@ var DrizzleService = class extends Service {
163
169
  host: null,
164
170
  port: null
165
171
  } : parseConnectionTarget(cfg.url));
166
- throw new BootConfigurationError(`Drizzle : le connecteur "${name}" (${dialect}: ${target}) n'a pas pu se connecter — ${diagnosis.explanation} Si l'adresse est la bonne, vérifier l'infrastructure déclarée (NF_DATABASE_URL / connectors) et que les entités sont portées sur ce dialecte, ou retirer le connecteur. Cause : ${cause}`, { cause: e });
172
+ throw new BootConfigurationError(describeConnectFailure(`Drizzle : le connecteur "${name}" (${dialect}: ${target})`, diagnosis, cause, " Si l'adresse est la bonne, vérifier l'infrastructure déclarée (NF_DATABASE_URL / connectors) et que les entités sont portées sur ce dialecte, ou retirer le connecteur."), { cause: e });
167
173
  }
168
174
  this.#orms.set(name, orm);
169
175
  this.log(`Drizzle « ${name} » connecté (${dialect}: ${target}) — schéma : ${ddl} ${DDL_EXPLAINED[ddl]}`, "INFO");
@@ -191,13 +197,19 @@ var DrizzleService = class extends Service {
191
197
  const env = readMigrationEnv(kernel);
192
198
  const check = resolveCheckMode(config.migrations?.check, env);
193
199
  const adviseOnly = ddl === "auto";
194
- if (adviseOnly && (check === "off" || env.runtime !== "development")) return;
200
+ const appDir = appMigrationsDir(kernel, config.migrations?.dir ?? "migrations");
201
+ const hasMigrations = ownsSharedMigrations(name) || connectorVersionsMigrations(name, appDir);
202
+ if (adviseOnly && (!hasMigrations || !adviseMigrations(check, env, filename))) return;
203
+ if (!hasMigrations) this.log(`Drizzle « ${name} » : \`ddl: "migrate"\` sans migration à appliquer — « ${name} » n'est pas le connecteur du framework (« ${FRAMEWORK_CONNECTOR} ») et aucune migration n'est versionnée sous son dossier. Son schéma n'est ni dérivé ni migré par le framework : \`nodefony orm:generate --connector ${name}\` écrit sa première migration.`, "WARNING");
195
204
  const target = {
196
205
  dialect,
197
206
  filename,
198
207
  url: cfg.url
199
208
  };
200
- const sources = await defaultMigrationSources(appMigrationsDir(kernel, config.migrations?.dir ?? "migrations"), { framework: config.frameworkEntities !== false });
209
+ const sources = await defaultMigrationSources(appDir, {
210
+ framework: config.frameworkEntities !== false,
211
+ connector: name
212
+ });
201
213
  const migrator = new DrizzleMigrator({
202
214
  connector: name,
203
215
  ...target,
@@ -71,6 +71,16 @@ var DrizzleAuditStore = class DrizzleAuditStore {
71
71
  get location() {
72
72
  return this.#location;
73
73
  }
74
+ /** Connecteur ORM qui porte ce store — posé par {@link DrizzleAuditStore.from}. */
75
+ #connector;
76
+ /**
77
+ * Connecteur ORM qui porte ce store, lu par `readStoreConnector` pour le
78
+ * registre des stores : la console rattache la brique à SON connecteur au
79
+ * lieu de le déduire. `undefined` pour un store construit sans ORM (bancs).
80
+ */
81
+ get connector() {
82
+ return this.#connector;
83
+ }
74
84
  /**
75
85
  * Construit le store depuis un {@link DrizzleOrm}. Le handle est résolu **lazy**
76
86
  * (gardé par `isConnected()` → `null` tant que l'ORM n'est pas/plus connecté).
@@ -83,7 +93,9 @@ var DrizzleAuditStore = class DrizzleAuditStore {
83
93
  * @param retentionMs - fenêtre de rétention (ms).
84
94
  */
85
95
  static from(orm, now, retentionMs) {
86
- return new DrizzleAuditStore(() => orm.isConnected() ? orm.getNativeConnection() : null, now, retentionMs, createAuditEventTable(orm.dialect), orm.location);
96
+ const store = new DrizzleAuditStore(() => orm.isConnected() ? orm.getNativeConnection() : null, now, retentionMs, createAuditEventTable(orm.dialect), orm.location);
97
+ store.#connector = orm.name;
98
+ return store;
87
99
  }
88
100
  async append(event) {
89
101
  const db = this.#resolveDb();
@@ -104,8 +104,10 @@ var DrizzleIdempotencyStore = class DrizzleIdempotencyStore {
104
104
  * base ({@link DrizzleOrm.location}) pour Studio. Lazy (comme `resolveDb`) car
105
105
  * le store est fabriqué AVANT le connect de l'ORM → l'emplacement n'est lisible
106
106
  * qu'une fois l'ORM enregistré (lu au `onReady`, pas à la construction).
107
+ * @param connector - connecteur ORM qui porte la table, publié au registre des stores.
107
108
  */
108
- constructor(resolveDb, now = Date.now, leaseMs = DEFAULT_LEASE_MS, ttlMs = DEFAULT_TTL_MS, table = idempotencyKeyTable, resolveLocation) {
109
+ constructor(resolveDb, now = Date.now, leaseMs = DEFAULT_LEASE_MS, ttlMs = DEFAULT_TTL_MS, table = idempotencyKeyTable, resolveLocation, connector) {
110
+ this.#connector = connector;
109
111
  this.#resolveDb = resolveDb;
110
112
  this.#table = table;
111
113
  this.#c = execTable(table);
@@ -124,6 +126,16 @@ var DrizzleIdempotencyStore = class DrizzleIdempotencyStore {
124
126
  get location() {
125
127
  return this.#resolveLocation?.();
126
128
  }
129
+ /** Connecteur ORM qui porte ce store — reçu du constructeur. */
130
+ #connector;
131
+ /**
132
+ * Connecteur ORM qui porte ce store, lu par `readStoreConnector` pour le
133
+ * registre des stores : la console rattache la brique à SON connecteur au
134
+ * lieu de le déduire. `undefined` pour un store construit sans ORM (bancs).
135
+ */
136
+ get connector() {
137
+ return this.#connector;
138
+ }
127
139
  /**
128
140
  * Construit le store depuis un {@link DrizzleOrm}. Le handle est résolu **lazy**
129
141
  * (gardé par `isConnected()` → `null` tant que l'ORM n'est pas/plus connecté).
@@ -137,7 +149,7 @@ var DrizzleIdempotencyStore = class DrizzleIdempotencyStore {
137
149
  * @param ttlMs - rétention d'une réponse mémorisée (ms).
138
150
  */
139
151
  static from(orm, now, leaseMs, ttlMs) {
140
- return new DrizzleIdempotencyStore(() => orm.isConnected() ? orm.getNativeConnection() : null, now, leaseMs, ttlMs, createIdempotencyTable(orm.dialect), () => orm.location);
152
+ return new DrizzleIdempotencyStore(() => orm.isConnected() ? orm.getNativeConnection() : null, now, leaseMs, ttlMs, createIdempotencyTable(orm.dialect), () => orm.location, orm.name);
141
153
  }
142
154
  /**
143
155
  * Approximation **per-pod, best-effort** : compteur local des réservations
@@ -75,6 +75,16 @@ var DrizzleTokenStore = class DrizzleTokenStore {
75
75
  get location() {
76
76
  return this.#location;
77
77
  }
78
+ /** Connecteur ORM qui porte ce store — posé par {@link DrizzleTokenStore.from}. */
79
+ #connector;
80
+ /**
81
+ * Connecteur ORM qui porte ce store, lu par `readStoreConnector` pour le
82
+ * registre des stores : la console rattache la brique à SON connecteur au
83
+ * lieu de le déduire. `undefined` pour un store construit sans ORM (bancs).
84
+ */
85
+ get connector() {
86
+ return this.#connector;
87
+ }
78
88
  /**
79
89
  * Construit le store depuis un {@link DrizzleOrm} connecté. Les entités
80
90
  * (`registerTokenEntities`) doivent avoir été enregistrées **avant**
@@ -85,7 +95,9 @@ var DrizzleTokenStore = class DrizzleTokenStore {
85
95
  * @param retentionRevokedMs - rétention des PAT révoqués sans `exp`.
86
96
  */
87
97
  static from(orm, now, retentionRevokedMs) {
88
- return new DrizzleTokenStore(orm.getRepository(TOKEN_ENTITY_NAMES.records), orm.getRepository(TOKEN_ENTITY_NAMES.denied), orm.getRepository(TOKEN_ENTITY_NAMES.revocations), now, retentionRevokedMs, orm.location);
98
+ const store = new DrizzleTokenStore(orm.getRepository(TOKEN_ENTITY_NAMES.records), orm.getRepository(TOKEN_ENTITY_NAMES.denied), orm.getRepository(TOKEN_ENTITY_NAMES.revocations), now, retentionRevokedMs, orm.location);
99
+ store.#connector = orm.name;
100
+ return store;
89
101
  }
90
102
  /**
91
103
  * Insère ou remplace un record (PAT / refresh) — 1 requête, `upsert` atomique
@@ -40,6 +40,16 @@ var DrizzleTotpSecretStore = class DrizzleTotpSecretStore {
40
40
  get location() {
41
41
  return this.#location;
42
42
  }
43
+ /** Connecteur ORM qui porte ce store — posé par {@link DrizzleTotpSecretStore.from}. */
44
+ #connector;
45
+ /**
46
+ * Connecteur ORM qui porte ce store, lu par `readStoreConnector` pour le
47
+ * registre des stores : la console rattache la brique à SON connecteur au
48
+ * lieu de le déduire. `undefined` pour un store construit sans ORM (bancs).
49
+ */
50
+ get connector() {
51
+ return this.#connector;
52
+ }
43
53
  /**
44
54
  * Construit le store depuis un {@link DrizzleOrm} connecté. L'entité
45
55
  * (`registerTotpSecretEntity`) doit avoir été enregistrée **avant** `orm.connect()`.
@@ -47,7 +57,9 @@ var DrizzleTotpSecretStore = class DrizzleTotpSecretStore {
47
57
  * @param orm - ORM Drizzle connecté hébergeant la table du store.
48
58
  */
49
59
  static from(orm) {
50
- return new DrizzleTotpSecretStore(orm.getRepository(TOTP_SECRET_ENTITY), orm.location);
60
+ const store = new DrizzleTotpSecretStore(orm.getRepository(TOTP_SECRET_ENTITY), orm.location);
61
+ store.#connector = orm.name;
62
+ return store;
51
63
  }
52
64
  /** Row plate → secret du contrat (recoveryCodes copié = mutable indépendant). */
53
65
  #toSecret(row) {
@@ -39,6 +39,16 @@ var DrizzleWebAuthnCredentialStore = class DrizzleWebAuthnCredentialStore {
39
39
  get location() {
40
40
  return this.#location;
41
41
  }
42
+ /** Connecteur ORM qui porte ce store — posé par {@link DrizzleWebAuthnCredentialStore.from}. */
43
+ #connector;
44
+ /**
45
+ * Connecteur ORM qui porte ce store, lu par `readStoreConnector` pour le
46
+ * registre des stores : la console rattache la brique à SON connecteur au
47
+ * lieu de le déduire. `undefined` pour un store construit sans ORM (bancs).
48
+ */
49
+ get connector() {
50
+ return this.#connector;
51
+ }
42
52
  /**
43
53
  * Construit le store depuis un {@link DrizzleOrm} connecté. L'entité
44
54
  * (`registerWebAuthnCredentialEntity`) doit avoir été enregistrée **avant**
@@ -47,7 +57,9 @@ var DrizzleWebAuthnCredentialStore = class DrizzleWebAuthnCredentialStore {
47
57
  * @param orm - ORM Drizzle connecté hébergeant la table du store.
48
58
  */
49
59
  static from(orm) {
50
- return new DrizzleWebAuthnCredentialStore(orm.getRepository(WEBAUTHN_CREDENTIAL_ENTITY), orm.location);
60
+ const store = new DrizzleWebAuthnCredentialStore(orm.getRepository(WEBAUTHN_CREDENTIAL_ENTITY), orm.location);
61
+ store.#connector = orm.name;
62
+ return store;
51
63
  }
52
64
  /** Row plate → credential du contrat (`nickname?` omis si `null`). */
53
65
  #toCredential(row) {
@@ -59,6 +59,16 @@ var DrizzleWebhookStore = class DrizzleWebhookStore {
59
59
  get location() {
60
60
  return this.#location;
61
61
  }
62
+ /** Connecteur ORM qui porte ce store — posé par {@link DrizzleWebhookStore.from}. */
63
+ #connector;
64
+ /**
65
+ * Connecteur ORM qui porte ce store, lu par `readStoreConnector` pour le
66
+ * registre des stores : la console rattache la brique à SON connecteur au
67
+ * lieu de le déduire. `undefined` pour un store construit sans ORM (bancs).
68
+ */
69
+ get connector() {
70
+ return this.#connector;
71
+ }
62
72
  /**
63
73
  * Construit le store depuis un {@link DrizzleOrm} connecté. L'entité
64
74
  * (`registerWebhookEndpointEntity`) doit avoir été enregistrée **avant**
@@ -67,7 +77,9 @@ var DrizzleWebhookStore = class DrizzleWebhookStore {
67
77
  * @param orm - ORM Drizzle connecté hébergeant la table du store.
68
78
  */
69
79
  static from(orm) {
70
- return new DrizzleWebhookStore(orm.getRepository(WEBHOOK_ENDPOINT_ENTITY), orm.location, orm.getNativeConnection(), orm.dialect);
80
+ const store = new DrizzleWebhookStore(orm.getRepository(WEBHOOK_ENDPOINT_ENTITY), orm.location, orm.getNativeConnection(), orm.dialect);
81
+ store.#connector = orm.name;
82
+ return store;
71
83
  }
72
84
  /** Row plate → endpoint du contrat (`events` mutable accepté en `readonly`). */
73
85
  #toEndpoint(row) {
@@ -41,6 +41,14 @@ var SessionStorage = class SessionStorage {
41
41
  return typeof loc === "string" && loc.length > 0 ? loc : void 0;
42
42
  }
43
43
  /**
44
+ * Connecteur ORM qui porte les sessions — lu par `readStoreConnector` pour le
45
+ * registre des stores. `undefined` tant que l'ORM de ce connecteur n'est pas
46
+ * enregistré : on ne publie pas un rattachement que rien ne porte.
47
+ */
48
+ get connector() {
49
+ return ormRegistry.has("default") ? SESSION_CONNECTOR : void 0;
50
+ }
51
+ /**
44
52
  * Repository de l'entité session, ou `null` si l'ORM n'est pas (ou plus)
45
53
  * connecté.
46
54
  *
@@ -0,0 +1,29 @@
1
+ //#region nodefony/src/frameworkConnector.ts
2
+ /**
3
+ * Connecteur conventionnel qui héberge le schéma framework (même convention que
4
+ * `SESSION_CONNECTOR` : `"default"` pour Drizzle, `"nodefony"` pour Mongoose).
5
+ *
6
+ * Vit dans ce fichier sans dépendance parce que DEUX familles le lisent : le
7
+ * registre des stores (où résoudre les briques du framework) et les migrations
8
+ * (qui les reçoit). Une copie du littéral de chaque côté dériverait en silence.
9
+ */
10
+ const FRAMEWORK_CONNECTOR = "default";
11
+ /**
12
+ * Ce connecteur possède-t-il les migrations du framework et de l'application ?
13
+ *
14
+ * **Seul le connecteur du framework les possède.** Le dossier des migrations de
15
+ * l'application (`migrations/<dialect>`) ne porte aucune notion de connecteur :
16
+ * il décrit UNE base, celle où vivent les briques du framework et les entités
17
+ * de l'application. Les attribuer à tout connecteur faisait qu'une base
18
+ * secondaire (analyse, base d'un module) en mode `migrate` y créait des tables
19
+ * qui ne lui appartiennent pas, et qu'en mode `auto` elle annonçait à chaque
20
+ * démarrage des migrations qui ne la concernent pas.
21
+ *
22
+ * @param connector - nom du connecteur (clé de `connectors`).
23
+ * @returns `true` pour le connecteur du framework, `false` pour tout autre.
24
+ */
25
+ function ownsSharedMigrations(connector) {
26
+ return connector === FRAMEWORK_CONNECTOR;
27
+ }
28
+ //#endregion
29
+ export { FRAMEWORK_CONNECTOR, ownsSharedMigrations };
@@ -0,0 +1,13 @@
1
+ //#region nodefony/src/memoryDatabase.ts
2
+ /**
3
+ * Nom de fichier d'une base SQLite en MÉMOIRE — volatile, elle repart vide à
4
+ * chaque démarrage.
5
+ *
6
+ * Module FEUILLE (aucun import) : l'adapter, le migrateur, les commandes et le
7
+ * service le consultent tous, et le placer dans l'un d'eux ferait importer le
8
+ * migrateur par l'adapter. Une seule écriture du littéral : c'est d'elle que
9
+ * dépend la règle « une base volatile dérive son schéma » (`resolveDdlMode`).
10
+ */
11
+ const MEMORY_DATABASE = ":memory:";
12
+ //#endregion
13
+ export { MEMORY_DATABASE };