@nodefony/drizzle 10.0.0-alpha.6 → 10.0.0-alpha.8
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/README.md +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.149.0 → _@oxc-project_runtime@0.150.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.149.0 → _@oxc-project_runtime@0.150.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/index.js +4 -3
- package/dist/nodefony/command/orm-generate.js +70 -6
- package/dist/nodefony/service/DrizzleService.js +2 -2
- package/dist/nodefony/src/migrator/DrizzleMigrator.js +17 -1
- package/dist/nodefony/src/migrator/destructive.js +78 -19
- package/dist/nodefony/src/migrator/explain.js +42 -1
- package/dist/nodefony/src/migrator/kit.js +5 -0
- package/dist/nodefony/src/migrator/resolve.js +74 -4
- package/dist/nodefony/src/orm-core/DrizzleOrm.js +121 -21
- 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/types/index.d.ts +1 -1
- package/dist/types/nodefony/command/orm-generate.d.ts +12 -0
- package/dist/types/nodefony/src/migrator/destructive.d.ts +36 -0
- package/dist/types/nodefony/src/migrator/explain.d.ts +19 -0
- package/dist/types/nodefony/src/migrator/resolve.d.ts +54 -1
- 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/migrations.md +30 -1
- package/package.json +15 -15
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Adapter [Drizzle ORM](https://orm.drizzle.team/) pour Nodefony, branché sur
|
|
|
11
11
|
## Installation
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
npm install @nodefony/drizzle drizzle-orm better-sqlite3
|
|
14
|
+
npm install @nodefony/drizzle@alpha drizzle-orm better-sqlite3
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
## Utilisation comme module Nodefony (bootable)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.150.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.
|
|
1
|
+
//#region \0@oxc-project+runtime@0.150.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
|
@@ -13,6 +13,7 @@ import { appMigrationsDir, buildMigrator, knownConnectors, readMigrationEnv, res
|
|
|
13
13
|
import { DrizzleRepository } from "./nodefony/src/orm-core/DrizzleRepository.js";
|
|
14
14
|
import { DrizzleTransaction } from "./nodefony/src/orm-core/DrizzleTransaction.js";
|
|
15
15
|
import { DrizzleOrm } from "./nodefony/src/orm-core/DrizzleOrm.js";
|
|
16
|
+
import { seedEntityRow } from "./nodefony/src/orm-core/seedRow.js";
|
|
16
17
|
import "./nodefony/src/orm-core/index.js";
|
|
17
18
|
import DrizzleService from "./nodefony/service/DrizzleService.js";
|
|
18
19
|
import { TOKEN_ENTITY_NAMES, accessTokenTable, createAccessTokenTable, createDeniedJtiTable, createSubjectRevocationTable, createTokenEntities, deniedJtiTable, registerTokenEntities, subjectRevocationTable } from "./nodefony/entity/tokenEntity.js";
|
|
@@ -36,8 +37,8 @@ import OrmMigrateStatus from "./nodefony/command/orm-migrate-status.js";
|
|
|
36
37
|
import OrmMigrateBaseline from "./nodefony/command/orm-migrate-baseline.js";
|
|
37
38
|
import OrmMigrateRepair from "./nodefony/command/orm-migrate-repair.js";
|
|
38
39
|
import OrmReset from "./nodefony/command/orm-reset.js";
|
|
39
|
-
import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.
|
|
40
|
-
import __decorate from "./_virtual/_@oxc-project_runtime@0.
|
|
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
42
|
import SessionStorage from "./nodefony/src/SessionStorage.js";
|
|
42
43
|
import "./nodefony/src/migrator/index.js";
|
|
43
44
|
import { DrizzleUserRepository } from "./nodefony/src/DrizzleUserRepository.js";
|
|
@@ -102,4 +103,4 @@ let Drizzle = class Drizzle extends Module {
|
|
|
102
103
|
Drizzle = __decorate([services([DrizzleService]), __decorateMetadata("design:paramtypes", [typeof Kernel === "undefined" ? Object : Kernel])], Drizzle);
|
|
103
104
|
var drizzle_default = Drizzle;
|
|
104
105
|
//#endregion
|
|
105
|
-
export { APP_RANK, APP_SOURCE, AUDIT_ENTITY_NAMES, DDL_MODES, DEFAULT_LOCK_TIMEOUT_MS, DIVERGENCE_MODES, DrizzleAuditStore, DrizzleIdempotencyStore, DrizzleMigrator, DrizzleOrm, DrizzleRepository, DrizzleService, DrizzleTokenStore, DrizzleTotpSecretStore, DrizzleTransaction, DrizzleUserRepository, DrizzleWebAuthnCredentialStore, DrizzleWebhookStore, EXIT, FORMAT_MARKER, FRAMEWORK_CONNECTOR, FRAMEWORK_RANK, FRAMEWORK_SOURCE, HISTORY_TABLE, IDEMPOTENCY_ENTITY_NAME, MIGRATE_URL_ENV, MIGRATION_CHECK_MODES, MIGRATION_FORMAT_VERSION, MYSQL_LOCK_NAME_SQL, MigrationVerdictError, PG_LOCK_KEY, SESSION_CONNECTOR, SESSION_ENTITY_NAME, SessionStorage, TOKEN_ENTITY_NAMES, TOTP_SECRET_ENTITY, WEBAUTHN_CREDENTIAL_ENTITY, WEBHOOK_ENDPOINT_ENTITY, accessTokenTable, action, appMigrationsDir, auditEventTable, buildMigrator, buildReport, createAccessTokenTable, createAuditEntities, createAuditEventTable, createDeniedJtiTable, createIdempotencyEntities, createIdempotencyTable, createSessionEntity, createSessionTable, createSubjectRevocationTable, createTokenEntities, createTotpSecretEntity, createTotpSecretTable, createUserEntity, createUserTable, createWebAuthnCredentialEntity, createWebAuthnCredentialTable, createWebhookEndpointEntity, createWebhookEndpointTable, drizzle_default as default, defaultMigrationSources, defineDrizzleConfig, deniedJtiTable, drizzleConfigJsonSchema, drizzleConfigSchema, exitCodeOf, frameworkMigrationsDir, idempotencyKeyTable, isFrameworkFallbackEntity, knownConnectors, loadSources, meaningOf, migrationHash, openMigrationDriver, orderSources, readMigrationEnv, registerAuditEntities, registerDrizzleFrameworkStores, registerIdempotencyEntities, registerSessionEntity, registerTokenEntities, registerTotpSecretEntity, registerUserEntity, registerWebAuthnCredentialEntity, registerWebhookEndpointEntity, renderRefusal, renderStatus, resetAllowed, resolveCheckMode, resolveConnector, resolveDdlMode, resolveDivergenceMode, sessionTable, styleFor, subjectRevocationTable, totpSecretTable, userTable, verdictOf, webAuthnCredentialTable, webhookEndpointTable };
|
|
106
|
+
export { APP_RANK, APP_SOURCE, AUDIT_ENTITY_NAMES, DDL_MODES, DEFAULT_LOCK_TIMEOUT_MS, DIVERGENCE_MODES, DrizzleAuditStore, DrizzleIdempotencyStore, DrizzleMigrator, DrizzleOrm, DrizzleRepository, DrizzleService, DrizzleTokenStore, DrizzleTotpSecretStore, DrizzleTransaction, DrizzleUserRepository, DrizzleWebAuthnCredentialStore, DrizzleWebhookStore, EXIT, FORMAT_MARKER, FRAMEWORK_CONNECTOR, FRAMEWORK_RANK, FRAMEWORK_SOURCE, HISTORY_TABLE, IDEMPOTENCY_ENTITY_NAME, MIGRATE_URL_ENV, MIGRATION_CHECK_MODES, MIGRATION_FORMAT_VERSION, MYSQL_LOCK_NAME_SQL, MigrationVerdictError, PG_LOCK_KEY, SESSION_CONNECTOR, SESSION_ENTITY_NAME, SessionStorage, TOKEN_ENTITY_NAMES, TOTP_SECRET_ENTITY, WEBAUTHN_CREDENTIAL_ENTITY, WEBHOOK_ENDPOINT_ENTITY, accessTokenTable, action, appMigrationsDir, auditEventTable, buildMigrator, buildReport, createAccessTokenTable, createAuditEntities, createAuditEventTable, createDeniedJtiTable, createIdempotencyEntities, createIdempotencyTable, createSessionEntity, createSessionTable, createSubjectRevocationTable, createTokenEntities, createTotpSecretEntity, createTotpSecretTable, createUserEntity, createUserTable, createWebAuthnCredentialEntity, createWebAuthnCredentialTable, createWebhookEndpointEntity, createWebhookEndpointTable, drizzle_default as default, defaultMigrationSources, defineDrizzleConfig, deniedJtiTable, drizzleConfigJsonSchema, drizzleConfigSchema, exitCodeOf, frameworkMigrationsDir, idempotencyKeyTable, isFrameworkFallbackEntity, knownConnectors, loadSources, meaningOf, migrationHash, openMigrationDriver, orderSources, readMigrationEnv, registerAuditEntities, registerDrizzleFrameworkStores, registerIdempotencyEntities, registerSessionEntity, registerTokenEntities, registerTotpSecretEntity, registerUserEntity, registerWebAuthnCredentialEntity, registerWebhookEndpointEntity, renderRefusal, renderStatus, resetAllowed, resolveCheckMode, resolveConnector, resolveDdlMode, resolveDivergenceMode, seedEntityRow, sessionTable, styleFor, subjectRevocationTable, totpSecretTable, userTable, verdictOf, webAuthnCredentialTable, webhookEndpointTable };
|
|
@@ -3,7 +3,7 @@ import { EXIT, action } from "../src/migrator/explain.js";
|
|
|
3
3
|
import { openMigrationDriver } from "../src/migrator/drivers/index.js";
|
|
4
4
|
import { frameworkMigrationsDir } from "../src/migrator/paths.js";
|
|
5
5
|
import { createdTables, frameworkTables, loadSources } from "../src/migrator/sources.js";
|
|
6
|
-
import { appMigrationsDir } from "../src/migrator/resolve.js";
|
|
6
|
+
import { appMigrationsDir, generateApplyAllowed, readMigrationEnv } from "../src/migrator/resolve.js";
|
|
7
7
|
import { summarizeGap } from "../src/migrator/schemaDiff.js";
|
|
8
8
|
import { gapAgainstDeclared } from "../src/migrator/divergence.js";
|
|
9
9
|
import { auditMigrationSql, runGenerate, stampFormatMarker } from "../src/migrator/kit.js";
|
|
@@ -62,6 +62,17 @@ const options = {
|
|
|
62
62
|
* nodefony orm:migrate # ou : le travail de déploiement
|
|
63
63
|
* ```
|
|
64
64
|
*
|
|
65
|
+
* @example En développement — écrire ET appliquer en un seul geste
|
|
66
|
+
* ```bash
|
|
67
|
+
* nodefony orm:generate --name ajout_du_titre --apply
|
|
68
|
+
* ```
|
|
69
|
+
* `--apply` n'est rien d'autre qu'un `orm:migrate` enchaîné : la commande
|
|
70
|
+
* DÉLÈGUE, elle ne rejoue pas l'application pour son compte. Toutes les gardes
|
|
71
|
+
* du verbe d'application valent donc à l'identique — refus du destructif sans
|
|
72
|
+
* `--allow-destructive`, verrou, historique. Refusé hors développement : un
|
|
73
|
+
* déploiement n'écrit pas ses migrations au moment de les appliquer, il
|
|
74
|
+
* applique celles que la revue de code a validées.
|
|
75
|
+
*
|
|
65
76
|
* @example Ce que le modèle déclaratif ne peut pas déduire
|
|
66
77
|
* ```bash
|
|
67
78
|
* nodefony orm:generate --custom --name vue_des_ventes
|
|
@@ -75,6 +86,7 @@ var OrmGenerate = class extends OrmMigrateCommand {
|
|
|
75
86
|
this.addOption("-n, --name <nom>", "nom de la migration, en minuscules et « _ » — il entre dans le tag, qui est immuable une fois publié");
|
|
76
87
|
this.addOption("--custom", "écrit un fichier SQL VIDE et son entrée de journal, sans rien déduire : vues, déclencheurs, clés étrangères, remplissages");
|
|
77
88
|
this.addOption("--allow-destructive", "accepte une migration qui SUPPRIME des données — à ne poser qu'après avoir relu le fichier produit");
|
|
89
|
+
this.addOption("--apply", "applique la migration dans la foulée (développement seulement) — écrire puis appliquer en un seul geste");
|
|
78
90
|
}
|
|
79
91
|
/**
|
|
80
92
|
* Racine de l'application — c'est depuis là que l'outil est lancé.
|
|
@@ -118,7 +130,7 @@ var OrmGenerate = class extends OrmMigrateCommand {
|
|
|
118
130
|
dialect: resolution.dialect,
|
|
119
131
|
name
|
|
120
132
|
});
|
|
121
|
-
return this.#emit({
|
|
133
|
+
return await this.#emit({
|
|
122
134
|
formatVersion: 1,
|
|
123
135
|
connector,
|
|
124
136
|
generated: true,
|
|
@@ -230,7 +242,7 @@ var OrmGenerate = class extends OrmMigrateCommand {
|
|
|
230
242
|
], opts.json, EXIT.actionRequired);
|
|
231
243
|
return this;
|
|
232
244
|
}
|
|
233
|
-
return this.#emit({
|
|
245
|
+
return await this.#emit({
|
|
234
246
|
formatVersion: 1,
|
|
235
247
|
connector,
|
|
236
248
|
generated: false,
|
|
@@ -273,7 +285,7 @@ var OrmGenerate = class extends OrmMigrateCommand {
|
|
|
273
285
|
], opts.json, EXIT.actionRequired, discovery);
|
|
274
286
|
return this;
|
|
275
287
|
}
|
|
276
|
-
return this.#emit({
|
|
288
|
+
return await this.#emit({
|
|
277
289
|
formatVersion: 1,
|
|
278
290
|
connector,
|
|
279
291
|
generated: true,
|
|
@@ -384,17 +396,69 @@ var OrmGenerate = class extends OrmMigrateCommand {
|
|
|
384
396
|
*
|
|
385
397
|
* @returns `this`.
|
|
386
398
|
*/
|
|
387
|
-
|
|
399
|
+
/**
|
|
400
|
+
* Écrit le compte rendu de génération, puis enchaîne l'application si
|
|
401
|
+
* `--apply` est posé.
|
|
402
|
+
*
|
|
403
|
+
* 🔴 **En sortie machine, `--apply` ne produit qu'UN document** : celui de
|
|
404
|
+
* l'application, qui porte le verdict final. Le compte rendu de génération
|
|
405
|
+
* part alors sur la sortie d'ERREUR — libre par contrat — plutôt que d'écrire
|
|
406
|
+
* deux objets à la suite sur la sortie standard, ce qui casserait tout `| jq`
|
|
407
|
+
* sans qu'aucune erreur ne le dise.
|
|
408
|
+
*
|
|
409
|
+
* @param report - le rapport de génération.
|
|
410
|
+
* @param opts - options reçues.
|
|
411
|
+
* @param headline - la première ligne, déjà rédigée par l'appelant.
|
|
412
|
+
* @returns `this`, la sortie ayant été écrite.
|
|
413
|
+
*/
|
|
414
|
+
async #emit(report, opts, headline) {
|
|
388
415
|
const style = this.style;
|
|
389
416
|
let human = `${style.green("✓")} ${headline}\n`;
|
|
390
417
|
if (report.files.length > 0) human += `\n${report.files.map((f) => ` + ${f}`).join("\n")}\n`;
|
|
391
418
|
if (report.otherDialect.length > 0) human += `\n${style.dim(`Écrites pour un autre moteur, donc hors de cette migration :`)}\n` + report.otherDialect.map((o) => ` • ${o.table} (${o.dialect}) — ${o.file}`).join("\n") + "\n";
|
|
392
419
|
if (report.unreadable.length > 0) human += `\n${style.dim("Fichiers d'entités non lus (aucune entité enregistrée n'en dépend) :")}\n` + report.unreadable.map((u) => ` • ${u.file} — ${u.cause}`).join("\n") + "\n";
|
|
393
420
|
if (report.warnings.length > 0) human += `\n${style.bold("⚠️ À REGARDER avant d'appliquer")} (rien n'est détruit — mais l'application peut cesser de répondre le temps de l'opération, ou la migration ÉCHOUER si la table porte déjà des lignes) :\n` + report.warnings.map((r) => ` • ${r.id} : ${r.what}\n → ${r.todo}`).join("\n") + "\n";
|
|
394
|
-
if (report.generated) human += `\n${style.bold("À faire :")}\n ${style.dim("relire le fichier, puis")} ${style.green("nodefony orm:migrate")}\n`;
|
|
421
|
+
if (report.generated && opts.apply !== true) human += `\n${style.bold("À faire :")}\n ${style.dim("relire le fichier, puis")} ${style.green("nodefony orm:migrate")}\n ${style.dim("ou, en développement, les deux d'un geste :")} ${style.green("nodefony orm:generate --apply")}\n`;
|
|
422
|
+
if (report.generated && opts.apply === true) {
|
|
423
|
+
if (opts.json === true) process.stderr.write(human.endsWith("\n") ? human : `${human}\n`);
|
|
424
|
+
else process.stdout.write(human.endsWith("\n") ? human : `${human}\n`);
|
|
425
|
+
await this.#applyNow(opts, report.connector);
|
|
426
|
+
return this;
|
|
427
|
+
}
|
|
395
428
|
this.respond(report, human, EXIT.ok, opts.json);
|
|
396
429
|
return this;
|
|
397
430
|
}
|
|
431
|
+
/**
|
|
432
|
+
* Enchaîne l'application quand `--apply` est posé.
|
|
433
|
+
*
|
|
434
|
+
* DÉLÈGUE au verbe `orm:migrate` par son point d'entrée public
|
|
435
|
+
* ({@link Cli.getCommand}) : rejouer ici l'application aurait produit une
|
|
436
|
+
* SECONDE implémentation du même geste, qui aurait divergé de la première au
|
|
437
|
+
* premier réglage — et c'est précisément elle qui porte le refus du
|
|
438
|
+
* destructif, le verrou et l'écriture de l'historique.
|
|
439
|
+
*
|
|
440
|
+
* Refusé hors développement, sans drapeau pour le lever : générer puis
|
|
441
|
+
* appliquer dans le même souffle suppose qu'on relit le SQL entre les deux,
|
|
442
|
+
* ce que personne ne fait sur un serveur. En production, la migration a été
|
|
443
|
+
* écrite, relue et versionnée ailleurs ; `orm:migrate` l'applique.
|
|
444
|
+
*
|
|
445
|
+
* @param opts - options reçues, passées telles quelles au verbe d'application.
|
|
446
|
+
* @param connector - connecteur résolu, pour le message de refus.
|
|
447
|
+
* @returns `true` si la main a été passée (la sortie est déjà écrite).
|
|
448
|
+
*/
|
|
449
|
+
async #applyNow(opts, connector) {
|
|
450
|
+
if (!generateApplyAllowed(readMigrationEnv(this.kernel))) {
|
|
451
|
+
this.fail(connector, "NF_MIGRATE_NOT_DEVELOPMENT", "« --apply » n'existe qu'en développement.", "Écrire une migration et l'appliquer dans le même geste suppose qu'on relit le SQL produit entre les deux — ce que personne ne fait sur un serveur. Hors développement, la migration a été écrite, relue et versionnée ailleurs : c'est « orm:migrate » qui l'applique, et lui seul.", [action("nodefony orm:migrate")], opts.json, EXIT.actionRequired);
|
|
452
|
+
return true;
|
|
453
|
+
}
|
|
454
|
+
const migrate = (this.kernel?.getModule("drizzle"))?.commands?.["orm:migrate"];
|
|
455
|
+
if (!migrate) {
|
|
456
|
+
this.fail(connector, "NF_MIGRATE_UNAVAILABLE", "Le verbe « orm:migrate » est introuvable : la migration a été ÉCRITE mais n'a pas été appliquée.", "« --apply » ne fait qu'enchaîner ce verbe ; sans lui, il n'y a rien à enchaîner.", [action("nodefony orm:migrate")], opts.json);
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
await migrate.generate(opts);
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
398
462
|
};
|
|
399
463
|
//#endregion
|
|
400
464
|
export { OrmGenerate as default };
|
|
@@ -2,7 +2,7 @@ import { buildReport, isAheadOnly, meaningOf } from "../src/migrator/explain.js"
|
|
|
2
2
|
import { defaultConnectorFilename } from "../src/connectorTarget.js";
|
|
3
3
|
import { defaultMigrationSources } from "../src/migrator/paths.js";
|
|
4
4
|
import { DrizzleMigrator } from "../src/migrator/DrizzleMigrator.js";
|
|
5
|
-
import { appMigrationsDir, readMigrationEnv, resetAllowed, resolveCheckMode, resolveDdlMode } from "../src/migrator/resolve.js";
|
|
5
|
+
import { appMigrationsDir, appVersionsMigrations, readMigrationEnv, resetAllowed, resolveCheckMode, resolveDdlMode } from "../src/migrator/resolve.js";
|
|
6
6
|
import { describeDivergence } from "../src/migrator/divergence.js";
|
|
7
7
|
import { DrizzleOrm } from "../src/orm-core/DrizzleOrm.js";
|
|
8
8
|
import "../src/orm-core/index.js";
|
|
@@ -122,7 +122,7 @@ var DrizzleService = class extends Service {
|
|
|
122
122
|
/** Connecte un connecteur (crée le dossier de la base SQLite si nécessaire). */
|
|
123
123
|
async #connectOne(name, cfg, connect = true) {
|
|
124
124
|
const dialect = cfg.dialect ?? "sqlite";
|
|
125
|
-
const ddl = resolveDdlMode(cfg.ddl, readMigrationEnv(this.kernel));
|
|
125
|
+
const ddl = resolveDdlMode(cfg.ddl, readMigrationEnv(this.kernel), appVersionsMigrations(appMigrationsDir(this.kernel, this.#config().migrations?.dir ?? "migrations")));
|
|
126
126
|
let filename;
|
|
127
127
|
if (dialect === "sqlite") {
|
|
128
128
|
filename = cfg.filename ?? this.#defaultFilename(name);
|
|
@@ -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**.
|
|
@@ -64,23 +64,82 @@ const PATTERNS = [
|
|
|
64
64
|
}
|
|
65
65
|
];
|
|
66
66
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* `x`, renommer. Ce `DROP TABLE` est **structurel**, les données ont été
|
|
72
|
-
* recopiées juste avant ; le signaler comme une perte ferait crier au loup à
|
|
73
|
-
* chaque changement de colonne, et le garde serait désarmé au bout de trois
|
|
74
|
-
* fois.
|
|
75
|
-
*
|
|
76
|
-
* ⚠️ Ce n'est pas une garantie que rien n'est perdu : si une colonne
|
|
77
|
-
* n'apparaît pas dans le `INSERT … SELECT`, ses données disparaissent bel et
|
|
78
|
-
* bien. C'est pourquoi la recréation reste SIGNALÉE, avec le geste — lire le
|
|
79
|
-
* SQL —, et seulement déclassée en avertissement.
|
|
67
|
+
* Le SQL d'une migration, qu'on le reçoive découpé ou d'un bloc.
|
|
68
|
+
*
|
|
69
|
+
* Les deux formes existent pour de bon : l'audit d'APPLICATION travaille sur
|
|
70
|
+
* les instructions déjà séparées, celui de GÉNÉRATION sur le fichier entier.
|
|
80
71
|
*/
|
|
81
|
-
function
|
|
82
|
-
|
|
83
|
-
|
|
72
|
+
function asSql(sql) {
|
|
73
|
+
return typeof sql === "string" ? sql : sql.join("\n");
|
|
74
|
+
}
|
|
75
|
+
/** Un nom de table tel qu'il s'écrit en SQL, guillemets facultatifs. */
|
|
76
|
+
const IDENT = "[`\"']?(\\w+)[`\"']?";
|
|
77
|
+
/**
|
|
78
|
+
* Les tables qu'une migration RECONSTRUIT, par la ronde que SQLite impose.
|
|
79
|
+
*
|
|
80
|
+
* 🔴 **La convention de nommage n'est PAS la règle.** La version précédente
|
|
81
|
+
* cherchait le préfixe `__new_` que produit l'outil de génération : elle ne
|
|
82
|
+
* reconnaissait donc une reconstruction que si c'est LUI qui l'avait écrite.
|
|
83
|
+
* Une migration reprise à la main — cas courant, et mesuré sur un agent réel
|
|
84
|
+
* qui avait nommé sa table d'étape `articles_new` — retombait sur
|
|
85
|
+
* « supprime une table et TOUTES ses lignes », sur un SQL qui ne perd rien.
|
|
86
|
+
*
|
|
87
|
+
* Ce qui fait une reconstruction, c'est la RONDE, quels que soient les noms :
|
|
88
|
+
* une table d'étape est créée, les lignes de la table visée y sont recopiées,
|
|
89
|
+
* la visée est supprimée, et l'étape prend sa place. Les quatre doivent être
|
|
90
|
+
* présentes dans le même fichier, et se RÉPONDRE — c'est le renommage final
|
|
91
|
+
* qui dit quelle table d'étape appartient à quelle table visée.
|
|
92
|
+
*
|
|
93
|
+
* ⚠️ Ce n'est pas une garantie que rien n'est perdu : si une colonne n'apparaît
|
|
94
|
+
* pas dans le `INSERT … SELECT`, ses données disparaissent bel et bien. La
|
|
95
|
+
* reconstruction reste donc SIGNALÉE, avec le geste — lire le SQL —, et
|
|
96
|
+
* seulement déclassée en avertissement.
|
|
97
|
+
*
|
|
98
|
+
* @param sql - les instructions de la migration, ou le fichier entier.
|
|
99
|
+
* @returns les tables reconstruites, en minuscules ; vide s'il n'y en a aucune.
|
|
100
|
+
*/
|
|
101
|
+
function rebuiltTables(sql) {
|
|
102
|
+
const joint = asSql(sql);
|
|
103
|
+
const out = /* @__PURE__ */ new Set();
|
|
104
|
+
const renames = joint.matchAll(new RegExp(`\\bALTER\\s+TABLE\\s+${IDENT}\\s+RENAME\\s+TO\\s+${IDENT}`, "gi"));
|
|
105
|
+
for (const rename of renames) {
|
|
106
|
+
const staging = rename[1];
|
|
107
|
+
const target = rename[2];
|
|
108
|
+
if (staging === void 0 || target === void 0) continue;
|
|
109
|
+
const created = new RegExp(`\\bCREATE\\s+TABLE\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?[\`"']?${staging}[\`"']?\\b`, "i");
|
|
110
|
+
const copied = new RegExp(`\\bINSERT\\s+INTO\\s+[\`"']?${staging}[\`"']?[\\s\\S]{0,4000}?\\bFROM\\s+[\`"']?${target}[\`"']?\\b`, "i");
|
|
111
|
+
const dropped = new RegExp(`\\bDROP\\s+TABLE\\s+(?:IF\\s+EXISTS\\s+)?[\`"']?${target}[\`"']?\\b`, "i");
|
|
112
|
+
if (created.test(joint) && copied.test(joint) && dropped.test(joint)) out.add(target.toLowerCase());
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Les tables qu'un SQL SUPPRIME, dans l'ordre où il les nomme.
|
|
118
|
+
*
|
|
119
|
+
* Exportée pour que l'audit de génération et celui d'application lisent la
|
|
120
|
+
* MÊME chose : deux extractions écrites séparément divergeraient, et l'une des
|
|
121
|
+
* deux surfaces se mettrait à refuser ce que l'autre laisse passer.
|
|
122
|
+
*
|
|
123
|
+
* @param sql - les instructions de la migration, ou le fichier entier.
|
|
124
|
+
* @returns les noms supprimés, en minuscules.
|
|
125
|
+
*/
|
|
126
|
+
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());
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* L'instruction fait-elle partie de la mécanique d'une reconstruction ?
|
|
131
|
+
*
|
|
132
|
+
* Le verdict porte sur la table que CETTE instruction touche, jamais sur le
|
|
133
|
+
* fichier entier : une migration qui reconstruit `articles` et supprime
|
|
134
|
+
* `brouillons` doit continuer de signaler la seconde.
|
|
135
|
+
*/
|
|
136
|
+
function isRebuildMechanics(statement, kind, rebuilt) {
|
|
137
|
+
if (kind === "drop-table") return droppedTables(statement).some((name) => rebuilt.has(name));
|
|
138
|
+
if (kind === "rename") {
|
|
139
|
+
const target = new RegExp(`\\bRENAME\\s+TO\\s+${IDENT}`, "i").exec(statement)?.[1];
|
|
140
|
+
return target !== void 0 && rebuilt.has(target.toLowerCase());
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
84
143
|
}
|
|
85
144
|
/** Borne une instruction pour l'affichage, sans jamais la déformer. */
|
|
86
145
|
function truncate(statement) {
|
|
@@ -96,10 +155,10 @@ function truncate(statement) {
|
|
|
96
155
|
function scanDestructive(files) {
|
|
97
156
|
const out = [];
|
|
98
157
|
for (const file of files) {
|
|
99
|
-
const
|
|
158
|
+
const rebuilt = rebuiltTables(file.statements);
|
|
100
159
|
for (const statement of file.statements) for (const p of PATTERNS) {
|
|
101
160
|
if (!p.re.test(statement)) continue;
|
|
102
|
-
const structurel =
|
|
161
|
+
const structurel = isRebuildMechanics(statement, p.kind, rebuilt);
|
|
103
162
|
out.push({
|
|
104
163
|
source: file.source,
|
|
105
164
|
tag: file.tag,
|
|
@@ -210,4 +269,4 @@ function checkDataAdvice(connector, target) {
|
|
|
210
269
|
return `Ces migrations ont modifié des tables qui portaient déjà des lignes. Pour VÉRIFIER que les données ont suivi, ne repars pas d'une base vide : ce sont ces lignes-là qui sont la réponse, les effacer efface la question. Deux moyens. Compter et regarder sur place — « SELECT COUNT(*) » sur les tables touchées, et les valeurs des colonnes nouvellement remplies. Ou rejouer le même lot sur une COPIE, sans toucher à celle-ci : ${copy}, puis désigne-la par NF_MIGRATE_DATABASE_URL :\n NF_MIGRATE_DATABASE_URL=${url} nodefony orm:migrate${suffixe}\n (PowerShell : $env:NF_MIGRATE_DATABASE_URL = "${url}" ; nodefony orm:migrate${suffixe})`;
|
|
211
270
|
}
|
|
212
271
|
//#endregion
|
|
213
|
-
export { checkDataAdvice, dataLoss, destructiveActions, renderDestructive, scanDestructive, summarizeDestructive, touchesExistingRows };
|
|
272
|
+
export { checkDataAdvice, dataLoss, destructiveActions, droppedTables, rebuiltTables, renderDestructive, scanDestructive, summarizeDestructive, touchesExistingRows };
|
|
@@ -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,6 @@
|
|
|
1
1
|
import { FORMAT_MARKER } from "./types.js";
|
|
2
2
|
import { MigrationToolError, generationFailed, generationNeedsAnswer, generationToolMissing, introspectFailed } from "./refusals.js";
|
|
3
|
+
import { droppedTables, rebuiltTables } from "./destructive.js";
|
|
3
4
|
import fs from "node:fs";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { spawnSync } from "node:child_process";
|
|
@@ -149,6 +150,10 @@ const DESTRUCTIVE_PATTERNS = [
|
|
|
149
150
|
{
|
|
150
151
|
id: "drop-table",
|
|
151
152
|
pattern: /\bDROP\s+TABLE\b/i,
|
|
153
|
+
detect: (code) => {
|
|
154
|
+
const rebuilt = rebuiltTables(code);
|
|
155
|
+
return droppedTables(code).some((table) => !rebuilt.has(table));
|
|
156
|
+
},
|
|
152
157
|
what: "supprime une table ET toutes ses lignes",
|
|
153
158
|
todo: "sauvegarder, puis appliquer en deux temps (cf expand/contract)"
|
|
154
159
|
},
|
|
@@ -4,6 +4,7 @@ import { defaultMigrationSources } from "./paths.js";
|
|
|
4
4
|
import { DrizzleMigrator } from "./DrizzleMigrator.js";
|
|
5
5
|
import { parseDatabaseUrl, sqliteFilenameFromUrl } from "nodefony";
|
|
6
6
|
import { ormRegistry } from "@nodefony/orm-core";
|
|
7
|
+
import fs from "node:fs";
|
|
7
8
|
import path from "node:path";
|
|
8
9
|
//#region nodefony/src/migrator/resolve.ts
|
|
9
10
|
/**
|
|
@@ -52,6 +53,25 @@ function resetAllowed(env) {
|
|
|
52
53
|
return env.runtime === "development" && env.nodeEnv !== "test";
|
|
53
54
|
}
|
|
54
55
|
/**
|
|
56
|
+
* `orm:generate --apply` est-il recevable dans cet environnement ?
|
|
57
|
+
*
|
|
58
|
+
* Écrire une migration et l'appliquer dans le même souffle suppose qu'on relit
|
|
59
|
+
* le SQL produit entre les deux — ce que personne ne fait sur un serveur.
|
|
60
|
+
* Ailleurs qu'en développement, la migration a été écrite, relue et versionnée
|
|
61
|
+
* en amont : `orm:migrate` l'applique, et lui seul. Aucun drapeau ne lève ce
|
|
62
|
+
* refus, pour la même raison que {@link resetAllowed} n'en a pas.
|
|
63
|
+
*
|
|
64
|
+
* Même forme que `resetAllowed`, et c'est voulu : les deux disent « geste de
|
|
65
|
+
* développement », et deux rédactions de la même condition finiraient par
|
|
66
|
+
* diverger sans que rien ne le signale.
|
|
67
|
+
*
|
|
68
|
+
* @param env - environnement constaté.
|
|
69
|
+
* @returns `true` si l'enchaînement est recevable.
|
|
70
|
+
*/
|
|
71
|
+
function generateApplyAllowed(env) {
|
|
72
|
+
return env.runtime === "development" && env.nodeEnv !== "test";
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
55
75
|
* Le mode de schéma qui s'applique à un connecteur.
|
|
56
76
|
*
|
|
57
77
|
* Règle des défauts, et son pourquoi : **appliquer des migrations au démarrage
|
|
@@ -63,13 +83,63 @@ function resetAllowed(env) {
|
|
|
63
83
|
* - tout le reste → `none` : personne ne touche au schéma au démarrage, un
|
|
64
84
|
* travail externe applique les migrations avant que le trafic n'arrive.
|
|
65
85
|
*
|
|
86
|
+
* 🔴 **Une application qui VERSIONNE des migrations bascule en `migrate`, même
|
|
87
|
+
* en développement.** Deux fabricants du même schéma ne s'accordent pas, et
|
|
88
|
+
* rien ne le signale tant qu'on ne les fait pas se rencontrer : le DDL dérivé
|
|
89
|
+
* pose une contrainte d'unicité INLINE et anonyme (`content text NOT NULL
|
|
90
|
+
* UNIQUE`), là où le générateur de migrations raisonne sur un index NOMMÉ
|
|
91
|
+
* (`messages_content_unique`). La migration suivante émet alors un
|
|
92
|
+
* `DROP INDEX` d'un index qui n'a jamais existé, échoue sur `no such index`,
|
|
93
|
+
* inscrit son marqueur — et la réparation ne peut rien y faire, puisque le
|
|
94
|
+
* fichier est inapplicable. Vécu sur un agent tiers : trente-cinq minutes de
|
|
95
|
+
* `repair`/`baseline`/`reset`, deux bases détruites, la boucle brisée à la main.
|
|
96
|
+
* La condition est délibérément « l'application EN A », pas « elle en a en
|
|
97
|
+
* attente » : dès la première migration écrite, le schéma leur appartient.
|
|
98
|
+
* Elle est bornée au développement RÉEL, jamais à `NODE_ENV=test` : une suite
|
|
99
|
+
* lance un exemplaire par worker, et appliquer des migrations depuis plusieurs
|
|
100
|
+
* processus au démarrage est précisément ce que la règle ci-dessus refuse.
|
|
101
|
+
* Un `ddl` écrit dans la configuration gagne toujours — c'est un choix, pas une
|
|
102
|
+
* déduction.
|
|
103
|
+
*
|
|
66
104
|
* @param explicit - valeur écrite dans la configuration du connecteur, si elle l'est.
|
|
67
105
|
* @param env - environnement constaté.
|
|
106
|
+
* @param appHasMigrations - l'application versionne au moins une migration.
|
|
68
107
|
* @returns le mode effectif.
|
|
69
108
|
*/
|
|
70
|
-
function resolveDdlMode(explicit, env) {
|
|
109
|
+
function resolveDdlMode(explicit, env, appHasMigrations = false) {
|
|
71
110
|
if (explicit) return explicit;
|
|
72
|
-
|
|
111
|
+
if (env.runtime === "development" || env.nodeEnv === "test") return appHasMigrations && env.nodeEnv !== "test" ? "migrate" : "auto";
|
|
112
|
+
return "none";
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* L'application versionne-t-elle au moins une migration ?
|
|
116
|
+
*
|
|
117
|
+
* Lecture SYNCHRONE et bornée à un `readdirSync` par connecteur au démarrage —
|
|
118
|
+
* hors de tout chemin de requête. Elle doit l'être : le mode de schéma se
|
|
119
|
+
* décide avant la connexion, et le rendre asynchrone propagerait un `await`
|
|
120
|
+
* dans toute la chaîne de résolution pour une réponse qui ne change jamais
|
|
121
|
+
* pendant la vie du processus.
|
|
122
|
+
*
|
|
123
|
+
* Un dossier absent, illisible ou vide répond `false` — l'application n'a alors
|
|
124
|
+
* rien à appliquer, et `auto` reste le bon défaut de démarrage rapide.
|
|
125
|
+
*
|
|
126
|
+
* @param dir - dossier de migrations de l'application ({@link appMigrationsDir}).
|
|
127
|
+
* @returns `true` si au moins un fichier `.sql` y est versionné, à un niveau
|
|
128
|
+
* ou sous un sous-dossier de dialecte (`migrations/postgres/0001_x.sql`).
|
|
129
|
+
*/
|
|
130
|
+
function appVersionsMigrations(dir) {
|
|
131
|
+
if (!dir) return false;
|
|
132
|
+
try {
|
|
133
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
134
|
+
if (entry.isFile() && entry.name.endsWith(".sql")) return true;
|
|
135
|
+
if (entry.isDirectory() && entry.name !== "meta") {
|
|
136
|
+
if (fs.readdirSync(path.join(dir, entry.name)).some((name) => name.endsWith(".sql"))) return true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
73
143
|
}
|
|
74
144
|
/**
|
|
75
145
|
* La conduite de la sonde de disponibilité face à un schéma en retard.
|
|
@@ -217,7 +287,7 @@ function resolveConnector(connector, config, env, kernel, options = {}) {
|
|
|
217
287
|
url: fromMigrateUrl && dialect !== "sqlite" ? parsedUrl.url : base.url
|
|
218
288
|
},
|
|
219
289
|
fromMigrateUrl,
|
|
220
|
-
ddl: resolveDdlMode(declared.ddl, env)
|
|
290
|
+
ddl: resolveDdlMode(declared.ddl, env, appVersionsMigrations(appMigrationsDir(kernel, config.migrations.dir)))
|
|
221
291
|
};
|
|
222
292
|
}
|
|
223
293
|
/**
|
|
@@ -278,4 +348,4 @@ function resolveDivergenceMode(config) {
|
|
|
278
348
|
return config.migrations.divergence;
|
|
279
349
|
}
|
|
280
350
|
//#endregion
|
|
281
|
-
export { MIGRATE_URL_ENV, appMigrationsDir, buildMigrator, knownConnectors, readMigrationEnv, resetAllowed, resolveCheckMode, resolveConnector, resolveDdlMode, resolveDivergenceMode };
|
|
351
|
+
export { MIGRATE_URL_ENV, appMigrationsDir, appVersionsMigrations, buildMigrator, generateApplyAllowed, knownConnectors, readMigrationEnv, resetAllowed, resolveCheckMode, resolveConnector, resolveDdlMode, resolveDivergenceMode };
|