Merge pull request #9642 from strapi/i18n/default-params-locale

I18n/default params locale
This commit is contained in:
Alexandre BODIN 2021-03-09 11:44:08 +01:00 committed by GitHub
commit e82c7fb611
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 410 additions and 72 deletions

View File

@ -8,10 +8,7 @@
"options": {
"draftAndPublish": true,
"increments": true,
"timestamps": [
"created_at",
"updated_at"
],
"timestamps": ["created_at", "updated_at"],
"comment": ""
},
"pluginOptions": {
@ -41,13 +38,7 @@
}
},
"price_range": {
"enum": [
"very_cheap",
"cheap",
"average",
"expensive",
"very_expensive"
],
"enum": ["very_cheap", "cheap", "average", "expensive", "very_expensive"],
"type": "enumeration",
"pluginOptions": {
"i18n": {
@ -169,15 +160,6 @@
"localized": true
}
}
},
"parent": {
"collection": "restaurant",
"via": "children",
"dominant": true
},
"children": {
"collection": "restaurant",
"via": "parent"
}
}
}

View File

@ -16,7 +16,8 @@ module.exports = async () => {
const { actionProvider } = strapi.admin.services.permission;
actionProvider.register(actions);
getService('entity-service-decorator').decorate();
const { decorator } = getService('entity-service-decorator');
strapi.entityService.decorate(decorator);
await getService('locales').initDefaultLocale();

View File

@ -0,0 +1,240 @@
'use strict';
jest.mock('../localizations', () => {
return {
syncLocalizations: jest.fn(async () => {}),
updateNonLocalizedFields: jest.fn(async () => {}),
};
});
const { decorator } = require('../entity-service-decorator');
const { syncLocalizations, updateNonLocalizedFields } = require('../localizations');
const model = {
pluginOptions: {
i18n: {
localized: true,
},
},
};
const nonLocalizedModel = {
pluginOptions: {
i18n: {
localized: false,
},
},
};
const models = {
'test-model': model,
'non-localized-model': nonLocalizedModel,
};
describe('Entity service decorator', () => {
beforeAll(() => {
global.strapi = {
query() {
return {
create() {},
update() {},
};
},
db: {
getModel(uid) {
return models[uid || 'test-model'];
},
},
store: () => ({ get: () => 'en' }),
};
});
beforeEach(() => {
syncLocalizations.mockClear();
updateNonLocalizedFields.mockClear();
});
describe('wrapOptions', () => {
test('Calls original wrapOptions', async () => {
const defaultService = {
wrapOptions: jest.fn(() => Promise.resolve('li')),
};
const service = decorator(defaultService);
const input = { populate: ['test'] };
await service.wrapOptions(input, { model: 'test-model' });
expect(defaultService.wrapOptions).toHaveBeenCalledWith(input, { model: 'test-model' });
});
test('Does not wrap options if model is not localized', async () => {
const defaultService = {
wrapOptions: jest.fn(opts => Promise.resolve(opts)),
};
const service = decorator(defaultService);
const input = { populate: ['test'] };
const output = await service.wrapOptions(input, { model: 'non-localized-model' });
expect(output).toStrictEqual(input);
});
test('does not change non params options', async () => {
const defaultService = {
wrapOptions: jest.fn(opts => Promise.resolve(opts)),
};
const service = decorator(defaultService);
const input = { populate: ['test'] };
const output = await service.wrapOptions(input, { model: 'test-model' });
expect(output.populate).toStrictEqual(input.populate);
});
test('Adds locale param', async () => {
const defaultService = {
wrapOptions: jest.fn(opts => Promise.resolve(opts)),
};
const service = decorator(defaultService);
const input = { populate: ['test'] };
const output = await service.wrapOptions(input, { model: 'test-model' });
expect(output).toMatchObject({ params: { locale: 'en' } });
});
test('Replaces _locale param', async () => {
const defaultService = {
wrapOptions: jest.fn(opts => Promise.resolve(opts)),
};
const service = decorator(defaultService);
const input = {
params: {
_locale: 'fr',
},
populate: ['test'],
};
const output = await service.wrapOptions(input, { model: 'test-model' });
expect(output).toMatchObject({ params: { locale: 'fr' } });
});
});
describe('create', () => {
test('Calls original create', async () => {
const entry = {
id: 1,
};
const defaultService = {
create: jest.fn(() => Promise.resolve(entry)),
};
const service = decorator(defaultService);
const input = { data: { title: 'title ' } };
await service.create(input, { model: 'test-model' });
expect(defaultService.create).toHaveBeenCalledWith(input, { model: 'test-model' });
});
test('Calls syncLocalizations if model is localized', async () => {
const entry = {
id: 1,
localizations: [{ id: 2 }],
};
const defaultService = {
create: jest.fn(() => Promise.resolve(entry)),
};
const service = decorator(defaultService);
const input = { data: { title: 'title ' } };
await service.create(input, { model: 'test-model' });
expect(defaultService.create).toHaveBeenCalledWith(input, { model: 'test-model' });
expect(syncLocalizations).toHaveBeenCalledWith(entry, { model });
});
test('Skip processing if model is not localized', async () => {
const entry = {
id: 1,
localizations: [{ id: 2 }],
};
const defaultService = {
create: jest.fn(() => Promise.resolve(entry)),
};
const service = decorator(defaultService);
const input = { data: { title: 'title ' } };
const output = await service.create(input, { model: 'non-localized-model' });
expect(defaultService.create).toHaveBeenCalledWith(input, { model: 'non-localized-model' });
expect(syncLocalizations).not.toHaveBeenCalled();
expect(output).toStrictEqual(entry);
});
});
describe('update', () => {
test('Calls original update', async () => {
const entry = {
id: 1,
};
const defaultService = {
update: jest.fn(() => Promise.resolve(entry)),
};
const service = decorator(defaultService);
const input = { params: { id: 1 }, data: { title: 'title ' } };
await service.update(input, { model: 'test-model' });
expect(defaultService.update).toHaveBeenCalledWith(input, { model: 'test-model' });
});
test('Calls updateNonLocalizedFields if model is localized', async () => {
const entry = {
id: 1,
localizations: [{ id: 2 }],
};
const defaultService = {
update: jest.fn(() => Promise.resolve(entry)),
};
const service = decorator(defaultService);
const input = { params: { id: 1 }, data: { title: 'title ' } };
const output = await service.update(input, { model: 'test-model' });
expect(defaultService.update).toHaveBeenCalledWith(input, { model: 'test-model' });
expect(updateNonLocalizedFields).toHaveBeenCalledWith(entry, { model });
expect(output).toStrictEqual(entry);
});
test('Skip processing if model is not localized', async () => {
const entry = {
id: 1,
localizations: [{ id: 2 }],
};
const defaultService = {
update: jest.fn(() => Promise.resolve(entry)),
};
const service = decorator(defaultService);
const input = { params: { id: 1 }, data: { title: 'title ' } };
await service.update(input, { model: 'non-localized-model' });
expect(defaultService.update).toHaveBeenCalledWith(input, { model: 'non-localized-model' });
expect(updateNonLocalizedFields).not.toHaveBeenCalled();
});
});
});

View File

@ -1,20 +1,68 @@
'use strict';
const { getService } = require('../utils');
const { has, omit } = require('lodash/fp');
module.exports = {
decorate() {
strapi.entityService.decorate(entityServiceDecorator);
},
const { getDefaultLocale } = require('./locales');
const { isLocalized } = require('./content-types');
const { syncLocalizations, updateNonLocalizedFields } = require('./localizations');
const LOCALE_QUERY_FILTER = '_locale';
/**
* Adds default locale or replaces _locale by locale in query params
* @param {object} params - query params
*/
const wrapParams = async (params = {}) => {
if (has(LOCALE_QUERY_FILTER, params)) {
return {
...omit(LOCALE_QUERY_FILTER, params),
locale: params[LOCALE_QUERY_FILTER],
};
}
return {
...params,
locale: await getDefaultLocale(),
};
};
const entityServiceDecorator = service => ({
async create(params, ctx) {
/**
* Decorates the entity service with I18N business logic
* @param {object} service - entity service
*/
const decorator = service => ({
/**
* Wraps query options. In particular will add default locale to query params
* @param {object} opts - Query options object (params, data, files, populate)
* @param {object} ctx - Query context
* @param {object} ctx.model - Model that is being used
*/
async wrapOptions(opts = {}, ctx) {
const wrappedOptions = await service.wrapOptions(opts, ctx);
const model = strapi.db.getModel(ctx.model);
const entry = await service.create(params, ctx);
if (getService('content-types').isLocalized(model)) {
await getService('localizations').syncLocalizations(entry, { model });
if (!isLocalized(model)) {
return wrappedOptions;
}
return {
...wrappedOptions,
params: await wrapParams(wrappedOptions.params),
};
},
/**
* Creates an entry & make links between it and its related localizaionts
* @param {object} opts - Query options object (params, data, files, populate)
* @param {object} ctx - Query context
* @param {object} ctx.model - Model that is being used
*/
async create(opts, ctx) {
const model = strapi.db.getModel(ctx.model);
const entry = await service.create(opts, ctx);
if (isLocalized(model)) {
await syncLocalizations(entry, { model });
}
return entry;
@ -22,17 +70,23 @@ const entityServiceDecorator = service => ({
/**
* Updates an entry & update related localizations fields
* @param {obj} params - query params
* @param {obj} ctx - query context (model)
* @param {object} opts - Query options object (params, data, files, populate)
* @param {object} ctx - Query context
* @param {object} ctx.model - Model that is being used
*/
async update(params, ctx) {
async update(opts, ctx) {
const model = strapi.db.getModel(ctx.model);
const entry = await service.update(params, ctx);
const entry = await service.update(opts, ctx);
if (getService('content-types').isLocalized(model)) {
await getService('localizations').updateNonLocalizedFields(entry, { model });
if (isLocalized(model)) {
await updateNonLocalizedFields(entry, { model });
}
return entry;
},
});
module.exports = {
decorator,
wrapParams,
};

View File

@ -43,11 +43,25 @@ const createDefaultImplementation = ({ db, eventHub, entityValidator }) => ({
uploadFiles,
/**
* Promise to fetch all records
*
* @return {Promise}
* Returns default opt
* it is async so decorators can do async processing
* @param {object} params - query params to extend
* @param {object=} ctx - Query context
* @param {object} ctx.model - Model that is being used
*/
async find({ params, populate }, { model }) {
async wrapOptions(options = {}) {
return options;
},
/**
* Returns a list of entries
* @param {object} opts - Query options object (params, data, files, populate)
* @param {object} ctx - Query context
* @param {object} ctx.model - Model that is being used
*/
async find(opts, { model }) {
const { params, populate } = await this.wrapOptions(opts, { model });
const { kind } = db.getModel(model);
// return first element and ignore filters
@ -59,41 +73,63 @@ const createDefaultImplementation = ({ db, eventHub, entityValidator }) => ({
return db.query(model).find(params, populate);
},
findPage({ params, populate }, { model }) {
/**
* Returns a paginated list of entries
* @param {object} opts - Query options object (params, data, files, populate)
* @param {object} ctx - Query context
* @param {object} ctx.model - Model that is being used
*/
async findPage(opts, { model }) {
const { params, populate } = await this.wrapOptions(opts, { model });
return db.query(model).findPage(params, populate);
},
findWithRelationCounts({ params, populate }, { model }) {
/**
* Returns a list of entries with relation counters
* @param {object} opts - Query options object (params, data, files, populate)
* @param {object} ctx - Query context
* @param {object} ctx.model - Model that is being used
*/
async findWithRelationCounts(opts, { model }) {
const { params, populate } = await this.wrapOptions(opts, { model });
return db.query(model).findWithRelationCounts(params, populate);
},
/**
* Promise to fetch record
*
* @return {Promise}
* Returns one entry
* @param {object} opts - Query options object (params, data, files, populate)
* @param {object} ctx - Query context
* @param {object} ctx.model - Model that is being used
*/
async findOne(opts, { model }) {
const { params, populate } = await this.wrapOptions(opts, { model });
findOne({ params, populate }, { model }) {
return db.query(model).findOne(params, populate);
},
/**
* Promise to count record
*
* @return {Promise}
* Returns a count of entries
* @param {object} opts - Query options object (params, data, files, populate)
* @param {object} ctx - Query context
* @param {object} ctx.model - Model that is being used
*/
async count(opts, { model }) {
const { params } = await this.wrapOptions(opts, { model });
count({ params }, { model }) {
return db.query(model).count(params);
},
/**
* Promise to add record
*
* @return {Promise}
* Creates & returns a new entry
* @param {object} opts - Query options object (params, data, files, populate)
* @param {object} ctx - Query context
* @param {object} ctx.model - Model that is being used
*/
async create(opts, { model }) {
const { data, files } = await this.wrapOptions(opts, { model });
async create({ data, files }, { model }) {
const modelDef = db.getModel(model);
if (modelDef.kind === 'singleType') {
@ -124,12 +160,14 @@ const createDefaultImplementation = ({ db, eventHub, entityValidator }) => ({
},
/**
* Promise to edit record
*
* @return {Promise}
* Updates & returns an existing entry
* @param {object} opts - Query options object (params, data, files, populate)
* @param {object} ctx - Query context
* @param {object} ctx.model - Model that is being used
*/
async update(opts, { model }) {
const { params, data, files } = await this.wrapOptions(opts, { model });
async update({ params, data, files }, { model }) {
const modelDef = db.getModel(model);
const existingEntry = await db.query(model).findOne(params);
@ -155,12 +193,14 @@ const createDefaultImplementation = ({ db, eventHub, entityValidator }) => ({
},
/**
* Promise to delete a record
*
* @return {Promise}
* Deletes & returns the entry that was deleted
* @param {object} opts - Query options object (params, data, files, populate)
* @param {object} ctx - Query context
* @param {object} ctx.model - Model that is being used
*/
async delete(opts, { model }) {
const { params } = await this.wrapOptions(opts, { model });
async delete({ params }, { model }) {
const entry = await db.query(model).delete(params);
const modelDef = db.getModel(model);
@ -173,29 +213,50 @@ const createDefaultImplementation = ({ db, eventHub, entityValidator }) => ({
},
/**
* Promise to search records
*
* @return {Promise}
* Returns a list of matching entries
* @param {object} opts - Query options object (params, data, files, populate)
* @param {object} ctx - Query context
* @param {object} ctx.model - Model that is being used
*/
async search(opts, { model }) {
const { params, populate } = await this.wrapOptions(opts, { model });
search({ params, populate }, { model }) {
return db.query(model).search(params, populate);
},
searchWithRelationCounts({ params, populate }, { model }) {
/**
* Returns a list of matching entries with relations counters
* @param {object} opts - Query options object (params, data, files, populate)
* @param {object} ctx - Query context
* @param {object} ctx.model - Model that is being used
*/
async searchWithRelationCounts(opts, { model }) {
const { params, populate } = await this.wrapOptions(opts, { model });
return db.query(model).searchWithRelationCounts(params, populate);
},
searchPage({ params, populate }, { model }) {
/**
* Returns a paginated list of matching entries
* @param {object} opts - Query options object (params, data, files, populate)
* @param {object} ctx - Query context
* @param {object} ctx.model - Model that is being used
*/
async searchPage(opts, { model }) {
const { params, populate } = await this.wrapOptions(opts, { model });
return db.query(model).searchPage(params, populate);
},
/**
* Promise to count searched records
*
* @return {Promise}
* @param {object} opts - Query options object (params, data, files, populate)
* @param {object} ctx - Query context
* @param {object} ctx.model - Model that is being used
*/
countSearch({ params }, { model }) {
async countSearch(opts, { model }) {
const { params } = await this.wrapOptions(opts, { model });
return db.query(model).countSearch(params);
},
});