@nodefony/drizzle 10.0.0-alpha.8 → 10.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +19 -19
  2. package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorate.js +1 -1
  3. package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorateMetadata.js +1 -1
  4. package/dist/index.js +9 -6
  5. package/dist/nodefony/command/migrateShared.js +17 -1
  6. package/dist/nodefony/command/orm-generate.js +14 -10
  7. package/dist/nodefony/command/orm-migrate-baseline.js +5 -2
  8. package/dist/nodefony/command/orm-reset.js +1 -0
  9. package/dist/nodefony/config/defineModuleConfig.js +23 -7
  10. package/dist/nodefony/entity/userTable.js +4 -1
  11. package/dist/nodefony/registerStores.js +4 -28
  12. package/dist/nodefony/service/DrizzleService.js +42 -16
  13. package/dist/nodefony/src/DrizzleAuditStore.js +14 -2
  14. package/dist/nodefony/src/DrizzleIdempotencyStore.js +14 -2
  15. package/dist/nodefony/src/DrizzleTokenStore.js +13 -1
  16. package/dist/nodefony/src/DrizzleTotpSecretStore.js +13 -1
  17. package/dist/nodefony/src/DrizzleWebAuthnCredentialStore.js +13 -1
  18. package/dist/nodefony/src/DrizzleWebhookStore.js +13 -1
  19. package/dist/nodefony/src/SessionStorage.js +8 -0
  20. package/dist/nodefony/src/connectorTarget.js +1 -1
  21. package/dist/nodefony/src/frameworkConnector.js +29 -0
  22. package/dist/nodefony/src/memoryDatabase.js +13 -0
  23. package/dist/nodefony/src/migrator/DrizzleMigrator.js +3 -3
  24. package/dist/nodefony/src/migrator/adopt.js +1 -1
  25. package/dist/nodefony/src/migrator/appSchema.js +44 -1
  26. package/dist/nodefony/src/migrator/destructive.js +2 -3
  27. package/dist/nodefony/src/migrator/drivers/mysqlDriver.js +1 -1
  28. package/dist/nodefony/src/migrator/drivers/postgresDriver.js +1 -1
  29. package/dist/nodefony/src/migrator/drivers/sqliteDriver.js +1 -0
  30. package/dist/nodefony/src/migrator/explain.js +7 -10
  31. package/dist/nodefony/src/migrator/history.js +25 -2
  32. package/dist/nodefony/src/migrator/kit.js +41 -6
  33. package/dist/nodefony/src/migrator/paths.js +55 -1
  34. package/dist/nodefony/src/migrator/rebuildCopy.js +6 -6
  35. package/dist/nodefony/src/migrator/refusals.js +50 -1
  36. package/dist/nodefony/src/migrator/resolve.js +59 -9
  37. package/dist/nodefony/src/migrator/sources.js +13 -8
  38. package/dist/nodefony/src/orm-core/DrizzleOrm.js +51 -30
  39. package/dist/nodefony/src/orm-core/DrizzleRepository.js +38 -26
  40. package/dist/nodefony/src/queryKit.js +5 -5
  41. package/dist/nodefony/src/safeTarget.js +1 -0
  42. package/dist/types/nodefony/command/migrateShared.d.ts +10 -0
  43. package/dist/types/nodefony/config/defineModuleConfig.d.ts +5 -1
  44. package/dist/types/nodefony/entity/userTable.d.ts +46 -3
  45. package/dist/types/nodefony/registerStores.d.ts +4 -3
  46. package/dist/types/nodefony/src/DrizzleAuditStore.d.ts +6 -0
  47. package/dist/types/nodefony/src/DrizzleIdempotencyStore.d.ts +8 -1
  48. package/dist/types/nodefony/src/DrizzleTokenStore.d.ts +6 -0
  49. package/dist/types/nodefony/src/DrizzleTotpSecretStore.d.ts +6 -0
  50. package/dist/types/nodefony/src/DrizzleUserRepository.d.ts +1 -1
  51. package/dist/types/nodefony/src/DrizzleWebAuthnCredentialStore.d.ts +6 -0
  52. package/dist/types/nodefony/src/DrizzleWebhookStore.d.ts +6 -0
  53. package/dist/types/nodefony/src/SessionStorage.d.ts +6 -0
  54. package/dist/types/nodefony/src/connectorTarget.d.ts +1 -1
  55. package/dist/types/nodefony/src/frameworkConnector.d.ts +24 -0
  56. package/dist/types/nodefony/src/memoryDatabase.d.ts +10 -0
  57. package/dist/types/nodefony/src/migrator/DrizzleMigrator.d.ts +4 -4
  58. package/dist/types/nodefony/src/migrator/appSchema.d.ts +25 -0
  59. package/dist/types/nodefony/src/migrator/destructive.d.ts +1 -1
  60. package/dist/types/nodefony/src/migrator/drivers/index.d.ts +2 -2
  61. package/dist/types/nodefony/src/migrator/explain.d.ts +9 -9
  62. package/dist/types/nodefony/src/migrator/kit.d.ts +15 -0
  63. package/dist/types/nodefony/src/migrator/name.d.ts +1 -1
  64. package/dist/types/nodefony/src/migrator/paths.d.ts +35 -0
  65. package/dist/types/nodefony/src/migrator/refusals.d.ts +36 -1
  66. package/dist/types/nodefony/src/migrator/resolve.d.ts +44 -1
  67. package/dist/types/nodefony/src/migrator/types.d.ts +2 -2
  68. package/dist/types/nodefony/src/orm-core/DrizzleOrm.d.ts +13 -2
  69. package/dist/types/nodefony/src/orm-core/DrizzleRepository.d.ts +1 -1
  70. package/dist/types/nodefony/src/orm-core/ddlPlan.d.ts +2 -2
  71. package/dist/types/nodefony/src/queryKit.d.ts +9 -9
  72. package/dist/types/nodefony/src/safeTarget.d.ts +2 -2
  73. package/docs/index.md +31 -28
  74. package/docs/migrations.md +39 -3
  75. package/package.json +19 -20
