2021-02-18 18:42:28 +01:00
|
|
|
'use strict';
|
|
|
|
|
2021-03-11 10:41:35 +01:00
|
|
|
const { singular } = require('pluralize');
|
2021-03-15 18:51:32 +01:00
|
|
|
const { has, omit, pick, orderBy } = require('lodash/fp');
|
2021-03-11 10:41:35 +01:00
|
|
|
const { shouldBeProcessed, getUpdatesInfo } = require('./utils');
|
2021-02-18 18:42:28 +01:00
|
|
|
|
2021-02-25 17:40:14 +01:00
|
|
|
const BATCH_SIZE = 1000;
|
|
|
|
|
2021-03-02 16:42:17 +01:00
|
|
|
const TMP_TABLE_NAME = '__tmp__i18n_field_migration';
|
|
|
|
|
|
|
|
const batchInsertInTmpTable = async (updatesInfo, trx) => {
|
|
|
|
const tmpEntries = [];
|
|
|
|
updatesInfo.forEach(({ entriesIdsToUpdate, attributesValues }) => {
|
|
|
|
entriesIdsToUpdate.forEach(id => {
|
|
|
|
tmpEntries.push({ id, ...attributesValues });
|
|
|
|
});
|
|
|
|
});
|
|
|
|
await trx.batchInsert(TMP_TABLE_NAME, tmpEntries, 100);
|
2021-03-01 11:26:44 +01:00
|
|
|
};
|
|
|
|
|
2021-03-02 16:42:17 +01:00
|
|
|
const batchUpdate = async (updatesInfo, trx, model) => {
|
|
|
|
const promises = updatesInfo.map(({ entriesIdsToUpdate, attributesValues }) =>
|
|
|
|
trx
|
|
|
|
.from(model.collectionName)
|
|
|
|
.update(attributesValues)
|
|
|
|
.whereIn('id', entriesIdsToUpdate)
|
|
|
|
);
|
|
|
|
await Promise.all(promises);
|
|
|
|
};
|
2021-03-01 11:26:44 +01:00
|
|
|
|
2021-03-02 16:42:17 +01:00
|
|
|
const updateFromTmpTable = async ({ model, trx, attributesToMigrate }) => {
|
|
|
|
const collectionName = model.collectionName;
|
|
|
|
if (model.client === 'pg') {
|
|
|
|
const substitutes = attributesToMigrate.map(() => '?? = ??.??').join(',');
|
2021-03-11 10:41:35 +01:00
|
|
|
const bindings = [collectionName];
|
2021-03-02 16:42:17 +01:00
|
|
|
attributesToMigrate.forEach(attr => bindings.push(attr, TMP_TABLE_NAME, attr));
|
|
|
|
bindings.push(TMP_TABLE_NAME, collectionName, TMP_TABLE_NAME);
|
|
|
|
|
|
|
|
await trx.raw(`UPDATE ?? SET ${substitutes} FROM ?? WHERE ??.id = ??.id;`, bindings);
|
|
|
|
} else if (model.client === 'mysql') {
|
|
|
|
const substitutes = attributesToMigrate.map(() => '??.?? = ??.??').join(',');
|
2021-03-11 10:41:35 +01:00
|
|
|
const bindings = [collectionName, TMP_TABLE_NAME, collectionName, TMP_TABLE_NAME];
|
2021-03-02 16:42:17 +01:00
|
|
|
attributesToMigrate.forEach(attr => bindings.push(collectionName, attr, TMP_TABLE_NAME, attr));
|
|
|
|
|
|
|
|
await trx.raw(`UPDATE ?? JOIN ?? ON ??.id = ??.id SET ${substitutes};`, bindings);
|
|
|
|
}
|
|
|
|
};
|
2021-02-25 17:40:14 +01:00
|
|
|
|
2021-03-02 16:42:17 +01:00
|
|
|
const createTmpTable = async ({ ORM, attributesToMigrate, model }) => {
|
2021-02-25 17:40:14 +01:00
|
|
|
const columnsToCopy = ['id', ...attributesToMigrate];
|
|
|
|
await ORM.knex.schema.dropTableIfExists(TMP_TABLE_NAME);
|
|
|
|
await ORM.knex.raw(`CREATE TABLE ?? AS ??`, [
|
|
|
|
TMP_TABLE_NAME,
|
|
|
|
ORM.knex
|
|
|
|
.select(columnsToCopy)
|
|
|
|
.from(model.collectionName)
|
|
|
|
.whereRaw('?', 0),
|
|
|
|
]);
|
2021-03-02 16:42:17 +01:00
|
|
|
};
|
2021-02-25 17:40:14 +01:00
|
|
|
|
2021-03-02 16:42:17 +01:00
|
|
|
const deleteTmpTable = ({ ORM }) => ORM.knex.schema.dropTableIfExists(TMP_TABLE_NAME);
|
|
|
|
|
2021-03-18 18:27:15 +01:00
|
|
|
const getSortedLocales = async trx => {
|
2021-03-15 18:51:32 +01:00
|
|
|
let defaultLocale;
|
|
|
|
try {
|
2021-03-18 18:27:15 +01:00
|
|
|
const defaultLocaleRow = await trx('core_store')
|
2021-03-15 18:51:32 +01:00
|
|
|
.select('value')
|
|
|
|
.where({ key: 'plugin_i18n_default_locale' });
|
|
|
|
defaultLocale = defaultLocaleRow[0].value;
|
|
|
|
} catch (e) {
|
|
|
|
throw new Error("Could not migrate because the default locale doesn't exist");
|
|
|
|
}
|
|
|
|
|
|
|
|
let locales;
|
|
|
|
try {
|
2021-03-18 18:27:15 +01:00
|
|
|
locales = await trx(strapi.plugins.i18n.models.locale.collectionName).select('code');
|
2021-03-15 18:51:32 +01:00
|
|
|
} catch (e) {
|
|
|
|
throw new Error('Could not migrate because no locale exist');
|
|
|
|
}
|
|
|
|
|
|
|
|
locales.forEach(locale => (locale.isDefault = locale.code === defaultLocale));
|
|
|
|
return orderBy(['isDefault', 'code'], ['desc', 'asc'])(locales); // Put default locale first
|
|
|
|
};
|
|
|
|
|
2021-03-18 18:27:15 +01:00
|
|
|
const processEntriesWith = async (processFn, { trx, model, attributesToMigrate }) => {
|
|
|
|
const locales = await getSortedLocales(trx);
|
2021-03-11 10:41:35 +01:00
|
|
|
const localizationAssoc = model.associations.find(a => a.alias === 'localizations');
|
|
|
|
const localizationTableName = localizationAssoc.tableCollectionName;
|
|
|
|
|
|
|
|
const locsAttr = model.attributes.localizations;
|
|
|
|
const foreignKey = `${singular(model.collectionName)}_${model.primaryKey}`;
|
|
|
|
const relatedKey = `${locsAttr.attribute}_${locsAttr.column}`;
|
2021-03-18 18:27:15 +01:00
|
|
|
const processedLocaleCodes = [];
|
|
|
|
for (const locale of locales) {
|
|
|
|
let offset = 0;
|
|
|
|
// eslint-disable-next-line no-constant-condition
|
|
|
|
while (true) {
|
|
|
|
let batch = await trx
|
|
|
|
.select([
|
|
|
|
...attributesToMigrate.map(attr => `model.${attr}`),
|
|
|
|
'model.id as __strapi_rootId',
|
|
|
|
'relModel.id as id',
|
|
|
|
'relModel.locale',
|
|
|
|
])
|
|
|
|
.join(
|
|
|
|
`${localizationTableName} as loc`,
|
|
|
|
`model.${model.primaryKey}`,
|
|
|
|
'=',
|
|
|
|
`loc.${foreignKey}`
|
|
|
|
)
|
|
|
|
.join(
|
|
|
|
`${model.collectionName} as relModel`,
|
|
|
|
`loc.${relatedKey}`,
|
|
|
|
'=',
|
|
|
|
`relModel.${model.primaryKey}`
|
|
|
|
)
|
|
|
|
.from(
|
|
|
|
trx
|
|
|
|
.select('*')
|
|
|
|
.from(`${model.collectionName} as subModel`)
|
|
|
|
.orderBy(`subModel.${model.primaryKey}`)
|
|
|
|
.where(`subModel.locale`, locale.code)
|
|
|
|
.offset(offset)
|
|
|
|
.limit(BATCH_SIZE)
|
|
|
|
.as('model')
|
|
|
|
);
|
|
|
|
let entries = batch.reduce((entries, entry) => {
|
|
|
|
if (has(entry.__strapi_rootId, entries)) {
|
|
|
|
entries[entry.__strapi_rootId].localizations.push(pick(['id', 'locale'], entry));
|
2021-03-02 16:42:17 +01:00
|
|
|
} else {
|
2021-03-18 18:27:15 +01:00
|
|
|
entries[entry.__strapi_rootId] = omit(['id', 'locale', '__strapi_rootId'], entry);
|
|
|
|
entries[entry.__strapi_rootId].localizations = [pick(['id', 'locale'], entry)];
|
2021-03-02 16:42:17 +01:00
|
|
|
}
|
2021-02-25 17:40:14 +01:00
|
|
|
|
2021-03-18 18:27:15 +01:00
|
|
|
return entries;
|
|
|
|
}, {});
|
|
|
|
entries = Object.values(entries);
|
2021-02-25 17:40:14 +01:00
|
|
|
|
2021-03-18 18:27:15 +01:00
|
|
|
offset += BATCH_SIZE;
|
2021-02-25 17:40:14 +01:00
|
|
|
|
2021-03-18 18:27:15 +01:00
|
|
|
const entriesToProcess = entries.filter(shouldBeProcessed(processedLocaleCodes));
|
|
|
|
const updatesInfo = getUpdatesInfo({ entriesToProcess, attributesToMigrate });
|
2021-02-25 17:40:14 +01:00
|
|
|
|
2021-03-18 18:27:15 +01:00
|
|
|
await processFn(updatesInfo, trx, model);
|
|
|
|
|
|
|
|
if (entries.length < BATCH_SIZE) {
|
|
|
|
break;
|
|
|
|
}
|
2021-03-02 16:42:17 +01:00
|
|
|
}
|
2021-03-18 18:27:15 +01:00
|
|
|
processedLocaleCodes.push(locale.code);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const migrateForBookshelf = async ({ ORM, model, attributesToMigrate }) => {
|
|
|
|
if (['pg', 'mysql'].includes(model.client)) {
|
2021-03-19 10:21:56 +01:00
|
|
|
// create table outside of the transaction because mysql doesn't accept the creation inside
|
2021-03-18 18:27:15 +01:00
|
|
|
await createTmpTable({ ORM, attributesToMigrate, model });
|
|
|
|
await ORM.knex.transaction(async trx => {
|
|
|
|
await processEntriesWith(batchInsertInTmpTable, { ORM, trx, model, attributesToMigrate });
|
|
|
|
await updateFromTmpTable({ model, trx, attributesToMigrate });
|
|
|
|
});
|
|
|
|
await deleteTmpTable({ ORM });
|
|
|
|
} else {
|
|
|
|
await ORM.knex.transaction(async trx => {
|
|
|
|
await processEntriesWith(batchUpdate, { ORM, trx, model, attributesToMigrate });
|
|
|
|
});
|
2021-02-25 17:40:14 +01:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-03-02 17:09:35 +01:00
|
|
|
module.exports = migrateForBookshelf;
|