Merge pull request #9430 from strapi/i18n/lifecycles-rework

Add Lifecycles for i18n
This commit is contained in:
Alexandre BODIN 2021-02-18 09:23:07 +01:00 committed by GitHub
commit 9160c46a80
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 251 additions and 28 deletions

View File

@ -398,7 +398,7 @@ const createOrUpdateTable = async ({ table, attributes, definition, ORM, model }
module.exports = async ({ ORM, loadedModel, definition, connection, model }) => {
// run migrations
await strapi.db.migrations.runMigration(migrateSchemas, {
await strapi.db.migrations.run(migrateSchemas, {
ORM,
loadedModel,
definition,

View File

@ -301,7 +301,7 @@ module.exports = async ({ models, target }, ctx) => {
const definitionDidChange = await didDefinitionChange(definition, instance);
// run migrations
await strapi.db.migrations.runMigration(migrateSchema, {
await strapi.db.migrations.run(migrateSchema, {
definition,
model: modelInstance,
ORM: instance,

View File

@ -0,0 +1,54 @@
'use strict';
const createLifecycleManager = require('../lifecycle-manager');
describe('Lifecycle Manager', () => {
test('Allows registering lifecycles', () => {
const manager = createLifecycleManager();
const lifecycle = {};
manager.register(lifecycle);
expect(manager.lifecycles).toEqual([lifecycle]);
});
test('Will run all the lifecycles if no model specified', async () => {
const lifecycleA = {
find: jest.fn(),
};
const lifecycleB = {
find: jest.fn(),
};
const manager = createLifecycleManager();
manager.register(lifecycleA).register(lifecycleB);
await manager.run('find', { uid: 'test-uid' });
expect(lifecycleA.find).toHaveBeenCalled();
expect(lifecycleB.find).toHaveBeenCalled();
});
test('Will match on model if specified', async () => {
const lifecycleA = {
model: 'test-uid',
find: jest.fn(),
};
const lifecycleB = {
model: 'other-uid',
find: jest.fn(),
};
const manager = createLifecycleManager();
manager.register(lifecycleA).register(lifecycleB);
await manager.run('find', { uid: 'test-uid' });
expect(lifecycleA.find).toHaveBeenCalled();
expect(lifecycleB.find).not.toHaveBeenCalled();
});
});

View File

@ -7,6 +7,7 @@ const createConnectorRegistry = require('./connector-registry');
const constants = require('./constants');
const { validateModelSchemas } = require('./validation');
const createMigrationManager = require('./migration-manager');
const createLifecycleManager = require('./lifecycle-manager');
class DatabaseManager {
constructor(strapi) {
@ -23,6 +24,7 @@ class DatabaseManager {
this.models = new Map();
this.migrations = createMigrationManager(this);
this.lifecycles = createLifecycleManager();
}
async initialize() {

View File

@ -0,0 +1,33 @@
'use strict';
const debug = require('debug')('strapi-database:lifecycle');
const { isFunction, isNil } = require('lodash/fp');
class LifecycleManager {
constructor() {
debug('Initialize lifecycle manager');
this.lifecycles = [];
}
register(lifecycle) {
debug('Register lifecycle');
this.lifecycles.push(lifecycle);
return this;
}
async run(action, model, ...args) {
for (const lifecycle of this.lifecycles) {
if (!isNil(lifecycle.model) && lifecycle.model !== model.uid) {
continue;
}
if (isFunction(lifecycle[action])) {
debug(`Run lifecycle ${action} for model ${model.uid}`);
await lifecycle[action](...args);
}
}
}
}
module.exports = () => new LifecycleManager();

View File

@ -14,7 +14,7 @@ class MigrationManager {
this.migrations.push(migration);
}
async runMigration(fn, options, context = {}) {
async run(fn, options, context = {}) {
debug('Run migration');
await this.runBefore(options, context);
await fn(options, context);

View File

@ -4,6 +4,14 @@ const _ = require('lodash');
const createQuery = require('../create-query');
describe('Database queries', () => {
global.strapi = {
db: {
lifecycles: {
run() {},
},
},
};
describe('Substitute id with primaryKey in parameters', () => {
test.each(['create', 'update', 'delete', 'find', 'findOne', 'search', 'count', 'countSearch'])(
'Calling "%s" replaces id by the primaryKey in the params of the model before calling the underlying connector',

View File

@ -3,6 +3,10 @@
const _ = require('lodash');
const executeLifecycle = async (lifecycle, model, ...args) => {
// Run registered lifecycles
await strapi.db.lifecycles.run(lifecycle, model, ...args);
// Run user lifecycles
if (_.has(model, `lifecycles.${lifecycle}`)) {
await model.lifecycles[lifecycle](...args);
}

View File

@ -1,6 +1,5 @@
'use strict';
const _ = require('lodash');
const { capitalize } = require('lodash/fp');
const { getService } = require('../../utils');
@ -26,23 +25,23 @@ module.exports = async () => {
await getService('locales').setDefaultLocale(DEFAULT_LOCALE);
}
Object.values(strapi.models).forEach(model => {
if (getService('content-types').isLocalized(model)) {
// TODO: support adding lifecycles programmatically or connecting to a database event handler to avoid conflicts with existing lifecycles fonctions
_.set(model, 'lifecycles.beforeCreate', async data => {
if (!data.locale) {
data.locale = await getService('locales').getDefaultLocale();
}
Object.values(strapi.models)
.filter(model => getService('content-types').isLocalized(model))
.forEach(model => {
strapi.db.lifecycles.register({
model: model.uid,
async beforeCreate(data) {
await getService('localizations').assignDefaultLocale(data);
},
async afterCreate(entry) {
await getService('localizations').addLocalizations(entry, { model });
},
async afterUpdate(entry) {
await getService('localizations').updateNonLocalizedFields(entry, { model });
},
async afterDelete(entry) {
await getService('localizations').removeEntryFromRelatedLocalizations(entry, { model });
},
});
_.set(model, 'lifecycles.afterCreate', async entry => {
await getService('localizations').addLocalizations(entry, { model });
});
_.set(model, 'lifecycles.afterUpdate', async entry => {
await getService('localizations').updateNonLocalizedFields(entry, { model });
});
}
});
});
};

View File

@ -1,6 +1,11 @@
'use strict';
const { addLocalizations, updateNonLocalizedFields } = require('../localizations');
const {
assignDefaultLocale,
addLocalizations,
updateNonLocalizedFields,
removeEntryFromRelatedLocalizations,
} = require('../localizations');
const model = {
uid: 'test-model',
@ -20,12 +25,43 @@ const model = {
};
describe('localizations service', () => {
describe('assignDefaultLocale', () => {
test('Does not change the input if locale is already defined', async () => {
const input = { locale: 'myLocale' };
await assignDefaultLocale(input);
expect(input).toStrictEqual({ locale: 'myLocale' });
});
test('Use default locale to set the locale on the input data', async () => {
const getDefaultLocaleMock = jest.fn(() => 'defaultLocale');
global.strapi = {
plugins: {
i18n: {
services: {
locales: {
getDefaultLocale: getDefaultLocaleMock,
},
},
},
},
};
const input = {};
await assignDefaultLocale(input);
expect(input).toStrictEqual({ locale: 'defaultLocale' });
expect(getDefaultLocaleMock).toHaveBeenCalled();
});
});
describe('addLocalizations', () => {
test('Does nothing if entry already as a localizations array', async () => {
const entry = { localizations: [] };
await addLocalizations(entry, { model });
expect(entry).toEqual({ localizations: [] });
expect(entry).toStrictEqual({ localizations: [] });
});
test('Updates entry in db', async () => {
@ -58,7 +94,7 @@ describe('localizations service', () => {
await addLocalizations(entry, { model });
expect(entry).toEqual({
expect(entry).toStrictEqual({
id: entry.id,
locale: entry.locale,
localizations: [
@ -127,4 +163,47 @@ describe('localizations service', () => {
expect(update).toHaveBeenCalledWith({ id: 2 }, { stars: 1 });
});
});
describe('removeEntryFromRelatedLocalizations', () => {
test('Does nothing if no localizations set', async () => {
const update = jest.fn();
global.strapi = {
query() {
return { update };
},
};
const entry = { id: 1, locale: 'test' };
await removeEntryFromRelatedLocalizations(entry, { model });
expect(update).not.toHaveBeenCalled();
});
test('Removes entry from localizations', async () => {
const update = jest.fn();
global.strapi = {
query() {
return { update };
},
};
const entry = {
id: 1,
locale: 'mainLocale',
localizations: [
{ id: 1, locale: 'mainLocale' },
{ id: 2, locale: 'otherLocale' },
],
};
await removeEntryFromRelatedLocalizations(entry, { model });
expect(update).toHaveBeenCalledTimes(1);
expect(update).toHaveBeenCalledWith(
{ id: 2 },
{ localizations: [{ id: 2, locale: 'otherLocale' }] }
);
});
});
});

View File

@ -1,8 +1,26 @@
'use strict';
const { pick, isNil } = require('lodash/fp');
const { getService } = require('../utils');
const { getNonLocalizedFields } = require('./content-types');
/**
* Adds the default locale to an object if it isn't defined yet
* @param {Object} data a data object before being persisted into db
*/
const assignDefaultLocale = async data => {
if (isNil(data.locale)) {
data.locale = await getService('locales').getDefaultLocale();
}
};
/**
* Create default localizations for an entry if it isn't defined yet
* @param {Object} entry entry to update
* @param {Object} options
* @param {Object} options.model corresponding model
*/
const addLocalizations = async (entry, { model }) => {
if (isNil(entry.localizations)) {
const localizations = [{ locale: entry.locale, id: entry.id }];
@ -12,10 +30,16 @@ const addLocalizations = async (entry, { model }) => {
}
};
/**
* Update non localized fields of all the related localizations of an entry with the entry values
* @param {Object} entry entry to update
* @param {Object} options
* @param {Object} options.model corresponding model
*/
const updateNonLocalizedFields = async (entry, { model }) => {
const fieldsToUpdate = pick(getNonLocalizedFields(model), entry);
if (Array.isArray(entry.localizations)) {
const fieldsToUpdate = pick(getNonLocalizedFields(model), entry);
const updateQueries = entry.localizations
.filter(({ id }) => id != entry.id)
.map(({ id }) => strapi.query(model.uid).update({ id }, fieldsToUpdate));
@ -24,8 +48,28 @@ const updateNonLocalizedFields = async (entry, { model }) => {
}
};
/**
* Remove entry from localizations & udpate related localizations
* This method should be used only after an entry is deleted
* @param {Object} entry entry to remove from localizations
* @param {Object} options
* @param {Object} options.model corresponding model
*/
const removeEntryFromRelatedLocalizations = async (entry, { model }) => {
if (Array.isArray(entry.localizations)) {
const newLocalizations = entry.localizations.filter(({ id }) => id != entry.id);
const updateQueries = newLocalizations.map(({ id }) => {
return strapi.query(model.uid).update({ id }, { localizations: newLocalizations });
});
await Promise.all(updateQueries);
}
};
module.exports = {
assignDefaultLocale,
addLocalizations,
updateNonLocalizedFields,
getNonLocalizedFields,
removeEntryFromRelatedLocalizations,
};