chore(migrations): renumber 108_add_backup_paths to 109 to avoid upstream collision

upstream/beta independently shipped 108_seed_sl_email_template_translations.js
(Slovenian email template translations) using the migration number
this branch had already claimed for 108_add_backup_paths.js. Knex's
filename-based ordering would have caused both to attempt the slot
at merge time.

Renamed via `git mv` so file history is preserved. All five
references updated in lockstep:
  - backend/src/services/_backupPathsBoot.js (require + comments)
  - backend/src/services/backupService.js (LEGACY_BACKUP_PATHS comment)
  - 3 integration test files (require + "migration 108" prose)
  - migration's own header comment, with a paragraph explaining the
    rename so reviewers don't wonder why the number jumped

**No data-migration impact for installs that already ran the
108-named version** (Ralf's beta, primarily): the migration's body
is idempotent — createTable is guarded by `hasTable`, and the seed
uses `onConflict('path').ignore()`. So when 109 runs against an
install whose backup_paths table is already populated, both the
schema step and the seed step no-op cleanly. The orphaned
`108_add_backup_paths.js` row in the `migrations` tracking table
sits harmlessly alongside the new `109_add_backup_paths.js` row.

No data lost, no double-insert, no schema drift. Mechanical rename
ahead of the PR opening.
This commit is contained in:
Luca
2026-06-01 00:31:08 +02:00
parent 43cb0ea4bf
commit 07f9110674
6 changed files with 22 additions and 14 deletions
@@ -72,7 +72,7 @@ describe('GET /api/admin/system-health/backup-coverage', () => {
async function restoreDefaultPaths() { async function restoreDefaultPaths() {
await db('backup_paths').del(); await db('backup_paths').del();
const { DEFAULT_PATHS } = require('../../migrations/core/108_add_backup_paths'); const { DEFAULT_PATHS } = require('../../migrations/core/109_add_backup_paths');
await db('backup_paths').insert(DEFAULT_PATHS.map((row) => ({ await db('backup_paths').insert(DEFAULT_PATHS.map((row) => ({
...row, ...row,
created_at: new Date(), created_at: new Date(),
@@ -1,7 +1,7 @@
/** /**
* Pins the Stage-B refactor that lifted the file-backup walker's * Pins the Stage-B refactor that lifted the file-backup walker's
* subdirectory list out of hard-coded JS into the `backup_paths` * subdirectory list out of hard-coded JS into the `backup_paths`
* table seeded by migration 108. * table seeded by migration 109.
* *
* Scenarios: * Scenarios:
* 1. Walker reads canonical seed → all 7 default subdirs walked * 1. Walker reads canonical seed → all 7 default subdirs walked
@@ -53,7 +53,7 @@ describe('backupService — configurable walker (backup_paths)', () => {
await db('backup_paths').del(); await db('backup_paths').del();
const { const {
DEFAULT_PATHS, DEFAULT_PATHS,
} = require('../../migrations/core/108_add_backup_paths'); } = require('../../migrations/core/109_add_backup_paths');
await db('backup_paths').insert(DEFAULT_PATHS.map((row) => ({ await db('backup_paths').insert(DEFAULT_PATHS.map((row) => ({
...row, ...row,
created_at: new Date(), created_at: new Date(),
@@ -61,7 +61,7 @@ describe('backupService — configurable walker (backup_paths)', () => {
}))); })));
}); });
it('migration 108 seeds the canonical 7 paths', async () => { it('migration 109 seeds the canonical 7 paths', async () => {
const rows = await db('backup_paths').orderBy('display_order', 'asc').select(); const rows = await db('backup_paths').orderBy('display_order', 'asc').select();
expect(rows.map((r) => r.path)).toEqual([ expect(rows.map((r) => r.path)).toEqual([
'events/active', 'events/active',
@@ -60,8 +60,8 @@ describe('backupService — per-Stage-B-path statistics', () => {
]).onConflict('setting_key').merge(); ]).onConflict('setting_key').merge();
fs.mkdirSync(path.join(storagePath, 'destination'), { recursive: true }); fs.mkdirSync(path.join(storagePath, 'destination'), { recursive: true });
// Restore canonical backup_paths from migration 108 // Restore canonical backup_paths from migration 109
const { DEFAULT_PATHS } = require('../../migrations/core/108_add_backup_paths'); const { DEFAULT_PATHS } = require('../../migrations/core/109_add_backup_paths');
await db('backup_paths').del(); await db('backup_paths').del();
await db('backup_paths').insert(DEFAULT_PATHS.map((row) => ({ await db('backup_paths').insert(DEFAULT_PATHS.map((row) => ({
...row, ...row,
@@ -170,7 +170,7 @@ describe('backupService — per-Stage-B-path statistics', () => {
// — once via each path. Per-path stats then attribute the file to the // — once via each path. Per-path stats then attribute the file to the
// longest-prefix-matching path BOTH times, producing inflated counts. // longest-prefix-matching path BOTH times, producing inflated counts.
// //
// The canonical seed in migration 108 contains no overlapping pairs, // The canonical seed in migration 109 contains no overlapping pairs,
// so this isn't exercised in practice. But an admin who hand-adds a // so this isn't exercised in practice. But an admin who hand-adds a
// broad row that overlaps an existing nested one will see double // broad row that overlaps an existing nested one will see double
// counts in their next backup's statistics + the destination will // counts in their next backup's statistics + the destination will
@@ -1,5 +1,13 @@
/** /**
* Migration 108 config-driven backup walker. * Migration 109 config-driven backup walker.
*
* (Originally numbered 108 on bugfix/crm-backup. Renumbered to 109
* before merge because upstream/beta independently shipped
* 108_seed_sl_email_template_translations.js. The createTable is
* idempotent via `hasTable` guard and the seed uses
* `onConflict('path').ignore()`, so beta installs that ran the
* 108-named version of this file get a harmless no-op when 109
* runs against the already-seeded table.)
* *
* Stage B of the three-stage backup-hardening plan. The file-backup * Stage B of the three-stage backup-hardening plan. The file-backup
* walker (`getFilesToBackupInternal` in backupService.js) historically * walker (`getFilesToBackupInternal` in backupService.js) historically
+5 -5
View File
@@ -4,7 +4,7 @@
* **Why this exists** * **Why this exists**
* *
* Knex won't re-run an applied migration, so once migration * Knex won't re-run an applied migration, so once migration
* 108_add_backup_paths.js has run, any later default we want to add * 109_add_backup_paths.js has run, any later default we want to add
* (a new subdirectory shipped by a future feature) would never reach * (a new subdirectory shipped by a future feature) would never reach
* already-deployed installs. The historical fix for this kind of * already-deployed installs. The historical fix for this kind of
* "schema is fine, seed drifted" problem is the boot-time self-heal * "schema is fine, seed drifted" problem is the boot-time self-heal
@@ -14,7 +14,7 @@
* *
* **Authoritative list** * **Authoritative list**
* *
* The list of defaults lives on migration 108 itself * The list of defaults lives on migration 109 itself
* (`DEFAULT_PATHS` export) — one source of truth that both the * (`DEFAULT_PATHS` export) — one source of truth that both the
* migration and this seeder read. Tests assert these two stay in * migration and this seeder read. Tests assert these two stay in
* lockstep. * lockstep.
@@ -22,14 +22,14 @@
* **Failure semantics** * **Failure semantics**
* *
* If the table doesn't exist yet (migrations haven't run, fresh * If the table doesn't exist yet (migrations haven't run, fresh
* install before migration 108 lands, etc.) we no-op and log. The * install before migration 109 lands, etc.) we no-op and log. The
* walker has a hard-coded `LEGACY_DEFAULTS` fallback for the same * walker has a hard-coded `LEGACY_DEFAULTS` fallback for the same
* reason — defense in depth so "Run Backup Now" can never silently * reason — defense in depth so "Run Backup Now" can never silently
* ship a files-only manifest because of a seed issue. See * ship a files-only manifest because of a seed issue. See
* `backupService.js` getFilesToBackupInternal. * `backupService.js` getFilesToBackupInternal.
*/ */
const { DEFAULT_PATHS } = require('../../migrations/core/108_add_backup_paths'); const { DEFAULT_PATHS } = require('../../migrations/core/109_add_backup_paths');
let booted = false; let booted = false;
@@ -45,7 +45,7 @@ async function seedBackupPathsAtBoot(db, logger) {
if (booted) return { seeded: [] }; if (booted) return { seeded: [] };
if (!(await db.schema.hasTable('backup_paths'))) { if (!(await db.schema.hasTable('backup_paths'))) {
log.warn('backup_paths table missing at boot — self-heal skipped (migration 108 may not have run yet)'); log.warn('backup_paths table missing at boot — self-heal skipped (migration 109 may not have run yet)');
return { seeded: [] }; return { seeded: [] };
} }
+1 -1
View File
@@ -419,7 +419,7 @@ async function scanDirectory(dirPath, fileList, basePath, excludePatterns = [])
/** /**
* Hard-coded fallback when `backup_paths` is missing/empty. Mirrors * Hard-coded fallback when `backup_paths` is missing/empty. Mirrors
* the canonical seed in migration 108 — kept here as defense in depth * the canonical seed in migration 109 — kept here as defense in depth
* so the walker can never silently degrade to "no directories scanned" * so the walker can never silently degrade to "no directories scanned"
* because of a seed problem. * because of a seed problem.
* *