@@ -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
  *
@@ -45,7 +45,7 @@ function defaultConnectorFilename(kernel, name) {
45
45
  * @returns les coordonnées, avec le fichier SQLite TOUJOURS résolu.
46
46
  */
47
47
  function resolveConnectorTarget(kernel, name, cfg) {
48
- const dialect = cfg.dialect ?? "sqlite";
48
+ const dialect = cfg.dialect;
49
49
  if (dialect !== "sqlite") return {
50
50
  dialect,
51
51
  url: cfg.url
@@ -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 };
@@ -376,7 +376,7 @@ var DrizzleMigrator = class {
376
376
  * @throws MigrationVerdictError portant le verdict structuré.
377
377
  */
378
378
  #assertApplicable(plan, files, options) {
379
- const first = plan.failed[0];
379
+ const first = plan.failed.at(0);
380
380
  if (first) throw this.#verdict("NF_MIGRATE_FAILED_MARKER", {
381
381
  source: first.source,
382
382
  tag: first.tag,
@@ -386,7 +386,7 @@ var DrizzleMigrator = class {
386
386
  },
387
387
  actions: this.#recoveryActions()
388
388
  }, `La migration « ${first.tag} » (source « ${first.source} ») a échoué ou n'a jamais fini. Inspecter la base, puis réparer avant de reprendre — une reprise aveugle n'est jamais sûre.\n\n${REPRISE_SANS_DESTRUCTION}`);
389
- const drift = plan.drifted[0];
389
+ const drift = plan.drifted.at(0);
390
390
  if (drift) throw this.#verdict("NF_MIGRATE_HASH_MISMATCH", {
391
391
  source: drift.source,
392
392
  tag: drift.tag,
@@ -411,7 +411,7 @@ var DrizzleMigrator = class {
411
411
  ]
412
412
  }]
413
413
  }, `Le fichier de la migration « ${drift.tag} » (source « ${drift.source} ») a changé depuis son application. Une migration déjà jouée est immuable : écrire une NOUVELLE migration, ou assumer le ré-alignement.`);
