Last active
January 15, 2023 15:32
-
-
Save MaximStone/891b042a93f17db13f04a98e5101a9df to your computer and use it in GitHub Desktop.
More flexable db migrations for Strapi. In my case several strapi instances were working as Kuber service. Migration should be executed only once.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
// Simple example (for similar situations I suggest to use sql-queries instead of js-based migrations) | |
// | |
// migrationUtils.js should be in [strapi-project]/database folder for this example. But it's up to you. | |
// Below the content of file: [strapi-project]/database/migrations/2023.01.15.correct-hotel-stars.js | |
const { prepareMigration } = require('../migrationUtils') | |
const path = require('path') | |
const migration = (knex) => { | |
const hotels = await global.strapi.entityService.findMany('api::hotel.hotel', { | |
fields: ['id', 'name', 'stars'] | |
}) | |
for (let i = 0; i < hotels.length; i++) { | |
const hotel = hotels[i] | |
try { | |
await global.strapi.entityService.update('api::hotel.hotel', hotel.id, { | |
data: { | |
stars: hotel.name?.inlcludes('Mariot') ? 5 : (hotel.stars || 3) | |
} | |
}) | |
} catch (error) { | |
strapi.log.warn(`Migration is not able to update stars. Skipping the hotel "${hotel.name}" with id ${hotel.id}`) | |
continue | |
} | |
} | |
} | |
const { up } = prepareMigration( | |
migration, | |
path.basename(__filename), | |
'My db migration that depends on "hotels" table', | |
['hotels'] | |
) | |
module.exports = { up } | |
*/ | |
module.exports = { | |
prepareMigration: ( | |
migrationFunctionBody, | |
migrationFileName, | |
migrationRunDescription = '', | |
dependencyTables = [], | |
maxAttempts = 10, | |
removeMigrationHistoryIfFails = true, | |
delayBetweenAttemptsMs = 45000 | |
) => { | |
return { | |
up: async (_knex) => { | |
const db = global.strapi.db | |
const localKnex = db.connection | |
let uniqueScriptExecutor = null | |
let attempt = 1 | |
let isDBEmpty = true | |
let migrationRunAllowed = false | |
const cancelMigration = async (message) => { | |
global.strapi.log.warn(`Migration canceled: ${message}`) | |
if (removeMigrationHistoryIfFails && (await db.getSchemaConnection().hasTable('strapi_migrations'))) { | |
// have to wait until record appears in the DB table | |
setTimeout(async () => { | |
await localKnex('strapi_migrations').withSchema(db.getSchemaConnection()._schema).delete().where({ | |
name: migrationFileName | |
}) | |
}, 1000) | |
} else { | |
global.strapi.log.info(`Migration history record remains.`) | |
} | |
if (uniqueScriptExecutor) { | |
await global.strapi.store.delete({ | |
type: 'migration', | |
name: 'executor', | |
key: migrationFileName | |
}) | |
} | |
} | |
if (typeof migrationFunctionBody !== 'function') { | |
return cancelMigration('First argument (migrationFunctionBody) must be a Function.') | |
} | |
if (typeof migrationFileName !== 'string' || migrationFileName === '') { | |
return cancelMigration('Second argument must contain a migration filename.') | |
} | |
if (attempt >= maxAttempts) { | |
return cancelMigration('Max attempts reached') | |
} | |
const recurrentOperation = async (firstTime = false) => { | |
if (firstTime) { | |
global.strapi.log.info(`Migration: ${migrationRunDescription || migrationFileName}`) | |
} | |
global.strapi.log.info(`---- Attempt #${attempt} ----`) | |
isDBEmpty = !(await db.getSchemaConnection().hasTable('strapi_core_store_settings')) | |
if (!isDBEmpty) { | |
const executor = await global.strapi.store.get({ | |
type: 'migration', | |
name: 'executor', | |
key: migrationFileName | |
}) | |
if (!executor && uniqueScriptExecutor) { | |
return global.strapi.log.warn('Migration executor was removed from db. Quit without removing migration db record.') | |
} | |
if (!executor && !uniqueScriptExecutor) { | |
uniqueScriptExecutor = executor | |
migrationRunAllowed = true | |
} else if (uniqueScriptExecutor && uniqueScriptExecutor === executor) { | |
migrationRunAllowed = true | |
} | |
} else { | |
global.strapi.log.warn( | |
`Migration cannot be executed. Strapi db schema is not ready.${ | |
attempt < maxAttempts ? ` Task will run again in ${delayBetweenAttemptsMs}ms.` : '' | |
}` | |
) | |
if (attempt < maxAttempts) { | |
setTimeout(recurrentOperation, delayBetweenAttemptsMs) | |
return | |
} else { | |
return cancelMigration('Max attempts reached') | |
} | |
} | |
if (!migrationRunAllowed) { | |
return global.strapi.log.warn('Migration not allowed to run. Quit without removing migration db record.') | |
} | |
if (migrationRunAllowed && !isDBEmpty) { | |
uniqueScriptExecutor = await global.strapi.store.set({ | |
type: 'migration', | |
name: 'executor', | |
key: migrationFileName, | |
value: Date.now() | |
}) | |
const notReadyTables = [] | |
for (let i = 0; i < dependencyTables.length; i++) { | |
const dependencyTable = dependencyTables[i] | |
global.strapi.log.info(`Migration: checking if "${dependencyTable}" table does exist`) | |
if (!(await db.getSchemaConnection().hasTable(dependencyTable))) { | |
global.strapi.log.info(`Migration: "${dependencyTable}" table does not exist.`) | |
notReadyTables.push(dependencyTable) | |
} | |
} | |
if (notReadyTables.length) { | |
global.strapi.log.warn( | |
`Migration cannot be executed. Some dependency tables are not ready (${notReadyTables.join(', ')}).${ | |
attempt < maxAttempts ? ` Task will run again in ${delayBetweenAttemptsMs}ms.` : '' | |
}` | |
) | |
if (attempt < maxAttempts) { | |
setTimeout(recurrentOperation, delayBetweenAttemptsMs) | |
} else { | |
return cancelMigration('Max attempts reached') | |
} | |
} else { | |
global.strapi.log.info('Migration: Dependency tables are ready.') | |
await migrationFunctionBody(_knex) | |
if (uniqueScriptExecutor) { | |
await global.strapi.store.delete({ | |
type: 'migration', | |
name: 'executor', | |
key: migrationFileName | |
}) | |
} | |
global.strapi.log.info(`Migration has been finished successfully!`) | |
} | |
attempt++ | |
} | |
} | |
await recurrentOperation(true) | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment