diff --git a/packages/strapi-connector-bookshelf/lib/build-database-schema.js b/packages/strapi-connector-bookshelf/lib/build-database-schema.js index 9f727713ce..3caf0ddcbb 100644 --- a/packages/strapi-connector-bookshelf/lib/build-database-schema.js +++ b/packages/strapi-connector-bookshelf/lib/build-database-schema.js @@ -4,7 +4,11 @@ const _ = require('lodash'); const { singular } = require('pluralize'); 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 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) { delete definition.attributes[loadedModel.hasTimestamps[0]]; 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 }) => { + const previousDefinitionRow = await getDefinitionFromStore(definition, ORM); + const previousDefinition = JSON.parse(_.get(previousDefinitionRow, 'value', null)); + // run migrations await strapi.db.migrations.run(migrateSchemas, { ORM, loadedModel, + previousDefinition, definition, connection, model, diff --git a/packages/strapi-connector-bookshelf/lib/migrations/draft-publish.js b/packages/strapi-connector-bookshelf/lib/migrations/draft-publish.js index 39e52449ad..6781a0aeea 100644 --- a/packages/strapi-connector-bookshelf/lib/migrations/draft-publish.js +++ b/packages/strapi-connector-bookshelf/lib/migrations/draft-publish.js @@ -4,12 +4,9 @@ const _ = require('lodash'); const { contentTypes: contentTypesUtils } = require('strapi-utils'); const { PUBLISHED_AT_ATTRIBUTE } = contentTypesUtils.constants; -const { getDefinitionFromStore } = require('../utils/store-definition'); -const getDraftAndPublishMigrationWay = async ({ definition, ORM }) => { - const previousDefRow = await getDefinitionFromStore(definition, ORM); - const previousDef = JSON.parse(_.get(previousDefRow, 'value', null)); - const previousDraftAndPublish = contentTypesUtils.hasDraftAndPublish(previousDef); +const getDraftAndPublishMigrationWay = async ({ definition, previousDefinition }) => { + const previousDraftAndPublish = contentTypesUtils.hasDraftAndPublish(previousDefinition); const actualDraftAndPublish = contentTypesUtils.hasDraftAndPublish(definition); if (previousDraftAndPublish === actualDraftAndPublish) { @@ -23,8 +20,8 @@ const getDraftAndPublishMigrationWay = async ({ definition, ORM }) => { } }; -const before = async ({ definition, ORM }, context) => { - const way = await getDraftAndPublishMigrationWay({ definition, ORM }); +const before = async ({ definition, previousDefinition, ORM }, context) => { + const way = await getDraftAndPublishMigrationWay({ definition, previousDefinition }); if (way === 'disable') { const publishedAtColumnExists = await ORM.knex.schema.hasColumn( @@ -50,8 +47,8 @@ const before = async ({ definition, ORM }, context) => { } }; -const after = async ({ definition, ORM }) => { - const way = await getDraftAndPublishMigrationWay({ definition, ORM }); +const after = async ({ definition, previousDefinition, ORM }) => { + const way = await getDraftAndPublishMigrationWay({ definition, previousDefinition }); if (way === 'enable') { const now = new Date(); diff --git a/packages/strapi-connector-mongoose/lib/migrations/draft-publish.js b/packages/strapi-connector-mongoose/lib/migrations/draft-publish.js index 139823c76c..2b3ea17bcb 100644 --- a/packages/strapi-connector-mongoose/lib/migrations/draft-publish.js +++ b/packages/strapi-connector-mongoose/lib/migrations/draft-publish.js @@ -4,15 +4,12 @@ const _ = require('lodash'); const { contentTypes: contentTypesUtils } = require('strapi-utils'); const { PUBLISHED_AT_ATTRIBUTE } = contentTypesUtils.constants; -const { getDefinitionFromStore } = require('../utils/store-definition'); -const getDraftAndPublishMigrationWay = async (definition, ORM) => { - const previousDefRow = await getDefinitionFromStore(definition, ORM); - const previousDef = JSON.parse(_.get(previousDefRow, 'value', null)); - const previousDraftAndPublish = contentTypesUtils.hasDraftAndPublish(previousDef); +const getDraftAndPublishMigrationWay = async ({ definition, previousDefinition }) => { + const previousDraftAndPublish = contentTypesUtils.hasDraftAndPublish(previousDefinition); const actualDraftAndPublish = contentTypesUtils.hasDraftAndPublish(definition); - if (!previousDefRow || previousDraftAndPublish === actualDraftAndPublish) { + if (!previousDefinition || previousDraftAndPublish === actualDraftAndPublish) { return 'none'; } if (!previousDraftAndPublish && actualDraftAndPublish) { @@ -23,8 +20,8 @@ const getDraftAndPublishMigrationWay = async (definition, ORM) => { } }; -const migrateDraftAndPublish = async ({ definition, model, ORM }) => { - let way = await getDraftAndPublishMigrationWay(definition, ORM); +const migrateDraftAndPublish = async ({ definition, previousDefinition, model }) => { + let way = await getDraftAndPublishMigrationWay({ definition, previousDefinition }); if (way === 'enable') { const createdAtCol = _.get(definition, 'timestamps.createdAt', 'createdAt'); diff --git a/packages/strapi-connector-mongoose/lib/mount-models.js b/packages/strapi-connector-mongoose/lib/mount-models.js index 9a9222f650..0b11dbc1a9 100644 --- a/packages/strapi-connector-mongoose/lib/mount-models.js +++ b/packages/strapi-connector-mongoose/lib/mount-models.js @@ -8,7 +8,11 @@ const utils = require('./utils'); const populateQueries = require('./utils/populate-queries'); const relations = require('./relations'); const { findComponentByGlobalId } = require('./utils/helpers'); -const { didDefinitionChange, storeDefinition } = require('./utils/store-definition'); +const { + didDefinitionChange, + storeDefinition, + getDefinitionFromStore, +} = require('./utils/store-definition'); const { PUBLISHED_AT_ATTRIBUTE, @@ -342,9 +346,13 @@ module.exports = async ({ models, target }, ctx) => { const modelInstance = target[model]; const definitionDidChange = await didDefinitionChange(definition, instance); + const previousDefinitionRow = await getDefinitionFromStore(definition, instance); + const previousDefinition = JSON.parse(_.get(previousDefinitionRow, 'value', null)); + // run migrations await strapi.db.migrations.run(migrateSchema, { definition, + previousDefinition, model: modelInstance, ORM: instance, }); diff --git a/packages/strapi-plugin-i18n/config/functions/migrations/__tests__/field.test.js b/packages/strapi-plugin-i18n/config/functions/migrations/__tests__/field.test.js new file mode 100644 index 0000000000..43c046d7cb --- /dev/null +++ b/packages/strapi-plugin-i18n/config/functions/migrations/__tests__/field.test.js @@ -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(); + }); + }); + }); +}); diff --git a/packages/strapi-plugin-i18n/config/functions/migrations/field/__tests__/utils.test.js b/packages/strapi-plugin-i18n/config/functions/migrations/field/__tests__/utils.test.js new file mode 100644 index 0000000000..5af244f3f8 --- /dev/null +++ b/packages/strapi-plugin-i18n/config/functions/migrations/field/__tests__/utils.test.js @@ -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); + }); + }); +}); diff --git a/packages/strapi-plugin-i18n/config/functions/migrations/field/index.js b/packages/strapi-plugin-i18n/config/functions/migrations/field/index.js new file mode 100644 index 0000000000..7f43dc1b43 --- /dev/null +++ b/packages/strapi-plugin-i18n/config/functions/migrations/field/index.js @@ -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, +}; diff --git a/packages/strapi-plugin-i18n/config/functions/migrations/field/migrate-for-bookshelf.js b/packages/strapi-plugin-i18n/config/functions/migrations/field/migrate-for-bookshelf.js new file mode 100644 index 0000000000..13f403c407 --- /dev/null +++ b/packages/strapi-plugin-i18n/config/functions/migrations/field/migrate-for-bookshelf.js @@ -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; diff --git a/packages/strapi-plugin-i18n/config/functions/migrations/field/migrate-for-mongoose.js b/packages/strapi-plugin-i18n/config/functions/migrations/field/migrate-for-mongoose.js new file mode 100644 index 0000000000..450bee2fe1 --- /dev/null +++ b/packages/strapi-plugin-i18n/config/functions/migrations/field/migrate-for-mongoose.js @@ -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; diff --git a/packages/strapi-plugin-i18n/config/functions/migrations/field/utils.js b/packages/strapi-plugin-i18n/config/functions/migrations/field/utils.js new file mode 100644 index 0000000000..39fb9baf41 --- /dev/null +++ b/packages/strapi-plugin-i18n/config/functions/migrations/field/utils.js @@ -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, +}; diff --git a/packages/strapi-plugin-i18n/config/functions/register.js b/packages/strapi-plugin-i18n/config/functions/register.js index 46216f6c32..6da08e999c 100644 --- a/packages/strapi-plugin-i18n/config/functions/register.js +++ b/packages/strapi-plugin-i18n/config/functions/register.js @@ -4,6 +4,7 @@ const _ = require('lodash'); const { PUBLISHED_AT_ATTRIBUTE } = require('strapi-utils').contentTypes.constants; const { getService } = require('../../utils'); +const fieldMigration = require('./migrations/field'); module.exports = () => { const contentTypeService = getService('content-types'); @@ -34,8 +35,5 @@ module.exports = () => { } }); - strapi.db.migrations.register({ - before() {}, - after() {}, - }); + strapi.db.migrations.register(fieldMigration); }; diff --git a/packages/strapi-plugin-i18n/package.json b/packages/strapi-plugin-i18n/package.json index efedb8a418..6a37cb9fc2 100644 --- a/packages/strapi-plugin-i18n/package.json +++ b/packages/strapi-plugin-i18n/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "lodash": "4.17.20", + "pluralize": "8.0.0", "strapi-utils": "3.5.3" }, "author": { diff --git a/packages/strapi-plugin-i18n/services/content-types.js b/packages/strapi-plugin-i18n/services/content-types.js index d174aef646..ff34b27df4 100644 --- a/packages/strapi-plugin-i18n/services/content-types.js +++ b/packages/strapi-plugin-i18n/services/content-types.js @@ -95,10 +95,22 @@ const copyNonLocalizedAttributes = (model, 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 = { isLocalized, getValidLocale, getNewLocalizationsFor, + getLocalizedAttributes, getNonLocalizedAttributes, copyNonLocalizedAttributes, }; diff --git a/yarn.lock b/yarn.lock index 2b859fa7ac..f7484b4b2a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15164,7 +15164,7 @@ please-upgrade-node@^3.2.0: dependencies: semver-compare "^1.0.0" -pluralize@^8.0.0: +pluralize@8.0.0, pluralize@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==