414
- const gone = plan.missing[0];
414
+ const gone = plan.missing.at(0);
415
415
  if (gone && options.ignoreMissing !== true) throw this.#verdict("NF_MIGRATE_MISSING_FILE", {
416
416
  source: gone.source,
417
417
  tag: gone.tag,
@@ -113,7 +113,7 @@ async function snapshotTables(outDir) {
113
113
  try {
114
114
  const raw = await fs.readFile(path.join(outDir, "meta", "0000_snapshot.json"), "utf8");
115
115
  const snapshot = JSON.parse(raw);
116
- return Object.values(snapshot.tables ?? {}).map((t, i) => t.name ?? Object.keys(snapshot.tables ?? {})[i] ?? "");
116
+ return Object.entries(snapshot.tables ?? {}).map(([key, t]) => t.name ?? key);
117
117
  } catch {
118
118
  return [];
119
119
  }
@@ -1,4 +1,5 @@
1
1
  import { FORMAT_MARKER } from "./types.js";
2
+ import { ownsSharedMigrations } from "../frameworkConnector.js";
2
3
  import "./kit.js";
3
4
  import { registerHooks } from "node:module";
4
5
  import { entityRegistry } from "@nodefony/orm-core";
@@ -410,5 +411,47 @@ function registeredTables(connector) {
410
411
  }
411
412
  return out;
412
413
  }
414
+ /**
415
+ * Tables d'un connecteur parmi celles que les fichiers fournissent — le
416
+ * partage qui fait qu'une migration ne décrit que SA base.
417
+ *
418
+ * Une table appartient au connecteur de l'entité qui la déclare (le REGISTRE
419
+ * le sait, un fichier non). Une table qu'aucune entité ne réclame reste au
420
+ * connecteur du framework, comme avant que les connecteurs secondaires aient
421
+ * leurs migrations. Sans ce partage, la table d'une entité secondaire entrait
422
+ * AUSSI dans les migrations du framework : créée dans une base qui ne s'en sert
423
+ * jamais, et proposée à la suppression le jour où elle n'y figure plus.
424
+ *
425
+ * @param tables - tables fournies par les fichiers de l'application.
426
+ * @param connector - connecteur visé.
427
+ * @param owners - `table → connecteurs` qui la déclarent ; par défaut, le registre.
428
+ * @returns les tables de ce connecteur, dans l'ordre reçu.
429
+ */
430
+ function tablesOfConnector(tables, connector, owners = registeredOwners()) {
431
+ return tables.filter((t) => {
432
+ const claimed = owners.get(t.tableName);
433
+ return claimed === void 0 ? ownsSharedMigrations(connector) : claimed.has(connector);
434
+ });
435
+ }
436
+ /**
437
+ * Pour chaque table enregistrée, les connecteurs dont une entité la déclare.
438
+ *
439
+ * @returns la table de correspondance, lue dans le registre des entités.
440
+ */
441
+ function registeredOwners() {
442
+ const owners = /* @__PURE__ */ new Map();
443
+ for (const entity of entityRegistry.list()) {
444
+ const schema = entity.schema;
445
+ if (!is(schema, Table)) continue;
446
+ const table = getTableName(schema);
447
+ let set = owners.get(table);
448
+ if (set === void 0) {
449
+ set = /* @__PURE__ */ new Set();
450
+ owners.set(table, set);
451
+ }
452
+ set.add(entity.connector);
453
+ }
454
+ return owners;
455
+ }
413
456
  //#endregion
