Files
strapi/examples/complex/scripts/db-sqlite.js
T
Ben Irvin 97f5cde810 fix(core): preserve createdBy/updatedBy on drafts created by discard-drafts migration (#26461)
* fix(core): preserve createdBy/updatedBy on drafts created by discard-drafts migration

Stage 1 of the v4→v5 discard-drafts migration now copies join-column foreign
keys when cloning published rows to drafts, so creator metadata is no longer
NULL on the new draft rows. Adds a focused migration test and extends the
complex example migration validator to catch this regression.

Fixes #26460

* fix(core): skip virtual join columns in discard-drafts stage 1

i18n localizations reuses document_id as a virtual join column; copying it
alongside the scalar duplicated the column in INSERT…SELECT and broke v4→v5
migration. Dedupe clone columns and apply the same filter in later stages.
2026-05-28 12:41:22 +02:00

159 lines
4.9 KiB
JavaScript

#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const SCRIPT_DIR = __dirname;
const COMPLEX_DIR = path.resolve(SCRIPT_DIR, '..');
const MONOREPO_ROOT = path.resolve(COMPLEX_DIR, '../..');
const V4_PROJECT_DIR = process.env.V4_OUTSIDE_DIR
? path.resolve(process.cwd(), process.env.V4_OUTSIDE_DIR)
: path.resolve(MONOREPO_ROOT, '..', 'complex-v4');
/**
* SQLite lives entirely as files; no container and no compose runtime.
*
* The v4 project stores its database under V4_PROJECT_DIR/.tmp/data.db.
* v5 reads/writes the same file so snapshots are interchangeable.
*/
const DATABASE_FILENAME =
process.env.SQLITE_DATABASE_FILENAME ||
process.env.DATABASE_FILENAME ||
path.join(V4_PROJECT_DIR, '.tmp', 'data.db');
const SNAPSHOTS_DIR = path.join(COMPLEX_DIR, 'snapshots');
const command = process.argv[2];
const snapshotName = process.argv[3];
function ensureSnapshotsDir() {
if (!fs.existsSync(SNAPSHOTS_DIR)) {
fs.mkdirSync(SNAPSHOTS_DIR, { recursive: true });
}
}
function snapshotPath(name) {
return path.join(SNAPSHOTS_DIR, `sqlite-${name}.db`);
}
function requireBetterSqlite() {
try {
// eslint-disable-next-line global-require
return require('better-sqlite3');
} catch (error) {
throw new Error(
'better-sqlite3 is required for sqlite operations. It is a peer dep of @strapi/strapi and should be present via workspace hoisting; if not, install it in examples/complex.'
);
}
}
switch (command) {
case 'start':
// No-op: sqlite is file-based.
console.log('✅ SQLite is file-based; nothing to start.');
console.log(` Database file: ${DATABASE_FILENAME}`);
break;
case 'stop':
// No-op: sqlite is file-based.
console.log('✅ SQLite is file-based; nothing to stop.');
break;
case 'snapshot': {
if (!snapshotName) {
console.error('Error: Snapshot name is required');
console.error('Usage: node db-sqlite.js snapshot <name>');
process.exit(1);
}
if (!fs.existsSync(DATABASE_FILENAME)) {
console.error(`Error: Database file not found: ${DATABASE_FILENAME}`);
console.error('Run the v4 app with `yarn develop:sqlite` and seed first to create it.');
process.exit(1);
}
ensureSnapshotsDir();
const target = snapshotPath(snapshotName);
fs.copyFileSync(DATABASE_FILENAME, target);
console.log(`✅ Snapshot created: ${target}`);
break;
}
case 'restore': {
if (!snapshotName) {
console.error('Error: Snapshot name is required');
console.error('Usage: node db-sqlite.js restore <name>');
process.exit(1);
}
const source = snapshotPath(snapshotName);
if (!fs.existsSync(source)) {
console.error(`Error: Snapshot not found: ${source}`);
process.exit(1);
}
// Remove walk-ahead / shared-memory sidecars so they don't conflict with the
// restored file's transaction state.
for (const suffix of ['', '-wal', '-shm', '-journal']) {
const sidecar = `${DATABASE_FILENAME}${suffix}`;
if (fs.existsSync(sidecar)) {
try {
fs.unlinkSync(sidecar);
} catch {
/* best-effort */
}
}
}
fs.mkdirSync(path.dirname(DATABASE_FILENAME), { recursive: true });
fs.copyFileSync(source, DATABASE_FILENAME);
console.log(`✅ Snapshot restored: ${snapshotName} -> ${DATABASE_FILENAME}`);
break;
}
case 'wipe':
for (const suffix of ['', '-wal', '-shm', '-journal']) {
const f = `${DATABASE_FILENAME}${suffix}`;
if (fs.existsSync(f)) {
fs.unlinkSync(f);
}
}
console.log(`✅ SQLite database file removed: ${DATABASE_FILENAME}`);
break;
case 'check': {
if (!fs.existsSync(DATABASE_FILENAME)) {
console.log('📊 No database file found (empty or wiped)');
break;
}
const Database = requireBetterSqlite();
const db = new Database(DATABASE_FILENAME, { readonly: true, fileMustExist: true });
try {
const tables = db
.prepare(
"SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
)
.all();
if (tables.length === 0) {
console.log('📊 No tables found (database is empty or wiped)');
break;
}
console.log('📊 Database Tables (row counts):\n');
console.log('Table Name | Row Count');
console.log('------------------------------------|----------');
for (const { name } of tables) {
// SQLite doesn't keep approximate row stats; use exact COUNT(*) here.
const { c } = db.prepare(`SELECT COUNT(*) AS c FROM "${name}"`).get();
const padded = name.padEnd(35);
console.log(`${padded} | ${c}`);
}
} finally {
db.close();
}
break;
}
default:
console.error('Error: Unknown command');
console.error('Usage: node db-sqlite.js <start|stop|snapshot|restore|wipe|check> [name]');
process.exit(1);
}