mirror of
https://github.com/strapi/strapi.git
synced 2025-09-12 18:19:47 +00:00
Merge pull request #9497 from strapi/i18n/disable-i18n-field
I18n/disable i18n field
This commit is contained in:
commit
c062e224be
@ -4,7 +4,11 @@ const _ = require('lodash');
|
|||||||
const { singular } = require('pluralize');
|
const { singular } = require('pluralize');
|
||||||
const { contentTypes: contentTypesUtils } = require('strapi-utils');
|
const { contentTypes: contentTypesUtils } = require('strapi-utils');
|
||||||
|
|
||||||
const { storeDefinition, getColumnsWhereDefinitionChanged } = require('./utils/store-definition');
|
const {
|
||||||
|
getDefinitionFromStore,
|
||||||
|
storeDefinition,
|
||||||
|
getColumnsWhereDefinitionChanged,
|
||||||
|
} = require('./utils/store-definition');
|
||||||
const { getManyRelations } = require('./utils/associations');
|
const { getManyRelations } = require('./utils/associations');
|
||||||
|
|
||||||
const migrateSchemas = async ({ ORM, loadedModel, definition, connection, model }, context) => {
|
const migrateSchemas = async ({ ORM, loadedModel, definition, connection, model }, context) => {
|
||||||
@ -94,7 +98,7 @@ const migrateSchemas = async ({ ORM, loadedModel, definition, connection, model
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove from attributes (auto handled by bookshlef and not displayed on ctb)
|
// Remove from attributes (auto handled by bookshelf and not displayed on ctb)
|
||||||
if (loadedModel.hasTimestamps) {
|
if (loadedModel.hasTimestamps) {
|
||||||
delete definition.attributes[loadedModel.hasTimestamps[0]];
|
delete definition.attributes[loadedModel.hasTimestamps[0]];
|
||||||
delete definition.attributes[loadedModel.hasTimestamps[1]];
|
delete definition.attributes[loadedModel.hasTimestamps[1]];
|
||||||
@ -397,10 +401,14 @@ const createOrUpdateTable = async ({ table, attributes, definition, ORM, model }
|
|||||||
};
|
};
|
||||||
|
|
||||||
module.exports = async ({ ORM, loadedModel, definition, connection, model }) => {
|
module.exports = async ({ ORM, loadedModel, definition, connection, model }) => {
|
||||||
|
const previousDefinitionRow = await getDefinitionFromStore(definition, ORM);
|
||||||
|
const previousDefinition = JSON.parse(_.get(previousDefinitionRow, 'value', null));
|
||||||
|
|
||||||
// run migrations
|
// run migrations
|
||||||
await strapi.db.migrations.run(migrateSchemas, {
|
await strapi.db.migrations.run(migrateSchemas, {
|
||||||
ORM,
|
ORM,
|
||||||
loadedModel,
|
loadedModel,
|
||||||
|
previousDefinition,
|
||||||
definition,
|
definition,
|
||||||
connection,
|
connection,
|
||||||
model,
|
model,
|
||||||
|
@ -4,12 +4,9 @@ const _ = require('lodash');
|
|||||||
const { contentTypes: contentTypesUtils } = require('strapi-utils');
|
const { contentTypes: contentTypesUtils } = require('strapi-utils');
|
||||||
|
|
||||||
const { PUBLISHED_AT_ATTRIBUTE } = contentTypesUtils.constants;
|
const { PUBLISHED_AT_ATTRIBUTE } = contentTypesUtils.constants;
|
||||||
const { getDefinitionFromStore } = require('../utils/store-definition');
|
|
||||||
|
|
||||||
const getDraftAndPublishMigrationWay = async ({ definition, ORM }) => {
|
const getDraftAndPublishMigrationWay = async ({ definition, previousDefinition }) => {
|
||||||
const previousDefRow = await getDefinitionFromStore(definition, ORM);
|
const previousDraftAndPublish = contentTypesUtils.hasDraftAndPublish(previousDefinition);
|
||||||
const previousDef = JSON.parse(_.get(previousDefRow, 'value', null));
|
|
||||||
const previousDraftAndPublish = contentTypesUtils.hasDraftAndPublish(previousDef);
|
|
||||||
const actualDraftAndPublish = contentTypesUtils.hasDraftAndPublish(definition);
|
const actualDraftAndPublish = contentTypesUtils.hasDraftAndPublish(definition);
|
||||||
|
|
||||||
if (previousDraftAndPublish === actualDraftAndPublish) {
|
if (previousDraftAndPublish === actualDraftAndPublish) {
|
||||||
@ -23,8 +20,8 @@ const getDraftAndPublishMigrationWay = async ({ definition, ORM }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const before = async ({ definition, ORM }, context) => {
|
const before = async ({ definition, previousDefinition, ORM }, context) => {
|
||||||
const way = await getDraftAndPublishMigrationWay({ definition, ORM });
|
const way = await getDraftAndPublishMigrationWay({ definition, previousDefinition });
|
||||||
|
|
||||||
if (way === 'disable') {
|
if (way === 'disable') {
|
||||||
const publishedAtColumnExists = await ORM.knex.schema.hasColumn(
|
const publishedAtColumnExists = await ORM.knex.schema.hasColumn(
|
||||||
@ -50,8 +47,8 @@ const before = async ({ definition, ORM }, context) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const after = async ({ definition, ORM }) => {
|
const after = async ({ definition, previousDefinition, ORM }) => {
|
||||||
const way = await getDraftAndPublishMigrationWay({ definition, ORM });
|
const way = await getDraftAndPublishMigrationWay({ definition, previousDefinition });
|
||||||
|
|
||||||
if (way === 'enable') {
|
if (way === 'enable') {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
@ -4,15 +4,12 @@ const _ = require('lodash');
|
|||||||
const { contentTypes: contentTypesUtils } = require('strapi-utils');
|
const { contentTypes: contentTypesUtils } = require('strapi-utils');
|
||||||
|
|
||||||
const { PUBLISHED_AT_ATTRIBUTE } = contentTypesUtils.constants;
|
const { PUBLISHED_AT_ATTRIBUTE } = contentTypesUtils.constants;
|
||||||
const { getDefinitionFromStore } = require('../utils/store-definition');
|
|
||||||
|
|
||||||
const getDraftAndPublishMigrationWay = async (definition, ORM) => {
|
const getDraftAndPublishMigrationWay = async ({ definition, previousDefinition }) => {
|
||||||
const previousDefRow = await getDefinitionFromStore(definition, ORM);
|
const previousDraftAndPublish = contentTypesUtils.hasDraftAndPublish(previousDefinition);
|
||||||
const previousDef = JSON.parse(_.get(previousDefRow, 'value', null));
|
|
||||||
const previousDraftAndPublish = contentTypesUtils.hasDraftAndPublish(previousDef);
|
|
||||||
const actualDraftAndPublish = contentTypesUtils.hasDraftAndPublish(definition);
|
const actualDraftAndPublish = contentTypesUtils.hasDraftAndPublish(definition);
|
||||||
|
|
||||||
if (!previousDefRow || previousDraftAndPublish === actualDraftAndPublish) {
|
if (!previousDefinition || previousDraftAndPublish === actualDraftAndPublish) {
|
||||||
return 'none';
|
return 'none';
|
||||||
}
|
}
|
||||||
if (!previousDraftAndPublish && actualDraftAndPublish) {
|
if (!previousDraftAndPublish && actualDraftAndPublish) {
|
||||||
@ -23,8 +20,8 @@ const getDraftAndPublishMigrationWay = async (definition, ORM) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const migrateDraftAndPublish = async ({ definition, model, ORM }) => {
|
const migrateDraftAndPublish = async ({ definition, previousDefinition, model }) => {
|
||||||
let way = await getDraftAndPublishMigrationWay(definition, ORM);
|
let way = await getDraftAndPublishMigrationWay({ definition, previousDefinition });
|
||||||
|
|
||||||
if (way === 'enable') {
|
if (way === 'enable') {
|
||||||
const createdAtCol = _.get(definition, 'timestamps.createdAt', 'createdAt');
|
const createdAtCol = _.get(definition, 'timestamps.createdAt', 'createdAt');
|
||||||
|
@ -8,7 +8,11 @@ const utils = require('./utils');
|
|||||||
const populateQueries = require('./utils/populate-queries');
|
const populateQueries = require('./utils/populate-queries');
|
||||||
const relations = require('./relations');
|
const relations = require('./relations');
|
||||||
const { findComponentByGlobalId } = require('./utils/helpers');
|
const { findComponentByGlobalId } = require('./utils/helpers');
|
||||||
const { didDefinitionChange, storeDefinition } = require('./utils/store-definition');
|
const {
|
||||||
|
didDefinitionChange,
|
||||||
|
storeDefinition,
|
||||||
|
getDefinitionFromStore,
|
||||||
|
} = require('./utils/store-definition');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
PUBLISHED_AT_ATTRIBUTE,
|
PUBLISHED_AT_ATTRIBUTE,
|
||||||
@ -342,9 +346,13 @@ module.exports = async ({ models, target }, ctx) => {
|
|||||||
const modelInstance = target[model];
|
const modelInstance = target[model];
|
||||||
const definitionDidChange = await didDefinitionChange(definition, instance);
|
const definitionDidChange = await didDefinitionChange(definition, instance);
|
||||||
|
|
||||||
|
const previousDefinitionRow = await getDefinitionFromStore(definition, instance);
|
||||||
|
const previousDefinition = JSON.parse(_.get(previousDefinitionRow, 'value', null));
|
||||||
|
|
||||||
// run migrations
|
// run migrations
|
||||||
await strapi.db.migrations.run(migrateSchema, {
|
await strapi.db.migrations.run(migrateSchema, {
|
||||||
definition,
|
definition,
|
||||||
|
previousDefinition,
|
||||||
model: modelInstance,
|
model: modelInstance,
|
||||||
ORM: instance,
|
ORM: instance,
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,150 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { after } = require('../field');
|
||||||
|
|
||||||
|
describe('i18n - Migration - disable localization on a field', () => {
|
||||||
|
describe('after', () => {
|
||||||
|
describe('Should not migrate', () => {
|
||||||
|
test("Doesn't migrate if model isn't localized", async () => {
|
||||||
|
const find = jest.fn();
|
||||||
|
global.strapi = {
|
||||||
|
query: () => {
|
||||||
|
find;
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
i18n: {
|
||||||
|
services: {
|
||||||
|
'content-types': {
|
||||||
|
isLocalized: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const model = {};
|
||||||
|
const previousDefinition = {};
|
||||||
|
|
||||||
|
await after({ model, definition: model, previousDefinition });
|
||||||
|
expect(find).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Doesn't migrate if no attribute changed (without i18n)", async () => {
|
||||||
|
const find = jest.fn();
|
||||||
|
const getLocalizedAttributes = jest.fn(() => []);
|
||||||
|
|
||||||
|
global.strapi = {
|
||||||
|
query: () => {
|
||||||
|
find;
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
i18n: {
|
||||||
|
services: {
|
||||||
|
'content-types': {
|
||||||
|
isLocalized: () => true,
|
||||||
|
getLocalizedAttributes,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const model = { attributes: { name: {} } };
|
||||||
|
const previousDefinition = { attributes: { name: {} } };
|
||||||
|
|
||||||
|
await after({ model, definition: model, previousDefinition });
|
||||||
|
expect(getLocalizedAttributes).toHaveBeenCalledTimes(2);
|
||||||
|
expect(find).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Doesn't migrate if no attribute changed (with i18n)", async () => {
|
||||||
|
const find = jest.fn();
|
||||||
|
const getLocalizedAttributes = jest.fn(() => ['name']);
|
||||||
|
global.strapi = {
|
||||||
|
query: () => {
|
||||||
|
find;
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
i18n: {
|
||||||
|
services: {
|
||||||
|
'content-types': {
|
||||||
|
isLocalized: () => true,
|
||||||
|
getLocalizedAttributes,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const model = { attributes: { name: {} } };
|
||||||
|
const previousDefinition = { attributes: { name: {} } };
|
||||||
|
|
||||||
|
await after({ model, definition: model, previousDefinition });
|
||||||
|
expect(getLocalizedAttributes).toHaveBeenCalledTimes(2);
|
||||||
|
expect(find).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Doesn't migrate if field become localized", async () => {
|
||||||
|
const find = jest.fn();
|
||||||
|
const getLocalizedAttributes = jest
|
||||||
|
.fn()
|
||||||
|
.mockReturnValueOnce(['name'])
|
||||||
|
.mockReturnValueOnce([]);
|
||||||
|
|
||||||
|
global.strapi = {
|
||||||
|
query: () => {
|
||||||
|
find;
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
i18n: {
|
||||||
|
services: {
|
||||||
|
'content-types': {
|
||||||
|
isLocalized: () => true,
|
||||||
|
getLocalizedAttributes,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const model = { attributes: { name: {} } };
|
||||||
|
const previousDefinition = { attributes: { name: {} } };
|
||||||
|
|
||||||
|
await after({ model, definition: model, previousDefinition });
|
||||||
|
expect(getLocalizedAttributes).toHaveBeenCalledTimes(2);
|
||||||
|
expect(find).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Doesn't migrate if field is deleted", async () => {
|
||||||
|
const find = jest.fn();
|
||||||
|
const getLocalizedAttributes = jest
|
||||||
|
.fn()
|
||||||
|
.mockReturnValueOnce([])
|
||||||
|
.mockReturnValueOnce(['name']);
|
||||||
|
|
||||||
|
global.strapi = {
|
||||||
|
query: () => {
|
||||||
|
find;
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
i18n: {
|
||||||
|
services: {
|
||||||
|
'content-types': {
|
||||||
|
isLocalized: () => true,
|
||||||
|
getLocalizedAttributes,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const model = { attributes: {} };
|
||||||
|
const previousDefinition = { attributes: { name: {} } };
|
||||||
|
|
||||||
|
await after({ model, definition: model, previousDefinition });
|
||||||
|
expect(getLocalizedAttributes).toHaveBeenCalledTimes(2);
|
||||||
|
expect(find).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,53 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { shouldBeProcessed, getUpdatesInfo } = require('../utils');
|
||||||
|
|
||||||
|
describe('i18n - migration utils', () => {
|
||||||
|
describe('shouldBeProcessed', () => {
|
||||||
|
const testData = [
|
||||||
|
[[], [], false],
|
||||||
|
[['en'], [], false],
|
||||||
|
[['en', 'fr'], [], false],
|
||||||
|
[['en', 'fr'], [{ locale: 'en' }], false],
|
||||||
|
[['en', 'fr'], [{ locale: 'fr' }], false],
|
||||||
|
[['en'], [{ locale: 'fr' }, { locale: 'en' }], false],
|
||||||
|
[['en', 'fr'], [{ locale: 'fr' }, { locale: 'en' }], false],
|
||||||
|
[[], [{ locale: 'en' }], true],
|
||||||
|
[['en'], [{ locale: 'fr' }], true],
|
||||||
|
[['en', 'fr'], [{ locale: 'it' }], true],
|
||||||
|
];
|
||||||
|
|
||||||
|
test.each(testData)('%p %j : %p', (processedLocaleCodes, localizations, expectedResult) => {
|
||||||
|
const result = shouldBeProcessed(processedLocaleCodes)({ localizations });
|
||||||
|
|
||||||
|
expect(result).toBe(expectedResult);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getUpdatesInfo', () => {
|
||||||
|
const testData = [
|
||||||
|
[
|
||||||
|
[{ name: 'Name', nickname: 'Nickname', localizations: [{ id: 1 }, { id: 2 }] }],
|
||||||
|
['name'],
|
||||||
|
[{ entriesIdsToUpdate: [1, 2], attributesValues: { name: 'Name' } }],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
{ name: 'Name 1', nickname: 'Nickname 1', localizations: [{ id: 1 }, { id: 2 }] },
|
||||||
|
{ name: 'Name 2', nickname: 'Nickname 2', localizations: [{ id: 3 }, { id: 4 }] },
|
||||||
|
],
|
||||||
|
['name'],
|
||||||
|
[
|
||||||
|
{ entriesIdsToUpdate: [1, 2], attributesValues: { name: 'Name 1' } },
|
||||||
|
{ entriesIdsToUpdate: [3, 4], attributesValues: { name: 'Name 2' } },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
test.each(testData)('%j', (entriesToProcess, attributesToMigrate, expectedResult) => {
|
||||||
|
const result = getUpdatesInfo({ entriesToProcess, attributesToMigrate });
|
||||||
|
|
||||||
|
expect(result).toEqual(expectedResult);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,37 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { difference, intersection } = require('lodash/fp');
|
||||||
|
const { getService } = require('../../../../utils');
|
||||||
|
const migrateForMongoose = require('./migrate-for-mongoose');
|
||||||
|
const migrateForBookshelf = require('./migrate-for-bookshelf');
|
||||||
|
|
||||||
|
// Migration when i18n is disabled on a field of a content-type that have i18n enabled
|
||||||
|
const after = async ({ model, definition, previousDefinition, ORM }) => {
|
||||||
|
const ctService = getService('content-types');
|
||||||
|
|
||||||
|
if (!ctService.isLocalized(model) || !previousDefinition) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const localizedAttributes = ctService.getLocalizedAttributes(definition);
|
||||||
|
const prevLocalizedAttributes = ctService.getLocalizedAttributes(previousDefinition);
|
||||||
|
const attributesDisabled = difference(prevLocalizedAttributes, localizedAttributes);
|
||||||
|
const attributesToMigrate = intersection(Object.keys(definition.attributes), attributesDisabled);
|
||||||
|
|
||||||
|
if (attributesToMigrate.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.orm === 'bookshelf') {
|
||||||
|
await migrateForBookshelf({ ORM, model, attributesToMigrate });
|
||||||
|
} else if (model.orm === 'mongoose') {
|
||||||
|
await migrateForMongoose({ model, attributesToMigrate });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const before = () => {};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
before,
|
||||||
|
after,
|
||||||
|
};
|
@ -0,0 +1,171 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { singular } = require('pluralize');
|
||||||
|
const { has, omit, pick, orderBy } = require('lodash/fp');
|
||||||
|
const { shouldBeProcessed, getUpdatesInfo } = require('./utils');
|
||||||
|
|
||||||
|
const BATCH_SIZE = 1000;
|
||||||
|
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateFromTmpTable = async ({ model, trx, attributesToMigrate }) => {
|
||||||
|
const collectionName = model.collectionName;
|
||||||
|
if (model.client === 'pg') {
|
||||||
|
const substitutes = attributesToMigrate.map(() => '?? = ??.??').join(',');
|
||||||
|
const bindings = [collectionName];
|
||||||
|
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(',');
|
||||||
|
const bindings = [collectionName, TMP_TABLE_NAME, collectionName, TMP_TABLE_NAME];
|
||||||
|
attributesToMigrate.forEach(attr => bindings.push(collectionName, attr, TMP_TABLE_NAME, attr));
|
||||||
|
|
||||||
|
await trx.raw(`UPDATE ?? JOIN ?? ON ??.id = ??.id SET ${substitutes};`, bindings);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createTmpTable = async ({ ORM, attributesToMigrate, model }) => {
|
||||||
|
const columnsToCopy = ['id', ...attributesToMigrate];
|
||||||
|
await deleteTmpTable({ ORM });
|
||||||
|
await ORM.knex.raw(`CREATE TABLE ?? AS ??`, [
|
||||||
|
TMP_TABLE_NAME,
|
||||||
|
ORM.knex
|
||||||
|
.select(columnsToCopy)
|
||||||
|
.from(model.collectionName)
|
||||||
|
.whereRaw('?', 0),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteTmpTable = ({ ORM }) => ORM.knex.schema.dropTableIfExists(TMP_TABLE_NAME);
|
||||||
|
|
||||||
|
const getSortedLocales = async trx => {
|
||||||
|
let defaultLocale;
|
||||||
|
try {
|
||||||
|
const defaultLocaleRow = await trx('core_store')
|
||||||
|
.select('value')
|
||||||
|
.where({ key: 'plugin_i18n_default_locale' })
|
||||||
|
.first();
|
||||||
|
defaultLocale = JSON.parse(defaultLocaleRow.value);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error("Could not migrate because the default locale doesn't exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
let locales;
|
||||||
|
try {
|
||||||
|
locales = await trx(strapi.plugins.i18n.models.locale.collectionName).select('code');
|
||||||
|
} 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
|
||||||
|
};
|
||||||
|
|
||||||
|
const processEntriesWith = async (processFn, { trx, model, attributesToMigrate }) => {
|
||||||
|
const locales = await getSortedLocales(trx);
|
||||||
|
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}`;
|
||||||
|
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));
|
||||||
|
} else {
|
||||||
|
entries[entry.__strapi_rootId] = omit(['id', 'locale', '__strapi_rootId'], entry);
|
||||||
|
entries[entry.__strapi_rootId].localizations = [pick(['id', 'locale'], entry)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}, {});
|
||||||
|
entries = Object.values(entries);
|
||||||
|
|
||||||
|
offset += BATCH_SIZE;
|
||||||
|
|
||||||
|
const entriesToProcess = entries.filter(shouldBeProcessed(processedLocaleCodes));
|
||||||
|
const updatesInfo = getUpdatesInfo({ entriesToProcess, attributesToMigrate });
|
||||||
|
|
||||||
|
await processFn(updatesInfo, trx, model);
|
||||||
|
|
||||||
|
if (entries.length < BATCH_SIZE) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
processedLocaleCodes.push(locale.code);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const migrateForBookshelf = async ({ ORM, model, attributesToMigrate }) => {
|
||||||
|
if (['pg', 'mysql'].includes(model.client)) {
|
||||||
|
// create table outside of the transaction because mysql doesn't accept the creation inside
|
||||||
|
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 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = migrateForBookshelf;
|
@ -0,0 +1,69 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { orderBy } = require('lodash/fp');
|
||||||
|
const { shouldBeProcessed, getUpdatesInfo } = require('./utils');
|
||||||
|
|
||||||
|
const BATCH_SIZE = 1000;
|
||||||
|
|
||||||
|
const getSortedLocales = async () => {
|
||||||
|
let defaultLocale;
|
||||||
|
try {
|
||||||
|
const defaultLocaleRow = await strapi.models['core_store'].findOne({
|
||||||
|
key: 'plugin_i18n_default_locale',
|
||||||
|
});
|
||||||
|
defaultLocale = JSON.parse(defaultLocaleRow.value);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error("Could not migrate because the default locale doesn't exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
let locales;
|
||||||
|
try {
|
||||||
|
strapi.models;
|
||||||
|
locales = await strapi.plugins.i18n.models.locale.find();
|
||||||
|
} 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
|
||||||
|
};
|
||||||
|
|
||||||
|
const migrateForMongoose = async ({ model, attributesToMigrate }) => {
|
||||||
|
const locales = await getSortedLocales();
|
||||||
|
const processedLocaleCodes = [];
|
||||||
|
for (const locale of locales) {
|
||||||
|
let lastId;
|
||||||
|
// eslint-disable-next-line no-constant-condition
|
||||||
|
while (true) {
|
||||||
|
const findParams = { locale: locale.code };
|
||||||
|
if (lastId) {
|
||||||
|
findParams._id = { $gt: lastId };
|
||||||
|
}
|
||||||
|
|
||||||
|
const batch = await model
|
||||||
|
.find(findParams, [...attributesToMigrate, 'locale', 'localizations'])
|
||||||
|
.populate('localizations', 'locale id')
|
||||||
|
.sort({ _id: 1 })
|
||||||
|
.limit(BATCH_SIZE);
|
||||||
|
|
||||||
|
if (batch.length > 0) {
|
||||||
|
lastId = batch[batch.length - 1]._id;
|
||||||
|
}
|
||||||
|
const entriesToProcess = batch.filter(shouldBeProcessed(processedLocaleCodes));
|
||||||
|
|
||||||
|
const updatesInfo = getUpdatesInfo({ entriesToProcess, attributesToMigrate });
|
||||||
|
const updates = updatesInfo.map(({ entriesIdsToUpdate, attributesValues }) => ({
|
||||||
|
updateMany: { filter: { _id: { $in: entriesIdsToUpdate } }, update: attributesValues },
|
||||||
|
}));
|
||||||
|
|
||||||
|
await model.bulkWrite(updates);
|
||||||
|
|
||||||
|
if (batch.length < BATCH_SIZE) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
processedLocaleCodes.push(locale.code);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = migrateForMongoose;
|
@ -0,0 +1,25 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { pick, prop, intersection } = require('lodash/fp');
|
||||||
|
|
||||||
|
const shouldBeProcessed = processedLocaleCodes => entry => {
|
||||||
|
return (
|
||||||
|
entry.localizations.length > 0 &&
|
||||||
|
intersection(entry.localizations.map(prop('locale')), processedLocaleCodes).length === 0
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUpdatesInfo = ({ entriesToProcess, attributesToMigrate }) => {
|
||||||
|
const updates = [];
|
||||||
|
for (const entry of entriesToProcess) {
|
||||||
|
const attributesValues = pick(attributesToMigrate, entry);
|
||||||
|
const entriesIdsToUpdate = entry.localizations.map(prop('id'));
|
||||||
|
updates.push({ entriesIdsToUpdate, attributesValues });
|
||||||
|
}
|
||||||
|
return updates;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
shouldBeProcessed,
|
||||||
|
getUpdatesInfo,
|
||||||
|
};
|
@ -4,6 +4,7 @@ const _ = require('lodash');
|
|||||||
const { PUBLISHED_AT_ATTRIBUTE } = require('strapi-utils').contentTypes.constants;
|
const { PUBLISHED_AT_ATTRIBUTE } = require('strapi-utils').contentTypes.constants;
|
||||||
|
|
||||||
const { getService } = require('../../utils');
|
const { getService } = require('../../utils');
|
||||||
|
const fieldMigration = require('./migrations/field');
|
||||||
|
|
||||||
module.exports = () => {
|
module.exports = () => {
|
||||||
const contentTypeService = getService('content-types');
|
const contentTypeService = getService('content-types');
|
||||||
@ -34,8 +35,5 @@ module.exports = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
strapi.db.migrations.register({
|
strapi.db.migrations.register(fieldMigration);
|
||||||
before() {},
|
|
||||||
after() {},
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lodash": "4.17.20",
|
"lodash": "4.17.20",
|
||||||
|
"pluralize": "8.0.0",
|
||||||
"strapi-utils": "3.5.3"
|
"strapi-utils": "3.5.3"
|
||||||
},
|
},
|
||||||
"author": {
|
"author": {
|
||||||
|
@ -95,10 +95,22 @@ const copyNonLocalizedAttributes = (model, entry) => {
|
|||||||
return pipe(pick(nonLocalizedAttributes), removeIds)(entry);
|
return pipe(pick(nonLocalizedAttributes), removeIds)(entry);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of attribute names that are localized
|
||||||
|
* @param {object} model
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
const getLocalizedAttributes = model => {
|
||||||
|
return getVisibleAttributes(model).filter(
|
||||||
|
attributeName => isLocalizedAttribute(model, attributeName)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
isLocalized,
|
isLocalized,
|
||||||
getValidLocale,
|
getValidLocale,
|
||||||
getNewLocalizationsFor,
|
getNewLocalizationsFor,
|
||||||
|
getLocalizedAttributes,
|
||||||
getNonLocalizedAttributes,
|
getNonLocalizedAttributes,
|
||||||
copyNonLocalizedAttributes,
|
copyNonLocalizedAttributes,
|
||||||
};
|
};
|
||||||
|
@ -15164,7 +15164,7 @@ please-upgrade-node@^3.2.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
semver-compare "^1.0.0"
|
semver-compare "^1.0.0"
|
||||||
|
|
||||||
pluralize@^8.0.0:
|
pluralize@8.0.0, pluralize@^8.0.0:
|
||||||
version "8.0.0"
|
version "8.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1"
|
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1"
|
||||||
integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==
|
integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==
|
||||||
|
Loading…
x
Reference in New Issue
Block a user