414
- export { collectTables, dialectOf, entityFilesOf, importSpecifier, missingProviders, postgresSchemaOf, registeredTables, usurpedTables, writeCustomMigration, writeKitConfig, writeSchemaModule };
457
+ export { collectTables, dialectOf, entityFilesOf, importSpecifier, missingProviders, postgresSchemaOf, registeredOwners, registeredTables, tablesOfConnector, usurpedTables, writeCustomMigration, writeKitConfig, writeSchemaModule };
@@ -103,8 +103,7 @@ function rebuiltTables(sql) {
103
103
  const out = /* @__PURE__ */ new Set();
104
104
  const renames = joint.matchAll(new RegExp(`\\bALTER\\s+TABLE\\s+${IDENT}\\s+RENAME\\s+TO\\s+${IDENT}`, "gi"));
105
105
  for (const rename of renames) {
106
- const staging = rename[1];
107
- const target = rename[2];
106
+ const [, staging, target] = rename;
108
107
  if (staging === void 0 || target === void 0) continue;
109
108
  const created = new RegExp(`\\bCREATE\\s+TABLE\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?[\`"']?${staging}[\`"']?\\b`, "i");
110
109
  const copied = new RegExp(`\\bINSERT\\s+INTO\\s+[\`"']?${staging}[\`"']?[\\s\\S]{0,4000}?\\bFROM\\s+[\`"']?${target}[\`"']?\\b`, "i");
@@ -124,7 +123,7 @@ function rebuiltTables(sql) {
124
123
  * @returns les noms supprimés, en minuscules.
125
124
  */
126
125
  function droppedTables(sql) {
127
- return [...asSql(sql).matchAll(new RegExp(`\\bDROP\\s+TABLE\\s+(?:IF\\s+EXISTS\\s+)?${IDENT}`, "gi"))].map((m) => m[1]).filter((n) => n !== void 0).map((n) => n.toLowerCase());
126
+ return [...asSql(sql).matchAll(new RegExp(`\\bDROP\\s+TABLE\\s+(?:IF\\s+EXISTS\\s+)?${IDENT}`, "gi"))].flatMap(([, name]) => name === void 0 ? [] : [name.toLowerCase()]);
128
127
  }
129
128
  /**
130
129
  * L'instruction fait-elle partie de la mécanique d'une reconstruction ?
@@ -68,7 +68,7 @@ var MysqlMigrationDriver = class {
68
68
  }
69
69
  const cx = await createConnection(this.#url);
70
70
  cx.on("error", (error) => {
71
- this.#lost = error?.message ?? String(error);
71
+ this.#lost = error instanceof Error ? error.message : String(error);
72
72
  });
73
73
  this.#lost = null;
74
74
  this.#cxOrNull = cx;
@@ -56,7 +56,7 @@ var PostgresMigrationDriver = class {
56
56
  }
57
57
  const client = new ClientCtor({ connectionString: this.#url });
58
58
  client.on("error", (error) => {
59
- this.#lost = error?.message ?? String(error);
59
+ this.#lost = error instanceof Error ? error.message : String(error);
60
60
  });
61
61
  await client.connect();
62
62
  this.#lost = null;
@@ -1,5 +1,6 @@
1
1
  import { HISTORY_TABLE } from "../types.js";
2
2
  import { schemaReader } from "../catalog.js";
3
+ import "../../memoryDatabase.js";
3
4
  //#region nodefony/src/migrator/drivers/sqliteDriver.ts
4
5
  /**
5
6
  * Pilote SQLite de l'applicateur — connexion `better-sqlite3` dédiée.
@@ -309,18 +309,15 @@ function summaryOf(plan, verdict, divergence = null) {
309
309
  }
310
310
  case "drift": {
311
311
  const parts = [];
312
- if (plan.drifted.length > 0) {
313
- const d = plan.drifted[0];
314
- parts.push(`le fichier « ${d.source}/${d.tag} » a été modifié APRÈS avoir été appliqué (son empreinte a changé)`);
315
- }
316
- if (plan.missing.length > 0) {
317
- const m = plan.missing[0];
318
- parts.push(`la migration « ${m.source}/${m.tag} » est enregistrée comme appliquée mais son fichier n'existe plus`);
319
- }
312
+ const [d] = plan.drifted;
313
+ if (d !== void 0) parts.push(`le fichier « ${d.source}/${d.tag} » a été modifié APRÈS avoir été appliqué (son empreinte a changé)`);
314
+ const [m] = plan.missing;
315
+ if (m !== void 0) parts.push(`la migration « ${m.source}/${m.tag} » est enregistrée comme appliquée mais son fichier n'existe plus`);
320
316
  return `Le connecteur « ${c} » ne concorde plus avec son historique : ${parts.join(" ; ")}.`;
321
317
  }
322
318
  case "failed": {
323
- const f = plan.failed[0];
319
+ const [f] = plan.failed;
320
+ if (f === void 0) return `Le connecteur « ${c} » porte une migration qui n'a pas abouti.`;
324
321
  const when = f.startedAt ? new Date(f.startedAt).toISOString() : "à une date inconnue";
325
322
  const why = f.error ? ` (erreur : ${f.error})` : "";
326
323
  return `Le connecteur « ${c} » porte ${plan.failed.length} migration${plan.failed.length > 1 ? "s" : ""} qui n'a pas abouti : « ${f.source}/${f.tag} », commencée le ${when}${why}.`;
@@ -516,7 +513,7 @@ function describeDiscovery(facts, style) {
516
513
  */
517
514
  function renderRefusal(verdict, message, style, ddl) {
518
515
  const enMode = ddl ? refusalInMode(verdict.code, ddl, verdict.connector) : null;
519
- const meaning = enMode?.meaning ?? REFUSAL_MEANING[verdict.code] ?? "";
516
+ const meaning = enMode?.meaning ?? REFUSAL_MEANING[verdict.code];
520
517
  const actions = enMode?.actions ?? verdict.nextActions;
521
518
  return `${style.red(style.bold("Migration refusée"))} ${style.dim(`[${verdict.code}]`)}\n\n` + renderBlocks(style, message, meaning, actions);
522
519
  }
@@ -98,8 +98,8 @@ async function readHistory(driver) {
98
98
  finishedAt: row.finished_at === null ? null : Number(row.finished_at),
99
99
  executionMs: row.execution_ms === null ? null : Number(row.execution_ms),
100
100
  success: toBoolean(row.success),
101
- error: row.error === null ? null : String(row.error),
102
- appliedBy: row.applied_by === null ? null : String(row.applied_by)
101
+ error: toNullableText(row.error),
102
+ appliedBy: toNullableText(row.applied_by)
103
103
  }));
104
104
  }
