@nodefony/drizzle 10.0.0-alpha.9 → 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.
- package/README.md +19 -19
- package/dist/index.js +3 -3
- package/dist/nodefony/command/orm-generate.js +3 -3
- package/dist/nodefony/config/defineModuleConfig.js +2 -2
- package/dist/nodefony/entity/userTable.js +1 -1
- package/dist/nodefony/registerStores.js +2 -2
- package/dist/nodefony/service/DrizzleService.js +26 -12
- package/dist/nodefony/src/DrizzleAuditStore.js +1 -1
- package/dist/nodefony/src/connectorTarget.js +1 -1
- package/dist/nodefony/src/migrator/DrizzleMigrator.js +3 -3
- package/dist/nodefony/src/migrator/adopt.js +1 -1
- package/dist/nodefony/src/migrator/destructive.js +2 -3
- package/dist/nodefony/src/migrator/drivers/mysqlDriver.js +1 -1
- package/dist/nodefony/src/migrator/drivers/postgresDriver.js +1 -1
- package/dist/nodefony/src/migrator/explain.js +7 -10
- package/dist/nodefony/src/migrator/history.js +25 -2
- package/dist/nodefony/src/migrator/kit.js +23 -9
- package/dist/nodefony/src/migrator/rebuildCopy.js +6 -6
- package/dist/nodefony/src/migrator/resolve.js +4 -4
- package/dist/nodefony/src/migrator/sources.js +13 -8
- package/dist/nodefony/src/orm-core/DrizzleOrm.js +49 -29
- package/dist/nodefony/src/orm-core/DrizzleRepository.js +38 -26
- package/dist/nodefony/src/queryKit.js +5 -5
- package/dist/types/nodefony/src/DrizzleUserRepository.d.ts +1 -1
- package/dist/types/nodefony/src/connectorTarget.d.ts +1 -1
- package/dist/types/nodefony/src/migrator/DrizzleMigrator.d.ts +4 -4
- package/dist/types/nodefony/src/migrator/destructive.d.ts +1 -1
- package/dist/types/nodefony/src/migrator/drivers/index.d.ts +2 -2
- package/dist/types/nodefony/src/migrator/explain.d.ts +9 -9
- package/dist/types/nodefony/src/migrator/name.d.ts +1 -1
- package/dist/types/nodefony/src/migrator/types.d.ts +2 -2
- package/dist/types/nodefony/src/orm-core/DrizzleOrm.d.ts +3 -3
- package/dist/types/nodefony/src/orm-core/DrizzleRepository.d.ts +1 -1
- package/dist/types/nodefony/src/orm-core/ddlPlan.d.ts +2 -2
- package/dist/types/nodefony/src/queryKit.d.ts +9 -9
- package/dist/types/nodefony/src/safeTarget.d.ts +2 -2
- package/docs/index.md +13 -13
- package/docs/migrations.md +1 -1
- package/package.json +14 -14
package/README.md
CHANGED
|
@@ -16,29 +16,29 @@ npm install @nodefony/drizzle@alpha drizzle-orm better-sqlite3
|
|
|
16
16
|
|
|
17
17
|
## Utilisation comme module Nodefony (bootable)
|
|
18
18
|
|
|
19
|
-
Ajouter `@nodefony/drizzle`
|
|
20
|
-
au boot un ORM par connecteur configuré (c'est l'ORM SQL par défaut recommandé).
|
|
19
|
+
Ajouter `@nodefony/drizzle` au manifeste `modules` de `nodefony.config.ts` : le `DrizzleService`
|
|
20
|
+
connecte au boot un ORM par connecteur configuré (c'est l'ORM SQL par défaut recommandé).
|
|
21
21
|
|
|
22
22
|
```typescript
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
// nodefony.config.ts
|
|
24
|
+
import { defineConfig, use } from "nodefony";
|
|
25
|
+
|
|
26
|
+
export default defineConfig({
|
|
27
|
+
modules: [
|
|
28
|
+
"@nodefony/http",
|
|
29
|
+
// connecte au boot, ferme au shutdown ; sans `filename`, la base vit sous var/databases/
|
|
30
|
+
use("@nodefony/drizzle", {
|
|
31
|
+
connectors: { default: { dialect: "sqlite" } },
|
|
32
|
+
}),
|
|
33
|
+
],
|
|
34
|
+
});
|
|
29
35
|
```
|
|
30
36
|
|
|
31
|
-
|
|
32
|
-
`nodefony/
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
connectors: {
|
|
37
|
-
default: { filename: "<root>/var/databases/nodefony-drizzle.db" },
|
|
38
|
-
// ":memory:" ou absent → base éphémère
|
|
39
|
-
},
|
|
40
|
-
};
|
|
41
|
-
```
|
|
37
|
+
Le schéma (`nodefony/config/config.ts`) porte les défauts ; l'application les surcharge par
|
|
38
|
+
`use("@nodefony/drizzle", { … })`, par `NF_DATABASE_URL` (dialecte déduit du schéma d'URL) ou par
|
|
39
|
+
l'override générique `NF__DRIZZLE__<CHEMIN>`. Sans `filename`, un connecteur SQLite écrit dans
|
|
40
|
+
`<app>/var/databases/nodefony-drizzle.db` (`nodefony-<connecteur>.db` pour les autres) ;
|
|
41
|
+
`filename: ":memory:"` donne une base éphémère, pour les tests.
|
|
42
42
|
|
|
43
43
|
Au runtime, l'ORM est récupérable via le registre :
|
|
44
44
|
|
package/dist/index.js
CHANGED
|
@@ -77,12 +77,12 @@ let Drizzle = class Drizzle extends Module {
|
|
|
77
77
|
* (convention Zod, alignée sur `@nodefony/mongoose` — audit config ORM 2026-06).
|
|
78
78
|
*/
|
|
79
79
|
async onKernelRegister() {
|
|
80
|
-
const validated = defineDrizzleConfig(this.options
|
|
80
|
+
const validated = defineDrizzleConfig(this.options, this.appOptions ?? {});
|
|
81
81
|
this.options = validated;
|
|
82
82
|
registerUserStore("drizzle");
|
|
83
83
|
const frameworkHost = validated.connectors.default;
|
|
84
|
-
if (validated.frameworkEntities
|
|
85
|
-
if (validated.frameworkEntities
|
|
84
|
+
if (validated.frameworkEntities && frameworkHost === void 0) this.log(`pas de connecteur "${FRAMEWORK_CONNECTOR}" (infrastructure déclarée non SQL) : schéma framework non déclaré sur drizzle — les stores durables vont au backend de cette infrastructure`, "INFO");
|
|
85
|
+
if (validated.frameworkEntities && frameworkHost !== void 0) {
|
|
86
86
|
const dialect = frameworkHost.dialect;
|
|
87
87
|
const report = registerDrizzleFrameworkStores(dialect);
|
|
88
88
|
if (report.unported.length) this.log(`schéma framework : entités non portées sur "${dialect}" → stores drizzle indisponibles pour [${report.unported.join(", ")}] (chantier multi-dialecte Ph.2.1)`, "WARNING");
|
|
@@ -246,7 +246,7 @@ var OrmGenerate = class extends OrmMigrateCommand {
|
|
|
246
246
|
], opts.json, EXIT.actionRequired);
|
|
247
247
|
return this;
|
|
248
248
|
}
|
|
249
|
-
return
|
|
249
|
+
return this.#emit({
|
|
250
250
|
formatVersion: 1,
|
|
251
251
|
connector,
|
|
252
252
|
generated: false,
|
|
@@ -289,11 +289,11 @@ var OrmGenerate = class extends OrmMigrateCommand {
|
|
|
289
289
|
], opts.json, EXIT.actionRequired, discovery);
|
|
290
290
|
return this;
|
|
291
291
|
}
|
|
292
|
-
return
|
|
292
|
+
return this.#emit({
|
|
293
293
|
formatVersion: 1,
|
|
294
294
|
connector,
|
|
295
295
|
generated: true,
|
|
296
|
-
tag: added
|
|
296
|
+
tag: added.at(-1) ?? null,
|
|
297
297
|
files: written,
|
|
298
298
|
warnings,
|
|
299
299
|
unreadable,
|
|
@@ -27,8 +27,8 @@ function applyEnvOverrides(config, declaredDefault) {
|
|
|
27
27
|
delete config.connectors.default;
|
|
28
28
|
return config;
|
|
29
29
|
}
|
|
30
|
-
if (database
|
|
31
|
-
const target = config.connectors
|
|
30
|
+
if (database?.family === "sql" && database.dialect) {
|
|
31
|
+
const target = Object.hasOwn(config.connectors, "default") ? "default" : Object.keys(config.connectors)[0];
|
|
32
32
|
const connector = target ? config.connectors[target] : void 0;
|
|
33
33
|
if (connector) {
|
|
34
34
|
connector.dialect = database.dialect;
|
|
@@ -136,7 +136,7 @@ function userTableColumns(table, dialect) {
|
|
|
136
136
|
const columns = dialect === "postgres" ? getTableConfig$1(table).columns : dialect === "mysql" ? getTableConfig$2(table).columns : getTableConfig(table).columns;
|
|
137
137
|
return new Map(columns.map((column) => [column.name, {
|
|
138
138
|
notNull: column.notNull,
|
|
139
|
-
isUnique: column.isUnique
|
|
139
|
+
isUnique: column.isUnique,
|
|
140
140
|
primary: column.primary
|
|
141
141
|
}]));
|
|
142
142
|
}
|
|
@@ -109,7 +109,7 @@ function registerDrizzleFrameworkStores(dialect) {
|
|
|
109
109
|
if (getTokenStoreFactory("drizzle")) return;
|
|
110
110
|
registerTokenStore("drizzle", (ctx) => {
|
|
111
111
|
const orm = resolveConnectedOrm(`tokenStore "drizzle"`, dialect);
|
|
112
|
-
const days = ctx
|
|
112
|
+
const days = ctx.config.tokenStore.retentionRevokedDays;
|
|
113
113
|
return DrizzleTokenStore.from(orm, void 0, typeof days === "number" ? days * 864e5 : void 0);
|
|
114
114
|
});
|
|
115
115
|
});
|
|
@@ -117,7 +117,7 @@ function registerDrizzleFrameworkStores(dialect) {
|
|
|
117
117
|
if (getAuditStoreFactory("drizzle")) return;
|
|
118
118
|
registerAuditStore("drizzle", (ctx) => {
|
|
119
119
|
const orm = resolveConnectedOrm(`audit.store "drizzle"`, dialect);
|
|
120
|
-
const days = ctx
|
|
120
|
+
const days = ctx.config.audit.retentionDays;
|
|
121
121
|
return DrizzleAuditStore.from(orm, void 0, typeof days === "number" ? days * 864e5 : void 0);
|
|
122
122
|
});
|
|
123
123
|
});
|
|
@@ -82,7 +82,7 @@ var DrizzleService = class extends Service {
|
|
|
82
82
|
*/
|
|
83
83
|
#watchers = null;
|
|
84
84
|
constructor(module) {
|
|
85
|
-
super(serviceName, module.container, module.notificationsCenter, module.options
|
|
85
|
+
super(serviceName, module.container, module.notificationsCenter, module.options);
|
|
86
86
|
this.module = module;
|
|
87
87
|
this.module.hookKernel("onBoot", async () => {
|
|
88
88
|
queryFlowMonitor.setEnabled(resolveOrmFlowEnabled(this.kernel));
|
|
@@ -96,11 +96,25 @@ var DrizzleService = class extends Service {
|
|
|
96
96
|
await this.disconnectAll().catch(() => {});
|
|
97
97
|
});
|
|
98
98
|
}
|
|
99
|
-
/**
|
|
99
|
+
/**
|
|
100
|
+
* Config du module telle que le service la TROUVE.
|
|
101
|
+
*
|
|
102
|
+
* Validée par Zod au boot du kernel ; mais le service se construit aussi hors
|
|
103
|
+
* kernel (bancs, usage direct) avec une config partielle, voire absente — et
|
|
104
|
+
* « config absente → rien à connecter » est un contrat couvert par
|
|
105
|
+
* `DrizzleService.test.ts`. Le type le dit, donc chaque lecture garde.
|
|
106
|
+
*/
|
|
100
107
|
#config() {
|
|
101
108
|
return this.module.config;
|
|
102
109
|
}
|
|
103
110
|
/**
|
|
111
|
+
* Config VALIDÉE, pour les lecteurs de migrations du plan d'administration :
|
|
112
|
+
* seul un kernel les appelle, et un kernel a validé la config au boot.
|
|
113
|
+
*/
|
|
114
|
+
#validatedConfig() {
|
|
115
|
+
return this.module.config;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
104
118
|
* Connecte tous les connecteurs déclarés en config (validée Zod).
|
|
105
119
|
*
|
|
106
120
|
* @param connect - `false` pour ENREGISTRER les ORM sans ouvrir de connexion.
|
|
@@ -127,7 +141,7 @@ var DrizzleService = class extends Service {
|
|
|
127
141
|
/** Connecte un connecteur (crée le dossier de la base SQLite si nécessaire). */
|
|
128
142
|
async #connectOne(name, cfg, connect = true) {
|
|
129
143
|
const dialect = cfg.dialect ?? "sqlite";
|
|
130
|
-
const ddl = resolveDdlMode(cfg.ddl, readMigrationEnv(this.kernel), connectorVersionsMigrations(name, appMigrationsDir(this.kernel, this.#config()
|
|
144
|
+
const ddl = resolveDdlMode(cfg.ddl, readMigrationEnv(this.kernel), connectorVersionsMigrations(name, appMigrationsDir(this.kernel, this.#config()?.migrations?.dir ?? "migrations")), cfg.filename === MEMORY_DATABASE);
|
|
131
145
|
let filename;
|
|
132
146
|
if (dialect === "sqlite") {
|
|
133
147
|
filename = cfg.filename ?? this.#defaultFilename(name);
|
|
@@ -141,17 +155,17 @@ var DrizzleService = class extends Service {
|
|
|
141
155
|
deriveSchema: ddl === "auto",
|
|
142
156
|
migrationStatus: async () => {
|
|
143
157
|
const { migrationStatusFor } = await import("../src/migrator/status.js");
|
|
144
|
-
const result = await migrationStatusFor(name, this.#
|
|
158
|
+
const result = await migrationStatusFor(name, this.#validatedConfig(), this.kernel);
|
|
145
159
|
return result.ok ? result.report : result.failure;
|
|
146
160
|
},
|
|
147
161
|
migrationPlan: async () => {
|
|
148
162
|
const { migrationPlanFor } = await import("../src/migrator/status.js");
|
|
149
|
-
const result = await migrationPlanFor(name, this.#
|
|
163
|
+
const result = await migrationPlanFor(name, this.#validatedConfig(), this.kernel);
|
|
150
164
|
return result.ok ? result.plan : result.failure;
|
|
151
165
|
},
|
|
152
166
|
applyMigrations: async () => {
|
|
153
167
|
const { applyMigrationsFor } = await import("../src/migrator/status.js");
|
|
154
|
-
const result = await applyMigrationsFor(name, this.#
|
|
168
|
+
const result = await applyMigrationsFor(name, this.#validatedConfig(), this.kernel);
|
|
155
169
|
return result.ok ? result.run : result.failure;
|
|
156
170
|
}
|
|
157
171
|
});
|
|
@@ -195,9 +209,9 @@ var DrizzleService = class extends Service {
|
|
|
195
209
|
const kernel = this.kernel;
|
|
196
210
|
const config = this.#config();
|
|
197
211
|
const env = readMigrationEnv(kernel);
|
|
198
|
-
const check = resolveCheckMode(config
|
|
212
|
+
const check = resolveCheckMode(config?.migrations?.check, env);
|
|
199
213
|
const adviseOnly = ddl === "auto";
|
|
200
|
-
const appDir = appMigrationsDir(kernel, config
|
|
214
|
+
const appDir = appMigrationsDir(kernel, config?.migrations?.dir ?? "migrations");
|
|
201
215
|
const hasMigrations = ownsSharedMigrations(name) || connectorVersionsMigrations(name, appDir);
|
|
202
216
|
if (adviseOnly && (!hasMigrations || !adviseMigrations(check, env, filename))) return;
|
|
203
217
|
if (!hasMigrations) this.log(`Drizzle « ${name} » : \`ddl: "migrate"\` sans migration à appliquer — « ${name} » n'est pas le connecteur du framework (« ${FRAMEWORK_CONNECTOR} ») et aucune migration n'est versionnée sous son dossier. Son schéma n'est ni dérivé ni migré par le framework : \`nodefony orm:generate --connector ${name}\` écrit sa première migration.`, "WARNING");
|
|
@@ -207,14 +221,14 @@ var DrizzleService = class extends Service {
|
|
|
207
221
|
url: cfg.url
|
|
208
222
|
};
|
|
209
223
|
const sources = await defaultMigrationSources(appDir, {
|
|
210
|
-
framework: config
|
|
224
|
+
framework: config?.frameworkEntities !== false,
|
|
211
225
|
connector: name
|
|
212
226
|
});
|
|
213
227
|
const migrator = new DrizzleMigrator({
|
|
214
228
|
connector: name,
|
|
215
229
|
...target,
|
|
216
230
|
sources,
|
|
217
|
-
lockTimeoutMs: config
|
|
231
|
+
lockTimeoutMs: config?.migrations?.lockTimeoutMs
|
|
218
232
|
});
|
|
219
233
|
if (adviseOnly) {
|
|
220
234
|
await this.#publishReadiness(name, migrator, ddl, "warn");
|
|
@@ -256,7 +270,7 @@ var DrizzleService = class extends Service {
|
|
|
256
270
|
if (!kernel) return;
|
|
257
271
|
try {
|
|
258
272
|
const plan = await migrator.status();
|
|
259
|
-
const mode = this.#config()
|
|
273
|
+
const mode = this.#config()?.migrations?.divergence ?? "report";
|
|
260
274
|
const report = buildReport(plan, {
|
|
261
275
|
ddl,
|
|
262
276
|
divergence: mode === "off" ? null : await describeDivergence(plan),
|
|
@@ -281,7 +295,7 @@ var DrizzleService = class extends Service {
|
|
|
281
295
|
*/
|
|
282
296
|
#watch(name, migrator, ddl, check) {
|
|
283
297
|
if (check === "off") return;
|
|
284
|
-
|
|
298
|
+
this.#watchers ??= /* @__PURE__ */ new Map();
|
|
285
299
|
const timer = setInterval(() => {
|
|
286
300
|
this.#publishReadiness(name, migrator, ddl, check).catch(() => void 0);
|
|
287
301
|
}, READINESS_POLL_MS);
|
|
@@ -145,7 +145,7 @@ var DrizzleAuditStore = class DrizzleAuditStore {
|
|
|
145
145
|
const hasNext = rows.length > limit;
|
|
146
146
|
const pageRows = hasNext ? rows.slice(0, limit) : rows;
|
|
147
147
|
const items = pageRows.map((row) => this.#toEvent(row));
|
|
148
|
-
const last = pageRows
|
|
148
|
+
const last = pageRows.at(-1);
|
|
149
149
|
return {
|
|
150
150
|
items,
|
|
151
151
|
limit,
|
|
@@ -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
|
|
48
|
+
const dialect = cfg.dialect;
|
|
49
49
|
if (dialect !== "sqlite") return {
|
|
50
50
|
dialect,
|
|
51
51
|
url: cfg.url
|
|
@@ -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
|
|
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
|
|
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
|
|
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.
|
|
116
|
+
return Object.entries(snapshot.tables ?? {}).map(([key, t]) => t.name ?? key);
|
|
117
117
|
} catch {
|
|
118
118
|
return [];
|
|
119
119
|
}
|
|
@@ -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
|
|
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"))].
|
|
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
|
|
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
|
|
59
|
+
this.#lost = error instanceof Error ? error.message : String(error);
|
|
60
60
|
});
|
|
61
61
|
await client.connect();
|
|
62
62
|
this.#lost = null;
|
|
@@ -309,18 +309,15 @@ function summaryOf(plan, verdict, divergence = null) {
|
|
|
309
309
|
}
|
|
310
310
|
case "drift": {
|
|
311
311
|
const parts = [];
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
|
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:
|
|
102
|
-
appliedBy:
|
|
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
|
|
@@ -69,7 +69,7 @@ function runGenerate({ cwd, configRel, name, label, regenerateCommand }) {
|
|
|
69
69
|
cwd,
|
|
70
70
|
encoding: "utf8"
|
|
71
71
|
});
|
|
72
|
-
const output =
|
|
72
|
+
const output = toolOutput(result);
|
|
73
73
|
if (result.status !== 0 || !generationHappened(output)) {
|
|
74
74
|
const awaited = topLevelAwaitFailure(output);
|
|
75
75
|
if (awaited !== null) throw new MigrationToolError(generationTopLevelAwait(label, awaited.file, awaited.line, regenerateCommand ?? `nodefony orm:generate --name ${name}`));
|
|
@@ -107,7 +107,7 @@ function runIntrospect({ cwd, configRel, label }) {
|
|
|
107
107
|
cwd,
|
|
108
108
|
encoding: "utf8"
|
|
109
109
|
});
|
|
110
|
-
const output =
|
|
110
|
+
const output = toolOutput(result);
|
|
111
111
|
if (result.status !== 0) throw new MigrationToolError(introspectFailed(label, result.status, output));
|
|
112
112
|
return output;
|
|
113
113
|
}
|
|
@@ -228,9 +228,9 @@ const BLOCKING_PATTERNS = [
|
|
|
228
228
|
id: "colonne-neuve-puis-index-unique",
|
|
229
229
|
pattern: /\bCREATE\s+UNIQUE\s+INDEX\b/i,
|
|
230
230
|
detect: (code) => {
|
|
231
|
-
const added = [...code.matchAll(/\bADD\s+(?:COLUMN\s+)?[`"']?(\w+)[`"']?/gi)].
|
|
231
|
+
const added = [...code.matchAll(/\bADD\s+(?:COLUMN\s+)?[`"']?(\w+)[`"']?/gi)].flatMap(([, name]) => name === void 0 ? [] : [name.toLowerCase()]);
|
|
232
232
|
if (added.length === 0) return false;
|
|
233
|
-
return [...code.matchAll(/\bCREATE\s+UNIQUE\s+INDEX\b[^;\n]*?\(([^)]*)\)/gi)].some((
|
|
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)));
|
|
234
234
|
},
|
|
235
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",
|
|
236
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"
|
|
@@ -287,11 +287,11 @@ function isInteractivePromptFailure(output) {
|
|
|
287
287
|
*/
|
|
288
288
|
function topLevelAwaitFailure(output) {
|
|
289
289
|
const plain = output.replace(/\u001B\[[0-9;]*m/g, "");
|
|
290
|
-
const
|
|
291
|
-
return
|
|
292
|
-
file
|
|
293
|
-
line: Number(
|
|
294
|
-
}
|
|
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
295
|
}
|
|
296
296
|
/**
|
|
297
297
|
* Pose le marqueur de format en tête des `.sql` d'un dossier qui ne l'ont pas.
|
|
@@ -316,5 +316,19 @@ function stampFormatMarker(dir) {
|
|
|
316
316
|
}
|
|
317
317
|
return stamped;
|
|
318
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
|
+
}
|
|
319
333
|
//#endregion
|
|
320
334
|
export { FORMAT_MARKER, auditMigrationSql, generationHappened, isInteractivePromptFailure, resolveDrizzleKitBin, runGenerate, runIntrospect, stampFormatMarker, topLevelAwaitFailure };
|
|
@@ -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 (
|
|
72
|
-
const
|
|
73
|
-
const
|
|
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(
|
|
77
|
-
keptSelect.push(
|
|
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;
|
|
@@ -224,10 +224,10 @@ function describeOwner(name) {
|
|
|
224
224
|
driver: ""
|
|
225
225
|
};
|
|
226
226
|
}
|
|
227
|
-
const klass = orm
|
|
227
|
+
const klass = orm.constructor?.name;
|
|
228
228
|
let driver = "";
|
|
229
229
|
try {
|
|
230
|
-
driver = orm
|
|
230
|
+
driver = orm.describeConnection?.().driver ?? "";
|
|
231
231
|
} catch {
|
|
232
232
|
driver = "";
|
|
233
233
|
}
|
|
@@ -268,7 +268,7 @@ const SQL_DRIVERS = /* @__PURE__ */ new Set([
|
|
|
268
268
|
*/
|
|
269
269
|
function knownConnectors(config) {
|
|
270
270
|
const names = new Set(ormRegistry.list());
|
|
271
|
-
for (const name of Object.keys(config.connectors
|
|
271
|
+
for (const name of Object.keys(config.connectors)) names.add(name);
|
|
272
272
|
return [...names].sort();
|
|
273
273
|
}
|
|
274
274
|
/**
|
|
@@ -292,7 +292,7 @@ function knownConnectors(config) {
|
|
|
292
292
|
* @returns la réponse, discriminée par `kind`.
|
|
293
293
|
*/
|
|
294
294
|
function resolveConnector(connector, config, env, kernel, options = {}) {
|
|
295
|
-
const declared = config.connectors
|
|
295
|
+
const declared = config.connectors[connector];
|
|
296
296
|
if (!declared) {
|
|
297
297
|
if (ormRegistry.has(connector)) {
|
|
298
298
|
const owner = describeOwner(connector);
|
|
@@ -71,7 +71,8 @@ async function readJournal(dir, source) {
|
|
|
71
71
|
}
|
|
72
72
|
let journal;
|
|
73
73
|
try {
|
|
74
|
-
|
|
74
|
+
const parsed = JSON.parse(raw);
|
|
75
|
+
journal = typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
75
76
|
} catch (e) {
|
|
76
77
|
throw new MigrationVerdictError({
|
|
77
78
|
code: "NF_MIGRATE_UNKNOWN_FORMAT",
|
|
@@ -91,21 +92,25 @@ async function readJournal(dir, source) {
|
|
|
91
92
|
facts: { file },
|
|
92
93
|
nextActions: []
|
|
93
94
|
}, `Le journal des migrations « ${file} » ne porte pas de liste « entries » : ce n'est pas un journal que ce format sait lire.`);
|
|
94
|
-
|
|
95
|
+
const version = String(journal.version);
|
|
96
|
+
if (!SUPPORTED_JOURNAL_VERSIONS.includes(version)) throw new MigrationVerdictError({
|
|
95
97
|
code: "NF_MIGRATE_UNKNOWN_FORMAT",
|
|
96
98
|
connector: "",
|
|
97
99
|
source,
|
|
98
100
|
facts: {
|
|
99
101
|
file,
|
|
100
|
-
journalVersion:
|
|
102
|
+
journalVersion: version,
|
|
101
103
|
supported: SUPPORTED_JOURNAL_VERSIONS
|
|
102
104
|
},
|
|
103
105
|
nextActions: [{
|
|
104
106
|
command: "npm update @nodefony/drizzle",
|
|
105
107
|
args: ["update", "@nodefony/drizzle"]
|
|
106
108
|
}]
|
|
107
|
-
}, `Le journal de migrations de la source « ${source} » est en version ${
|
|
108
|
-
return
|
|
109
|
+
}, `Le journal de migrations de la source « ${source} » est en version ${version}, que cette version du framework ne sait pas lire (reconnue : ${SUPPORTED_JOURNAL_VERSIONS.join(", ")}).`);
|
|
110
|
+
return {
|
|
111
|
+
version,
|
|
112
|
+
entries: journal.entries
|
|
113
|
+
};
|
|
109
114
|
}
|
|
110
115
|
/**
|
|
111
116
|
* Lit un fichier de migration, valide son format et découpe ses statements.
|
|
@@ -370,8 +375,8 @@ function scanLine(line, needle, grammar, curseur) {
|
|
|
370
375
|
*/
|
|
371
376
|
function delimiteurDollar(line, debut) {
|
|
372
377
|
let i = debut + 1;
|
|
373
|
-
while (i < line.length && /[A-Za-z0-9_]/.test(line
|
|
374
|
-
if (i === debut + 1 && /[0-9]/.test(line
|
|
378
|
+
while (i < line.length && /[A-Za-z0-9_]/.test(line.charAt(i))) {
|
|
379
|
+
if (i === debut + 1 && /[0-9]/.test(line.charAt(i))) return null;
|
|
375
380
|
i += 1;
|
|
376
381
|
}
|
|
377
382
|
return line[i] === "$" ? line.slice(debut, i + 1) : null;
|
|
@@ -390,7 +395,7 @@ function delimiteurDollar(line, debut) {
|
|
|
390
395
|
function createdTables(files) {
|
|
391
396
|
const found = /* @__PURE__ */ new Set();
|
|
392
397
|
const pattern = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"']?([A-Za-z0-9_$]+)[`"']?/gi;
|
|
393
|
-
for (const file of files) for (const statement of file.statements) for (const
|
|
398
|
+
for (const file of files) for (const statement of file.statements) for (const [, table] of statement.matchAll(pattern)) if (table !== void 0) found.add(table);
|
|
394
399
|
return [...found];
|
|
395
400
|
}
|
|
396
401
|
/**
|