@nodefony/drizzle 10.0.0-alpha.7 → 10.0.0-alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/index.js +11 -7
- package/dist/nodefony/command/migrateShared.js +17 -1
- package/dist/nodefony/command/orm-generate.js +81 -13
- package/dist/nodefony/command/orm-migrate-baseline.js +5 -2
- package/dist/nodefony/command/orm-reset.js +1 -0
- package/dist/nodefony/config/defineModuleConfig.js +21 -5
- package/dist/nodefony/entity/userTable.js +3 -0
- package/dist/nodefony/registerStores.js +2 -26
- package/dist/nodefony/service/DrizzleService.js +19 -7
- package/dist/nodefony/src/DrizzleAuditStore.js +13 -1
- package/dist/nodefony/src/DrizzleIdempotencyStore.js +14 -2
- package/dist/nodefony/src/DrizzleTokenStore.js +13 -1
- package/dist/nodefony/src/DrizzleTotpSecretStore.js +13 -1
- package/dist/nodefony/src/DrizzleWebAuthnCredentialStore.js +13 -1
- package/dist/nodefony/src/DrizzleWebhookStore.js +13 -1
- package/dist/nodefony/src/SessionStorage.js +8 -0
- package/dist/nodefony/src/frameworkConnector.js +29 -0
- package/dist/nodefony/src/memoryDatabase.js +13 -0
- package/dist/nodefony/src/migrator/DrizzleMigrator.js +17 -1
- package/dist/nodefony/src/migrator/appSchema.js +44 -1
- package/dist/nodefony/src/migrator/drivers/sqliteDriver.js +1 -0
- package/dist/nodefony/src/migrator/explain.js +42 -1
- package/dist/nodefony/src/migrator/kit.js +23 -2
- package/dist/nodefony/src/migrator/paths.js +55 -1
- package/dist/nodefony/src/migrator/refusals.js +50 -1
- package/dist/nodefony/src/migrator/resolve.js +126 -6
- package/dist/nodefony/src/orm-core/DrizzleOrm.js +123 -22
- package/dist/nodefony/src/orm-core/ddlPlan.js +95 -0
- package/dist/nodefony/src/orm-core/index.js +2 -1
- package/dist/nodefony/src/orm-core/seedRow.js +65 -0
- package/dist/nodefony/src/safeTarget.js +1 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/nodefony/command/migrateShared.d.ts +10 -0
- package/dist/types/nodefony/command/orm-generate.d.ts +12 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +5 -1
- package/dist/types/nodefony/entity/userTable.d.ts +46 -3
- package/dist/types/nodefony/registerStores.d.ts +4 -3
- package/dist/types/nodefony/src/DrizzleAuditStore.d.ts +6 -0
- package/dist/types/nodefony/src/DrizzleIdempotencyStore.d.ts +8 -1
- package/dist/types/nodefony/src/DrizzleTokenStore.d.ts +6 -0
- package/dist/types/nodefony/src/DrizzleTotpSecretStore.d.ts +6 -0
- package/dist/types/nodefony/src/DrizzleWebAuthnCredentialStore.d.ts +6 -0
- package/dist/types/nodefony/src/DrizzleWebhookStore.d.ts +6 -0
- package/dist/types/nodefony/src/SessionStorage.d.ts +6 -0
- package/dist/types/nodefony/src/frameworkConnector.d.ts +24 -0
- package/dist/types/nodefony/src/memoryDatabase.d.ts +10 -0
- package/dist/types/nodefony/src/migrator/appSchema.d.ts +25 -0
- package/dist/types/nodefony/src/migrator/explain.d.ts +19 -0
- package/dist/types/nodefony/src/migrator/kit.d.ts +15 -0
- package/dist/types/nodefony/src/migrator/paths.d.ts +35 -0
- package/dist/types/nodefony/src/migrator/refusals.d.ts +36 -1
- package/dist/types/nodefony/src/migrator/resolve.d.ts +97 -1
- package/dist/types/nodefony/src/orm-core/DrizzleOrm.d.ts +11 -0
- package/dist/types/nodefony/src/orm-core/ddlPlan.d.ts +96 -0
- package/dist/types/nodefony/src/orm-core/index.d.ts +1 -0
- package/dist/types/nodefony/src/orm-core/seedRow.d.ts +23 -0
- package/docs/index.md +28 -25
- package/docs/migrations.md +69 -4
- package/package.json +19 -20
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { MIGRATE_URL_ENV, MigrationLockTimeoutError, MigrationVerdictError } from "./types.js";
|
|
2
|
+
import { alreadyInTargetState } from "./explain.js";
|
|
2
3
|
import { openMigrationDriver } from "./drivers/index.js";
|
|
3
4
|
import { deleteFailed, ensureHistorySchema, finishHistory, forgetEntries, insertHistory, readHistory } from "./history.js";
|
|
4
5
|
import "./paths.js";
|
|
@@ -51,6 +52,21 @@ const ERROR_MAX_LENGTH = 2e3;
|
|
|
51
52
|
* celle qu'il trouve seul est de recréer la base, ce qui emporte les données.
|
|
52
53
|
*/
|
|
53
54
|
const REPRISE_SANS_DESTRUCTION = "REPRENDRE ne demande PAS de recréer la base. Deux voies, selon ce qui a échoué : corriger le fichier de migration puis lever le marqueur (« orm:migrate:repair ») et reprendre — un fichier corrigé après un échec est rejoué tel quel, son empreinte n'est pas comparée ; ou, si la mise au point demande plusieurs essais, la faire sur une base d'essai en posant « " + MIGRATE_URL_ENV + " », qui détourne la commande vers une AUTRE base et laisse celle-ci intacte — sous PowerShell, poser la variable s'écrit « $env:" + MIGRATE_URL_ENV + " = \"…\" », « env » n'y existant pas. Écrire la migration suivante est toujours préférable à défaire ce qui est déjà appliqué.";
|
|
55
|
+
/**
|
|
56
|
+
* La troisième voie de reprise — celle qu'aucun message ne nommait.
|
|
57
|
+
*
|
|
58
|
+
* Les deux voies écrites jusqu'ici (corriger le fichier, ou travailler sur une
|
|
59
|
+
* base d'essai) supposent toutes deux que le fichier a tort. Quand le moteur
|
|
60
|
+
* répond « c'est déjà ainsi », ni l'une ni l'autre ne mène quelque part : le
|
|
61
|
+
* SQL est juste, la base est conforme, et rejouer redira la même chose. Sans
|
|
62
|
+
* cette phrase, la boucle `repair` → `migrate` → même refus n'a pas de sortie
|
|
63
|
+
* — et la seule qu'un agent trouve seul est de recréer la base.
|
|
64
|
+
*
|
|
65
|
+
* @param source - source de la migration (`app` ou `framework`).
|
|
66
|
+
* @param tag - identité de la migration en échec.
|
|
67
|
+
* @returns le paragraphe à insérer dans le verdict.
|
|
68
|
+
*/
|
|
69
|
+
const alreadyInTargetStateAdvice = (source, tag) => `Ce que dit ce message, c'est que la base est DÉJÀ dans l'état que cette migration vise : l'objet qu'elle crée existe, ou celui qu'elle retire n'a jamais existé sous ce nom. Corriger le fichier ne servira donc à rien, et le rejouer redira la même chose — c'est la boucle dans laquelle on tourne si on s'obstine. VÉRIFIE d'abord l'état réel (« nodefony inspect entities --json », ou le schéma de la table concernée) ; s'il correspond bien à ce que « ${source}/${tag} » voulait obtenir, la migration est SANS OBJET sur cette base : déclare-la appliquée plutôt que de la rejouer, avec « nodefony orm:migrate:baseline --up-to ${tag} ». Une cause courante en développement : la table a été créée par le schéma DÉRIVÉ du démarrage, qui pose les contraintes sans les nommer, là où une migration les nomme.`;
|
|
54
70
|
var DrizzleMigrator = class {
|
|
55
71
|
#options;
|
|
56
72
|
#now;
|
|
@@ -592,7 +608,7 @@ var DrizzleMigrator = class {
|
|
|
592
608
|
tag: file.tag,
|
|
593
609
|
facts: { error: truncate(cause.message) },
|
|
594
610
|
actions: this.#recoveryActions()
|
|
595
|
-
}, `La migration « ${file.tag} » (source « ${file.source} ») a échoué : ${truncate(cause.message)}\n\n${state}\n\nSon échec est INSCRIT : le prochain passage refusera de reprendre tant que le marqueur n'aura pas été levé, après inspection.\n\n
|
|
611
|
+
}, `La migration « ${file.tag} » (source « ${file.source} ») a échoué : ${truncate(cause.message)}\n\n${state}\n\nSon échec est INSCRIT : le prochain passage refusera de reprendre tant que le marqueur n'aura pas été levé, après inspection.\n\n` + (alreadyInTargetState(cause.message) ? `${alreadyInTargetStateAdvice(file.source, file.tag)}\n\n` : "") + REPRISE_SANS_DESTRUCTION);
|
|
596
612
|
}
|
|
597
613
|
/**
|
|
598
614
|
* Les gestes qui sortent d'une migration en échec — **sans toucher à la base**.
|
|
@@ -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 };
|
|
@@ -450,6 +450,8 @@ function renderStatus(report, style) {
|
|
|
450
450
|
if (report.sources.length === 0) out += ` ${style.dim("aucune source de migrations")}\n`;
|
|
451
451
|
if (report.divergence && !namesEverything(report.divergence)) out += renderDivergence(report.divergence, style);
|
|
452
452
|
out += `\n${renderBlocks(style, report.summary, meaningOf(report.verdict), report.nextActions)}`;
|
|
453
|
+
if (report.exitCode === EXIT.actionRequired) out += `\n${style.dim("code de sortie 1 — une action est requise, ce n'est pas un échec.")}\n`;
|
|
454
|
+
else if (report.exitCode === EXIT.error) out += `\n${style.dim("code de sortie 2 — la commande n'a pas pu travailler.")}\n`;
|
|
453
455
|
return out;
|
|
454
456
|
}
|
|
455
457
|
/** Combien d'écarts on déroule à l'écran avant de renvoyer au `--json`. */
|
|
@@ -561,5 +563,44 @@ const REFUSAL_MEANING = {
|
|
|
561
563
|
NF_MIGRATE_JOURNAL_MISMATCH: "Le journal d'une source annonce une migration dont le fichier n'est pas dans le dossier. La source est incohérente avec elle-même : une copie incomplète, un fichier ignoré par le gestionnaire de versions, ou un paquet publié sans ses migrations. Rien n'a été appliqué — l'applicateur ne devine jamais le contenu d'un fichier absent.",
|
|
562
564
|
NF_MIGRATE_UNKNOWN_SOURCE: "La source demandée n'est pas déclarée par cette application. Filtrer sur un nom inconnu ne touche aucune ligne et rend pourtant « rien à réparer » : le marqueur d'échec resterait en place, et la migration suivante échouerait pour la même raison."
|
|
563
565
|
};
|
|
566
|
+
/**
|
|
567
|
+
* Les moteurs, chacun dans sa langue, disant « c'est DÉJÀ ainsi ».
|
|
568
|
+
*
|
|
569
|
+
* Reconnaître ce cas change le conseil du tout au tout : la migration n'a pas
|
|
570
|
+
* échoué parce que la base est cassée, mais parce qu'elle est **déjà dans
|
|
571
|
+
* l'état visé**. Le geste n'est alors ni de corriger le SQL ni de recréer la
|
|
572
|
+
* base — c'est de déclarer la migration appliquée.
|
|
573
|
+
*/
|
|
574
|
+
const ALREADY_IN_TARGET_STATE = [
|
|
575
|
+
/\bno such (index|column|table|trigger|view)\b/iu,
|
|
576
|
+
/\b(table|index|trigger|view)\b.*\balready exists\b/iu,
|
|
577
|
+
/\bduplicate column name\b/iu,
|
|
578
|
+
/\balready exists\b/iu,
|
|
579
|
+
/\bdoes not exist\b/iu,
|
|
580
|
+
/\bduplicate key name\b/iu,
|
|
581
|
+
/\bcan't drop\b/iu,
|
|
582
|
+
/\bcheck that (column|it|the) .*exists\b/iu
|
|
583
|
+
];
|
|
584
|
+
/**
|
|
585
|
+
* L'échec vient-il de ce que la base est DÉJÀ dans l'état que la migration vise ?
|
|
586
|
+
*
|
|
587
|
+
* Le cas est fréquent et sa sortie n'était nommée nulle part : le message de
|
|
588
|
+
* reprise proposait de corriger le fichier ou de travailler sur une base
|
|
589
|
+
* d'essai, deux voies qui ne mènent à rien quand le fichier est juste et la
|
|
590
|
+
* base déjà conforme. Un agent placé devant ce refus tourne alors en rond
|
|
591
|
+
* — `repair` lève le marqueur, `migrate` rejoue, le moteur redit la même
|
|
592
|
+
* chose — puis finit par recréer la base, ce qui emporte les données.
|
|
593
|
+
*
|
|
594
|
+
* Constaté sur un agent tiers : un `DROP INDEX messages_content_unique` sur une
|
|
595
|
+
* base dont l'unicité avait été posée INLINE par le DDL de démarrage, donc sans
|
|
596
|
+
* index nommé. Trente-cinq minutes, deux bases détruites, la boucle brisée à la
|
|
597
|
+
* main.
|
|
598
|
+
*
|
|
599
|
+
* @param message - le message d'erreur rendu par le moteur.
|
|
600
|
+
* @returns `true` si l'échec dit « c'est déjà ainsi ».
|
|
601
|
+
*/
|
|
602
|
+
function alreadyInTargetState(message) {
|
|
603
|
+
return ALREADY_IN_TARGET_STATE.some((pattern) => pattern.test(message));
|
|
604
|
+
}
|
|
564
605
|
//#endregion
|
|
565
|
-
export { EXIT, MIGRATION_FORMAT_VERSION, action, buildReport, describeDiscovery, divergenceIsBlocking, exitCodeOf, isAheadOnly, meaningOf, refusalInMode, renderRefusal, renderStatus, styleFor, verdictOf };
|
|
606
|
+
export { EXIT, MIGRATION_FORMAT_VERSION, action, alreadyInTargetState, buildReport, describeDiscovery, divergenceIsBlocking, exitCodeOf, isAheadOnly, meaningOf, refusalInMode, renderRefusal, renderStatus, styleFor, verdictOf };
|
|
@@ -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";
|
|
@@ -71,6 +71,8 @@ function runGenerate({ cwd, configRel, name, label, regenerateCommand }) {
|
|
|
71
71
|
});
|
|
72
72
|
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
|
|
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
|
}
|
|
@@ -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 m = /(?:^|\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 m ? {
|
|
292
|
+
file: m[1],
|
|
293
|
+
line: Number(m[2])
|
|
294
|
+
} : null;
|
|
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
|
|
@@ -296,4 +317,4 @@ function stampFormatMarker(dir) {
|
|
|
296
317
|
return stamped;
|
|
297
318
|
}
|
|
298
319
|
//#endregion
|
|
299
|
-
export { FORMAT_MARKER, auditMigrationSql, generationHappened, isInteractivePromptFailure, resolveDrizzleKitBin, runGenerate, runIntrospect, stampFormatMarker };
|
|
320
|
+
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 };
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { MIGRATE_URL_ENV } from "./types.js";
|
|
2
2
|
import { action } from "./explain.js";
|
|
3
|
+
import { FRAMEWORK_CONNECTOR } from "../frameworkConnector.js";
|
|
4
|
+
import { RESERVED_MIGRATION_DIRS } from "./paths.js";
|
|
3
5
|
import { knownConnectors } from "./resolve.js";
|
|
4
6
|
//#region nodefony/src/migrator/refusals.ts
|
|
5
7
|
/**
|
|
@@ -79,6 +81,30 @@ function generationNeedsAnswer(label, replay) {
|
|
|
79
81
|
};
|
|
80
82
|
}
|
|
81
83
|
/**
|
|
84
|
+
* Un fichier lu par l'outil de génération porte un `await` de premier niveau.
|
|
85
|
+
*
|
|
86
|
+
* Nodefony est ESM, et l'application démarre très bien avec ce fichier. Mais
|
|
87
|
+
* l'outil qui écrit les migrations (`drizzle-kit`) relit les fichiers de schéma
|
|
88
|
+
* HORS de la chaîne de l'application, en les transpilant en CommonJS — où un
|
|
89
|
+
* `await` de premier niveau n'existe pas. Sans ce refus, l'utilisateur recevait
|
|
90
|
+
* une pile d'appels d'esbuild qui ne nomme ni l'outil ni le remède.
|
|
91
|
+
*
|
|
92
|
+
* @param label - ce qui était généré, tel qu'on le cite à l'utilisateur.
|
|
93
|
+
* @param file - fichier fautif, tel que l'outil l'a nommé.
|
|
94
|
+
* @param line - ligne de l'`await`.
|
|
95
|
+
* @param replay - la commande à relancer une fois le fichier corrigé.
|
|
96
|
+
* @returns le refus, prêt pour la ligne de commande comme pour l'écran.
|
|
97
|
+
*/
|
|
98
|
+
function generationTopLevelAwait(label, file, line, replay) {
|
|
99
|
+
return {
|
|
100
|
+
code: "NF_GENERATE_TOP_LEVEL_AWAIT",
|
|
101
|
+
summary: `${file}:${line} — un \`await\` de premier niveau empêche de générer ${label} ; rien n'a été écrit.`,
|
|
102
|
+
meaning: "L'application démarre avec ce fichier : elle est ESM. Mais l'outil qui écrit les migrations (drizzle-kit) relit les fichiers de schéma de son côté, en CommonJS, où un `await` de premier niveau n'existe pas. Un fichier d'entité DÉCLARE un schéma : ce qui se décide à l'exécution (choisir une entité selon l'infrastructure, importer un module à la demande) se place dans l'`index.ts` du module — `@entities([...])` accepte une liste calculée. La base n'est pas en cause : elle n'a pas été interrogée.",
|
|
103
|
+
nextActions: [action(replay)],
|
|
104
|
+
exitCode: 2
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
82
108
|
* L'outil de génération s'est arrêté, et ce qu'il a dit est remonté TEL QUEL.
|
|
83
109
|
*
|
|
84
110
|
* 🔴 Ne JAMAIS remplacer sa sortie par une hypothèse. Le fourre-tout des
|
|
@@ -176,6 +202,29 @@ function notConfigured(connector, driver) {
|
|
|
176
202
|
};
|
|
177
203
|
}
|
|
178
204
|
/**
|
|
205
|
+
* Écrire une migration pour un connecteur secondaire dont le NOM est celui d'un
|
|
206
|
+
* dossier de dialecte — refusé.
|
|
207
|
+
*
|
|
208
|
+
* Un connecteur secondaire range ses migrations sous `migrations/<connecteur>/`,
|
|
209
|
+
* à côté des dossiers de dialecte du connecteur du framework. Nommé `postgres`,
|
|
210
|
+
* son dossier SERAIT celui des migrations PostgreSQL du framework : il les
|
|
211
|
+
* lirait comme les siennes, et les siennes seraient appliquées à la base du
|
|
212
|
+
* framework. On le refuse en nommant la collision, plutôt que de le découvrir
|
|
213
|
+
* au premier déploiement.
|
|
214
|
+
*
|
|
215
|
+
* @param connector - connecteur demandé.
|
|
216
|
+
* @returns le refus, prêt pour la ligne de commande comme pour l'écran.
|
|
217
|
+
*/
|
|
218
|
+
function reservedConnectorName(connector) {
|
|
219
|
+
return {
|
|
220
|
+
code: "NF_MIGRATE_SECONDARY_CONNECTOR",
|
|
221
|
+
summary: `Le connecteur « ${connector} » porte le nom d'un dossier réservé (${RESERVED_MIGRATION_DIRS.join(", ")}) : ses migrations ne peuvent pas avoir de dossier propre. Rien n'a été écrit.`,
|
|
222
|
+
meaning: `Les migrations d'un connecteur secondaire vivent sous \`migrations/<connecteur>/<dialecte>\`, à côté des dossiers de dialecte du connecteur « ${FRAMEWORK_CONNECTOR} ». Le dossier \`migrations/${connector}\` est déjà l'un d'eux : les deux connecteurs y liraient les migrations de l'autre, et chacune serait appliquée à la mauvaise base. Renomme le connecteur dans \`connectors\` (par exemple « ${connector}_data ») : son nom ne sert qu'à le désigner.`,
|
|
223
|
+
nextActions: [action("nodefony inspect config --json")],
|
|
224
|
+
exitCode: 2
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
179
228
|
* Traduit une résolution qui n'est PAS `ready` en refus lisible.
|
|
180
229
|
*
|
|
181
230
|
* @param wanted - nom demandé par l'appelant.
|
|
@@ -222,4 +271,4 @@ function describeResolutionRefusal(wanted, resolution, config) {
|
|
|
222
271
|
};
|
|
223
272
|
}
|
|
224
273
|
//#endregion
|
|
225
|
-
export { MigrationToolError, describeResolutionRefusal, formatToolOutput, generationFailed, generationNeedsAnswer, generationToolMissing, introspectFailed, moduleAbsent, notConfigured };
|
|
274
|
+
export { MigrationToolError, describeResolutionRefusal, formatToolOutput, generationFailed, generationNeedsAnswer, generationToolMissing, generationTopLevelAwait, introspectFailed, moduleAbsent, notConfigured, reservedConnectorName };
|