105
105
  /**
@@ -194,6 +194,29 @@ async function forgetEntries(driver, entries) {
194
194
  return removed;
195
195
  }
196
196
  /**
197
+ * Décode une colonne texte NULLABLE de l'historique.
198
+ *
199
+ * Une colonne texte rend une chaîne ; un `Buffer` (collation binaire MySQL) est
200
+ * lu en UTF-8, comme le faisait `String()`. Un objet inattendu est sérialisé en
201
+ * JSON plutôt que réduit à `[object Object]`, qui effacerait la cause.
202
+ *
203
+ * @param value - valeur brute rendue par le driver.
204
+ * @returns le texte, ou `null` si la colonne est nulle.
205
+ */
206
+ function toNullableText(value) {
207
+ if (value === null) return null;
208
+ switch (typeof value) {
209
+ case "string": return value;
210
+ case "number":
211
+ case "bigint":
212
+ case "boolean":
213
+ case "undefined": return String(value);
214
+ case "symbol": return value.toString();
215
+ case "function": return "function";
216
+ default: return Buffer.isBuffer(value) ? value.toString("utf8") : JSON.stringify(value);
217
+ }
218
+ }
219
+ /**
197
220
  * Normalise un booléen tel que le rend la base.
198
221
  *
199
222
  * PostgreSQL rend `true`, SQLite et MySQL rendent `1` — et un `1` textuel
@@ -1,5 +1,5 @@
1
1
  import { FORMAT_MARKER } from "./types.js";
2
- import { MigrationToolError, generationFailed, generationNeedsAnswer, generationToolMissing, introspectFailed } from "./refusals.js";
2
+ import { MigrationToolError, generationFailed, generationNeedsAnswer, generationToolMissing, generationTopLevelAwait, introspectFailed } from "./refusals.js";
3
3
  import { droppedTables, rebuiltTables } from "./destructive.js";
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
@@ -69,8 +69,10 @@ function runGenerate({ cwd, configRel, name, label, regenerateCommand }) {
69
69
  cwd,
70
70
  encoding: "utf8"
71
71
  });
72
- const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
72
+ const output = toolOutput(result);
73
73
  if (result.status !== 0 || !generationHappened(output)) {
74
+ const awaited = topLevelAwaitFailure(output);
75
+ if (awaited !== null) throw new MigrationToolError(generationTopLevelAwait(label, awaited.file, awaited.line, regenerateCommand ?? `nodefony orm:generate --name ${name}`));
74
76
  if (isInteractivePromptFailure(output)) throw new MigrationToolError(generationNeedsAnswer(label, regenerateCommand ?? `nodefony orm:generate --name ${name}`));
75
77
  throw new MigrationToolError(generationFailed(label, result.status, output));
76
78
  }
@@ -105,7 +107,7 @@ function runIntrospect({ cwd, configRel, label }) {
105
107
  cwd,
106
108
  encoding: "utf8"
107
109
  });
108
- const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
110
+ const output = toolOutput(result);
109
111
  if (result.status !== 0) throw new MigrationToolError(introspectFailed(label, result.status, output));
110
112
  return output;
111
113
  }
@@ -226,9 +228,9 @@ const BLOCKING_PATTERNS = [
226
228
  id: "colonne-neuve-puis-index-unique",
227
229
  pattern: /\bCREATE\s+UNIQUE\s+INDEX\b/i,
228
230
  detect: (code) => {
229
- const added = [...code.matchAll(/\bADD\s+(?:COLUMN\s+)?[`"']?(\w+)[`"']?/gi)].map((m) => m[1].toLowerCase());
231
+ const added = [...code.matchAll(/\bADD\s+(?:COLUMN\s+)?[`"']?(\w+)[`"']?/gi)].flatMap(([, name]) => name === void 0 ? [] : [name.toLowerCase()]);
230
232
  if (added.length === 0) return false;
231
- return [...code.matchAll(/\bCREATE\s+UNIQUE\s+INDEX\b[^;\n]*?\(([^)]*)\)/gi)].some((m) => m[1].split(",").map((c) => c.trim().replace(/[`"']/g, "").toLowerCase()).some((c) => added.includes(c)));
233
+ return [...code.matchAll(/\bCREATE\s+UNIQUE\s+INDEX\b[^;\n]*?\(([^)]*)\)/gi)].some(([, columns = ""]) => columns.split(",").map((c) => c.trim().replace(/[`"']/g, "").toLowerCase()).some((c) => added.includes(c)));
232
234
  },
233
235
  what: "ajoute une colonne ET pose son index UNIQUE dans la même migration : toutes les lignes déjà présentes reçoivent la même valeur, et l'index échoue sur la deuxième — cet enchaînement ne réussit que sur une table vide",
234
236
  todo: "séparer : ajouter la colonne sans contrainte d'unicité, remplir chaque ligne d'une valeur DISTINCTE (`nodefony orm:generate --custom`), puis poser l'index unique"
@@ -273,6 +275,25 @@ function isInteractivePromptFailure(output) {
273
275
  return /Interactive prompts require a TTY/i.test(output);
274
276
  }
275
277
  /**
278
+ * Reconnaît le refus de l'outil devant un `await` de premier niveau.
279
+ *
280
+ * C'est l'esbuild de `drizzle-kit` qui le détecte, en transpilant le fichier en
281
+ * CommonJS : sa ligne d'erreur nomme le fichier et la position. On la lit plutôt
282
+ * que d'analyser nous-mêmes les sources — l'outil qui refuse est aussi celui qui
283
+ * sait exactement où, sans faux positif. Fonction PURE, éprouvée sans process.
284
+ *
285
+ * @param output - sortie complète de l'outil.
286
+ * @returns le fichier et la ligne, ou `null` si l'échec est d'une autre nature.
287
+ */
288
+ function topLevelAwaitFailure(output) {
289
+ const plain = output.replace(/\u001B\[[0-9;]*m/g, "");
290
+ const [, file, line] = /(?:^|\n)[^\S\n]*(\S[^\n]*?\.[cm]?[jt]sx?):(\d+):\d+: ERROR: Top-level await is currently not supported with the \\?"cjs\\?" output format/u.exec(plain) ?? [];
291
+ return file === void 0 ? null : {
292
+ file,
293
+ line: Number(line)
294
+ };
295
+ }
296
+ /**
276
297
  * Pose le marqueur de format en tête des `.sql` d'un dossier qui ne l'ont pas.
277
298
  *
278
299
  * Écrit en fins de ligne `\n` quel que soit le système : le dépôt et le gabarit
@@ -295,5 +316,19 @@ function stampFormatMarker(dir) {
295
316
  }
296
317
  return stamped;
297
318
  }
319
+ /**
320
+ * Sortie complète (standard puis erreur) d'un `spawnSync`.
321
+ *
322
+ * Malgré leur type, `stdout` et `stderr` valent `null` quand le processus n'a
323
+ * pas pu démarrer (`ENOENT`, `EACCES`) : Node les tire alors d'un `output` nul.
324
+ *
325
+ * @param result - le retour de `spawnSync` en mode `encoding: "utf8"`.
326
+ * @returns les deux flux concaténés, vide si rien n'a été écrit.
327
+ */
328
+ function toolOutput(result) {
329
+ const stdout = result.stdout;
330
+ const stderr = result.stderr;
331
+ return `${stdout ?? ""}${stderr ?? ""}`;
332
+ }
298
333
  //#endregion
299
- export { FORMAT_MARKER, auditMigrationSql, generationHappened, isInteractivePromptFailure, resolveDrizzleKitBin, runGenerate, runIntrospect, stampFormatMarker };
334
+ export { FORMAT_MARKER, auditMigrationSql, generationHappened, isInteractivePromptFailure, resolveDrizzleKitBin, runGenerate, runIntrospect, stampFormatMarker, topLevelAwaitFailure };
@@ -1,3 +1,4 @@
1
+ import { ownsSharedMigrations } from "../frameworkConnector.js";
1
2
  import path from "node:path";
2
3
  import fs from "node:fs/promises";
3
4
  import { fileURLToPath } from "node:url";
@@ -15,6 +16,47 @@ const FRAMEWORK_RANK = 0;
15
16
  * celles des modules ; l'inverse n'arrive jamais.
16
17
  */
17
18
  const APP_RANK = 1e6;
19
+ /**
20
+ * Noms qui ne peuvent PAS désigner le dossier d'un connecteur secondaire : ce
21
+ * sont ceux des dossiers de dialecte (et du journal) que le connecteur du
22
+ * framework range déjà sous `migrations/`. Un connecteur `postgres` y lirait les
23
+ * migrations de `default` comme les siennes.
24
+ */
25
+ const RESERVED_MIGRATION_DIRS = [
26
+ "sqlite",
27
+ "postgres",
28
+ "mysql",
29
+ "meta"
30
+ ];
31
+ /**
32
+ * Le nom de ce connecteur collisionne-t-il avec un dossier de dialecte ?
33
+ *
34
+ * @param connector - nom du connecteur (clé de `connectors`).
35
+ * @returns `true` pour un connecteur SECONDAIRE nommé comme un dossier réservé.
36
+ */
37
+ function isReservedConnectorName(connector) {
38
+ return !ownsSharedMigrations(connector) && RESERVED_MIGRATION_DIRS.includes(connector);
39
+ }
40
+ /**
41
+ * Dossier des migrations PROPRES à un connecteur — la seule règle qui dit où
42
+ * elles vivent.
43
+ *
44
+ * Le connecteur du framework garde la racine (`migrations/<dialecte>`) : ses
45
+ * migrations décrivent la base du framework et des entités de l'application.
46
+ * Un connecteur secondaire reçoit son sous-dossier (`migrations/<connecteur>/
47
+ * <dialecte>`) : sa base ne reçoit que ce qui y est écrit, et celle du
48
+ * framework n'en voit rien — le dossier de dialecte qu'elle lit n'est pas le
49
+ * même.
50
+ *
51
+ * @param appDir - dossier de migrations de l'application (`appMigrationsDir`).
52
+ * @param connector - nom du connecteur.
53
+ * @returns le dossier parent des sous-dossiers de dialecte, ou `undefined` sans
54
+ * racine d'application, ou pour un nom réservé ({@link isReservedConnectorName}).
55
+ */
56
+ function connectorMigrationsDir(appDir, connector) {
57
+ if (appDir === void 0 || isReservedConnectorName(connector)) return;
58
+ return ownsSharedMigrations(connector) ? appDir : path.join(appDir, connector);
59
+ }
18
60
  /** Dossier de migrations, mémoïsé — la remontée ne se fait qu'une fois. */
19
61
  let cachedDir = null;
20
62
  /**
@@ -68,10 +110,22 @@ async function frameworkMigrationsDir() {
68
110
  *
69
111
  * @param appDir - dossier de migrations de l'application, s'il y en a un.
70
112
  * @param options.framework - `false` quand le module est data-only.
113
+ * @param options.connector - connecteur visé ; un connecteur secondaire ne
114
+ * reçoit que SES migrations ({@link connectorMigrationsDir}), jamais celles
115
+ * du framework. Omis : le registre complet (usage direct).
71
116
  * @returns le registre, prêt pour l'applicateur.
72
117
  */
73
118
  async function defaultMigrationSources(appDir, options = {}) {
74
119
  const sources = [];
120
+ if (options.connector !== void 0 && !ownsSharedMigrations(options.connector)) {
121
+ const own = connectorMigrationsDir(appDir, options.connector);
122
+ if (own !== void 0) sources.push({
123
+ name: "app",
124
+ dir: own,
125
+ rank: APP_RANK
126
+ });
127
+ return sources;
128
+ }
75
129
  if (options.framework !== false) sources.push({
76
130
  name: FRAMEWORK_SOURCE,
77
131
  dir: await frameworkMigrationsDir(),
@@ -85,4 +139,4 @@ async function defaultMigrationSources(appDir, options = {}) {
85
139
  return sources;
86
140
  }
87
141
  //#endregion
88
- export { APP_RANK, APP_SOURCE, FRAMEWORK_RANK, FRAMEWORK_SOURCE, defaultMigrationSources, frameworkMigrationsDir };
142
+ export { APP_RANK, APP_SOURCE, FRAMEWORK_RANK, FRAMEWORK_SOURCE, RESERVED_MIGRATION_DIRS, connectorMigrationsDir, defaultMigrationSources, frameworkMigrationsDir, isReservedConnectorName };
@@ -58,7 +58,6 @@ function repairRebuildCopy(sql, columnsOf) {
58
58
  const groups = args[args.length - 1];
59
59
  if (groups === void 0) return match;
60
60
  const { head, into, middle, select, tail, source } = groups;
61
- if (head === void 0 || into === void 0 || middle === void 0 || select === void 0 || tail === void 0 || source === void 0) return match;
62
61
  const known = columnsOf(source);
63
62
  if (known === null) return match;
64
63
  const intoColumns = splitColumns(into);
@@ -68,13 +67,14 @@ function repairRebuildCopy(sql, columnsOf) {
68
67
  const keptInto = [];
69
68
  const keptSelect = [];
70
69
  const removed = [];
71
- for (let i = 0; i < intoColumns.length; i++) {
72
- const target = bareName(intoColumns[i]);
73
- const read = bareName(selectColumns[i]);
70
+ for (const [i, intoColumn] of intoColumns.entries()) {
71
+ const selectColumn = selectColumns[i] ?? "";
72
+ const target = bareName(intoColumn);
73
+ const read = bareName(selectColumn);
74
74
  if (target === null || read === null) return match;
75
75
  if (carried.has(read)) {
76
- keptInto.push(intoColumns[i]);
77
- keptSelect.push(selectColumns[i]);
76
+ keptInto.push(intoColumn);
77
+ keptSelect.push(selectColumn);
78
78
  } else removed.push(`${source}.${read}`);
79
79
  }
80
80
  if (removed.length === 0) return match;