mirror of
https://github.com/strapi/strapi.git
synced 2025-11-12 08:08:05 +00:00
Merge pull request #9430 from strapi/i18n/lifecycles-rework
Add Lifecycles for i18n
This commit is contained in:
commit
9160c46a80
@ -398,7 +398,7 @@ const createOrUpdateTable = async ({ table, attributes, definition, ORM, model }
|
|||||||
|
|
||||||
module.exports = async ({ ORM, loadedModel, definition, connection, model }) => {
|
module.exports = async ({ ORM, loadedModel, definition, connection, model }) => {
|
||||||
// run migrations
|
// run migrations
|
||||||
await strapi.db.migrations.runMigration(migrateSchemas, {
|
await strapi.db.migrations.run(migrateSchemas, {
|
||||||
ORM,
|
ORM,
|
||||||
loadedModel,
|
loadedModel,
|
||||||
definition,
|
definition,
|
||||||
|
|||||||
@ -301,7 +301,7 @@ module.exports = async ({ models, target }, ctx) => {
|
|||||||
const definitionDidChange = await didDefinitionChange(definition, instance);
|
const definitionDidChange = await didDefinitionChange(definition, instance);
|
||||||
|
|
||||||
// run migrations
|
// run migrations
|
||||||
await strapi.db.migrations.runMigration(migrateSchema, {
|
await strapi.db.migrations.run(migrateSchema, {
|
||||||
definition,
|
definition,
|
||||||
model: modelInstance,
|
model: modelInstance,
|
||||||
ORM: instance,
|
ORM: instance,
|
||||||
|
|||||||
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -7,6 +7,7 @@ const createConnectorRegistry = require('./connector-registry');
|
|||||||
const constants = require('./constants');
|
const constants = require('./constants');
|
||||||
const { validateModelSchemas } = require('./validation');
|
const { validateModelSchemas } = require('./validation');
|
||||||
const createMigrationManager = require('./migration-manager');
|
const createMigrationManager = require('./migration-manager');
|
||||||
|
const createLifecycleManager = require('./lifecycle-manager');
|
||||||
|
|
||||||
class DatabaseManager {
|
class DatabaseManager {
|
||||||
constructor(strapi) {
|
constructor(strapi) {
|
||||||
@ -23,6 +24,7 @@ class DatabaseManager {
|
|||||||
this.models = new Map();
|
this.models = new Map();
|
||||||
|
|
||||||
this.migrations = createMigrationManager(this);
|
this.migrations = createMigrationManager(this);
|
||||||
|
this.lifecycles = createLifecycleManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
async initialize() {
|
async initialize() {
|
||||||
|
|||||||
33
packages/strapi-database/lib/lifecycle-manager.js
Normal file
33
packages/strapi-database/lib/lifecycle-manager.js
Normal 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();
|
||||||
@ -14,7 +14,7 @@ class MigrationManager {
|
|||||||
this.migrations.push(migration);
|
this.migrations.push(migration);
|
||||||
}
|
}
|
||||||
|
|
||||||
async runMigration(fn, options, context = {}) {
|
async run(fn, options, context = {}) {
|
||||||
debug('Run migration');
|
debug('Run migration');
|
||||||
await this.runBefore(options, context);
|
await this.runBefore(options, context);
|
||||||
await fn(options, context);
|
await fn(options, context);
|
||||||
|
|||||||
@ -4,6 +4,14 @@ const _ = require('lodash');
|
|||||||
const createQuery = require('../create-query');
|
const createQuery = require('../create-query');
|
||||||
|
|
||||||
describe('Database queries', () => {
|
describe('Database queries', () => {
|
||||||
|
global.strapi = {
|
||||||
|
db: {
|
||||||
|
lifecycles: {
|
||||||
|
run() {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
describe('Substitute id with primaryKey in parameters', () => {
|
describe('Substitute id with primaryKey in parameters', () => {
|
||||||
test.each(['create', 'update', 'delete', 'find', 'findOne', 'search', 'count', 'countSearch'])(
|
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',
|
'Calling "%s" replaces id by the primaryKey in the params of the model before calling the underlying connector',
|
||||||
|
|||||||
@ -3,6 +3,10 @@
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
const executeLifecycle = async (lifecycle, model, ...args) => {
|
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}`)) {
|
if (_.has(model, `lifecycles.${lifecycle}`)) {
|
||||||
await model.lifecycles[lifecycle](...args);
|
await model.lifecycles[lifecycle](...args);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const _ = require('lodash');
|
|
||||||
const { capitalize } = require('lodash/fp');
|
const { capitalize } = require('lodash/fp');
|
||||||
const { getService } = require('../../utils');
|
const { getService } = require('../../utils');
|
||||||
|
|
||||||
@ -26,23 +25,23 @@ module.exports = async () => {
|
|||||||
await getService('locales').setDefaultLocale(DEFAULT_LOCALE);
|
await getService('locales').setDefaultLocale(DEFAULT_LOCALE);
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.values(strapi.models).forEach(model => {
|
Object.values(strapi.models)
|
||||||
if (getService('content-types').isLocalized(model)) {
|
.filter(model => getService('content-types').isLocalized(model))
|
||||||
// TODO: support adding lifecycles programmatically or connecting to a database event handler to avoid conflicts with existing lifecycles fonctions
|
.forEach(model => {
|
||||||
|
strapi.db.lifecycles.register({
|
||||||
_.set(model, 'lifecycles.beforeCreate', async data => {
|
model: model.uid,
|
||||||
if (!data.locale) {
|
async beforeCreate(data) {
|
||||||
data.locale = await getService('locales').getDefaultLocale();
|
await getService('localizations').assignDefaultLocale(data);
|
||||||
}
|
},
|
||||||
});
|
async afterCreate(entry) {
|
||||||
|
|
||||||
_.set(model, 'lifecycles.afterCreate', async entry => {
|
|
||||||
await getService('localizations').addLocalizations(entry, { model });
|
await getService('localizations').addLocalizations(entry, { model });
|
||||||
});
|
},
|
||||||
|
async afterUpdate(entry) {
|
||||||
_.set(model, 'lifecycles.afterUpdate', async entry => {
|
|
||||||
await getService('localizations').updateNonLocalizedFields(entry, { model });
|
await getService('localizations').updateNonLocalizedFields(entry, { model });
|
||||||
|
},
|
||||||
|
async afterDelete(entry) {
|
||||||
|
await getService('localizations').removeEntryFromRelatedLocalizations(entry, { model });
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const { addLocalizations, updateNonLocalizedFields } = require('../localizations');
|
const {
|
||||||
|
assignDefaultLocale,
|
||||||
|
addLocalizations,
|
||||||
|
updateNonLocalizedFields,
|
||||||
|
removeEntryFromRelatedLocalizations,
|
||||||
|
} = require('../localizations');
|
||||||
|
|
||||||
const model = {
|
const model = {
|
||||||
uid: 'test-model',
|
uid: 'test-model',
|
||||||
@ -20,12 +25,43 @@ const model = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe('localizations service', () => {
|
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', () => {
|
describe('addLocalizations', () => {
|
||||||
test('Does nothing if entry already as a localizations array', async () => {
|
test('Does nothing if entry already as a localizations array', async () => {
|
||||||
const entry = { localizations: [] };
|
const entry = { localizations: [] };
|
||||||
await addLocalizations(entry, { model });
|
await addLocalizations(entry, { model });
|
||||||
|
|
||||||
expect(entry).toEqual({ localizations: [] });
|
expect(entry).toStrictEqual({ localizations: [] });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Updates entry in db', async () => {
|
test('Updates entry in db', async () => {
|
||||||
@ -58,7 +94,7 @@ describe('localizations service', () => {
|
|||||||
|
|
||||||
await addLocalizations(entry, { model });
|
await addLocalizations(entry, { model });
|
||||||
|
|
||||||
expect(entry).toEqual({
|
expect(entry).toStrictEqual({
|
||||||
id: entry.id,
|
id: entry.id,
|
||||||
locale: entry.locale,
|
locale: entry.locale,
|
||||||
localizations: [
|
localizations: [
|
||||||
@ -127,4 +163,47 @@ describe('localizations service', () => {
|
|||||||
expect(update).toHaveBeenCalledWith({ id: 2 }, { stars: 1 });
|
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' }] }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,8 +1,26 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const { pick, isNil } = require('lodash/fp');
|
const { pick, isNil } = require('lodash/fp');
|
||||||
|
|
||||||
|
const { getService } = require('../utils');
|
||||||
const { getNonLocalizedFields } = require('./content-types');
|
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 }) => {
|
const addLocalizations = async (entry, { model }) => {
|
||||||
if (isNil(entry.localizations)) {
|
if (isNil(entry.localizations)) {
|
||||||
const localizations = [{ locale: entry.locale, id: entry.id }];
|
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 updateNonLocalizedFields = async (entry, { model }) => {
|
||||||
|
if (Array.isArray(entry.localizations)) {
|
||||||
const fieldsToUpdate = pick(getNonLocalizedFields(model), entry);
|
const fieldsToUpdate = pick(getNonLocalizedFields(model), entry);
|
||||||
|
|
||||||
if (Array.isArray(entry.localizations)) {
|
|
||||||
const updateQueries = entry.localizations
|
const updateQueries = entry.localizations
|
||||||
.filter(({ id }) => id != entry.id)
|
.filter(({ id }) => id != entry.id)
|
||||||
.map(({ id }) => strapi.query(model.uid).update({ id }, fieldsToUpdate));
|
.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 = {
|
module.exports = {
|
||||||
|
assignDefaultLocale,
|
||||||
addLocalizations,
|
addLocalizations,
|
||||||
updateNonLocalizedFields,
|
updateNonLocalizedFields,
|
||||||
getNonLocalizedFields,
|
removeEntryFromRelatedLocalizations,
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user