Skip to content

Instantly share code, notes, and snippets.

@MaximStone
Last active January 15, 2023 15:32
Show Gist options
  • Save MaximStone/891b042a93f17db13f04a98e5101a9df to your computer and use it in GitHub Desktop.
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.
/*
// 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