add read/create/update route for locales

This commit is contained in:
Pierre Noël 2021-01-27 12:35:24 +01:00
parent 2c5e3d7872
commit 291aeb07bc
18 changed files with 1103 additions and 522 deletions

View File

@ -1,14 +1,9 @@
'use strict';
const { has, pipe, prop, pick } = require('lodash/fp');
const { MANY_RELATIONS } = require('strapi-utils').relations.constants;
const { MANY_RELATIONS, setCreatorFields } = require('strapi-utils').relations.constants;
const {
getService,
wrapBadRequest,
setCreatorFields,
pickWritableAttributes,
} = require('../utils');
const { getService, wrapBadRequest, pickWritableAttributes } = require('../utils');
const { validateBulkDeleteInput, validatePagination } = require('./validation');
module.exports = {

View File

@ -1,12 +1,9 @@
'use strict';
const { pipe } = require('lodash/fp');
const {
getService,
wrapBadRequest,
setCreatorFields,
pickWritableAttributes,
} = require('../utils');
const { setCreatorFields } = require('strapi-utils');
const { getService, wrapBadRequest, pickWritableAttributes } = require('../utils');
const findEntity = async model => {
const entityManager = getService('entity-manager');

View File

@ -98,6 +98,7 @@ const HIDDEN_CONTENT_TYPES = [
'plugins::upload.file',
'plugins::users-permissions.permission',
'plugins::users-permissions.role',
'plugins::i18n.locale',
];
const isHidden = ({ uid }) => startsWith('strapi::', uid) || HIDDEN_CONTENT_TYPES.includes(uid);

View File

@ -2,7 +2,6 @@
const { prop } = require('lodash/fp');
const wrapBadRequest = require('./wrap-bad-request');
const setCreatorFields = require('./set-creator-fields');
const pickWritableAttributes = require('./pick-writable-attributes');
// retrieve a local service
@ -13,6 +12,5 @@ const getService = name => {
module.exports = {
getService,
wrapBadRequest,
setCreatorFields,
pickWritableAttributes,
};

View File

@ -5,7 +5,43 @@
"path": "/iso-locales",
"handler": "iso-locales.listIsoLocales",
"config": {
"policies": []
"policies": [
"admin::isAuthenticatedAdmin",
["plugins::content-manager.hasPermissions", ["plugins::i18n.locale.read"]]
]
}
},
{
"method": "GET",
"path": "/locales",
"handler": "locales.listLocales",
"config": {
"policies": [
"admin::isAuthenticatedAdmin",
["plugins::content-manager.hasPermissions", ["plugins::i18n.locale.read"]]
]
}
},
{
"method": "POST",
"path": "/locales",
"handler": "locales.createLocale",
"config": {
"policies": [
"admin::isAuthenticatedAdmin",
["plugins::content-manager.hasPermissions", ["plugins::i18n.locale.create"]]
]
}
},
{
"method": "PUT",
"path": "/locales/:id",
"handler": "locales.updateLocale",
"config": {
"policies": [
"admin::isAuthenticatedAdmin",
["plugins::content-manager.hasPermissions", ["plugins::i18n.locale.update"]]
]
}
}
]

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,74 @@
'use strict';
const { setCreatorFields, sanitizeEntity } = require('strapi-utils');
const { getService } = require('../utils');
const { validateCreateLocaleInput, validateUpdateLocaleInput } = require('../validation/locales');
const { formatLocale } = require('../domain/locale');
const listLocales = async ctx => {
const localesService = getService('locales');
const locales = await localesService.find();
const model = strapi.getModel('locale', 'i18n');
ctx.body = sanitizeEntity(locales, { model });
};
const createLocale = async ctx => {
const { user } = ctx.state;
const { body } = ctx.request;
try {
await validateCreateLocaleInput(body);
} catch (err) {
return ctx.badRequest('ValidationError', err);
}
const localesService = getService('locales');
const model = strapi.getModel('locale', 'i18n');
const existingLocale = await localesService.findByCode(body.code);
if (existingLocale) {
return ctx.badRequest('This locale already exists');
}
let localeToCreate = formatLocale(body);
localeToCreate = setCreatorFields({ user })(localeToCreate);
const locale = await localesService.create(localeToCreate);
ctx.body = sanitizeEntity(locale, { model });
};
const updateLocale = async ctx => {
const { user } = ctx.state;
const { id } = ctx.params;
const { body } = ctx.request;
try {
await validateUpdateLocaleInput(body);
} catch (err) {
return ctx.badRequest('ValidationError', err);
}
const localesService = getService('locales');
const model = strapi.getModel('locale', 'i18n');
const existingLocale = await localesService.findById(id);
if (!existingLocale) {
return ctx.notFound('locale.notFound');
}
let updates = { name: body.name };
updates = setCreatorFields({ user, isEdition: true })(updates);
const updatedLocale = await localesService.update({ id }, updates);
ctx.body = sanitizeEntity(updatedLocale, { model });
};
module.exports = {
listLocales,
createLocale,
updateLocale,
};

View File

@ -0,0 +1,10 @@
'use strict';
const formatLocale = locale => {
return {
name: locale.name || null,
code: locale.code,
};
};
module.exports = { formatLocale };

View File

@ -0,0 +1,23 @@
{
"collectionName": "i18n_locales",
"info": {
"name": "locale",
"description": ""
},
"options": {
"timestamps": true
},
"attributes": {
"name": {
"type": "string",
"min": 1,
"max": 50,
"configurable": false
},
"code": {
"type": "string",
"unique": true,
"configurable": false
}
}
}

View File

@ -0,0 +1,141 @@
openapi: 3.0.2
info:
title: Strapi i18n Plugin
version: 1.0.0
servers:
- url: http://localhost:1337
description: Local server
externalDocs:
url: https://strapi.io/documentation
description: Strapi documentation
paths:
/i18n/iso-locales:
get:
tags:
- ISO-locales
description: List iso-locales that the app can use
responses:
200:
description: The list of the locales the app can use
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/iso-locale'
/i18n/locales:
get:
tags:
- Locales
description: List locales used by the app
responses:
200:
description: A list of locales
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/locale'
post:
tags:
- Locales
description: Create a locale
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/localeInputCreate'
responses:
200:
content:
application/json:
schema:
$ref: '#/components/schemas/locale'
/i18n/locales/{id}:
put:
tags:
- Locales
description: Update the name of a locale
parameters:
- $ref: '#/components/parameters/locale-id'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/localeInputUpdate'
responses:
200:
content:
application/json:
schema:
$ref: '#/components/schemas/locale'
components:
schemas:
id:
oneOf:
- type: string
- type: integer
localeInputCreate:
type: object
required:
- code
properties:
name:
type: string
description: Name of the locale
code:
type: string
description: ISO code of the locale
required: true
localeInputUpdate:
type: object
required:
- name
properties:
name:
type: string
description: Name of the locale
required: true
locale:
type: object
properties:
id:
$ref: '#/components/schemas/id'
name:
type: string
description: Name of the locale
code:
type: string
description: ISO code of the locale
iso-locale:
type: object
properties:
name:
type: string
description: Name of the locale
code:
type: string
description: ISO code of the locale
parameters:
locale-id:
in: path
name: id
schema:
type: string
required: true
description: Locale id
securitySchemes:
bearerAuth:
type: http
scheme: bearer
security:
- bearerAuth: []

View File

@ -9,7 +9,8 @@
"required": false
},
"dependencies": {
"lodash": "4.17.20"
"lodash": "4.17.20",
"strapi-utils": "3.4.5"
},
"author": {
"name": "A Strapi developer",

View File

@ -0,0 +1,39 @@
'use strict';
const find = async (...args) => {
const locales = await strapi.query('locale', 'i18n').find(...args);
return locales;
};
const findById = async id => {
const locales = await strapi.query('locale', 'i18n').findOne({ id });
return locales;
};
const findByCode = async code => {
const locales = await strapi.query('locale', 'i18n').findOne({ code });
return locales;
};
const create = async locale => {
const locales = await strapi.query('locale', 'i18n').create(locale);
return locales;
};
const update = async (params, updates) => {
const locales = await strapi.query('locale', 'i18n').update(params, updates);
return locales;
};
module.exports = {
find,
findById,
findByCode,
create,
update,
};

View File

@ -0,0 +1,205 @@
'use strict';
const { createStrapiInstance } = require('../../../test/helpers/strapi');
const { createAuthRequest } = require('../../../test/helpers/request');
const data = {
locales: [],
};
describe('CRUD locales', () => {
let rq;
let strapi;
beforeAll(async () => {
strapi = await createStrapiInstance();
rq = await createAuthRequest({ strapi });
}, 60000);
afterAll(async () => {
await strapi.destroy();
});
describe('Creation', () => {
test('Can create a locale', async () => {
const locale = {
name: 'French',
code: 'fr',
};
let res = await rq({
url: '/i18n/locales',
method: 'POST',
body: locale,
});
expect(res.statusCode).toBe(200);
expect(res.body).toMatchObject({
id: expect.anything(),
...locale,
});
data.locales.push(res.body);
});
test('Can create a locale if name is missing', async () => {
const locale = {
code: 'en',
};
let res = await rq({
url: '/i18n/locales',
method: 'POST',
body: locale,
});
expect(res.statusCode).toBe(200);
expect(res.body).toMatchObject({
id: expect.anything(),
name: null,
code: 'en',
});
data.locales.push(res.body);
});
test('Cannot create a locale if code is missing', async () => {
const locale = {
name: 'Italian',
};
let res = await rq({
url: '/i18n/locales',
method: 'POST',
body: locale,
});
expect(res.statusCode).toBe(400);
expect(res.body).toMatchObject({
data: { code: ['code is a required field'] },
error: 'Bad Request',
message: 'ValidationError',
statusCode: 400,
});
});
test('Cannot create a locale if code already exists', async () => {
const locale = {
code: 'fr',
name: 'random name',
};
let res = await rq({
url: '/i18n/locales',
method: 'POST',
body: locale,
});
expect(res.statusCode).toBe(400);
expect(res.body).toMatchObject({ message: 'This locale already exists' });
});
// - Faire documentation
// - Finir tests
// - voir comment stocker la locale par defaut
// -
test('Can create a locale even if name already exists', async () => {
const locale = {
name: 'French',
code: 'fr-FR',
};
let res = await rq({
url: '/i18n/locales',
method: 'POST',
body: locale,
});
expect(res.statusCode).toBe(200);
expect(res.body).toMatchObject({
id: expect.anything(),
...locale,
});
data.locales.push(res.body);
});
});
describe('Update', () => {
test('Can update the name of a locale', async () => {
const localeUpdate = {
name: 'French update',
};
let res = await rq({
url: `/i18n/locales/${data.locales[0].id}`,
method: 'PUT',
body: localeUpdate,
});
expect(res.statusCode).toBe(200);
expect(res.body).toMatchObject({
...data.locales[0],
...localeUpdate,
});
data.locales[0] = res.body;
});
test('Cannot update the code of a locale (without name)', async () => {
const localeUpdate = {
code: 'ak',
};
let res = await rq({
url: `/i18n/locales/${data.locales[0].id}`,
method: 'PUT',
body: localeUpdate,
});
expect(res.statusCode).toBe(400);
expect(res.body).toMatchObject({
data: {
name: ['name is a required field'],
undefined: ['this field has unspecified keys: code'],
},
error: 'Bad Request',
message: 'ValidationError',
statusCode: 400,
});
});
test('Cannot update the code of a locale (with name)', async () => {
const localeUpdate = {
name: 'French',
code: 'ak',
};
let res = await rq({
url: `/i18n/locales/${data.locales[0].id}`,
method: 'PUT',
body: localeUpdate,
});
expect(res.statusCode).toBe(400);
expect(res.body).toMatchObject({
data: {
undefined: ['this field has unspecified keys: code'],
},
error: 'Bad Request',
message: 'ValidationError',
statusCode: 400,
});
});
});
describe('Read', () => {
test('Can list the locales', async () => {
let res = await rq({
url: '/i18n/locales',
method: 'GET',
});
expect(res.statusCode).toBe(200);
expect(res.body).toHaveLength(data.locales.length);
expect(res.body.sort()).toMatchObject(data.locales.sort());
});
});
});

View File

@ -0,0 +1,49 @@
'use strict';
const { prop } = require('lodash/fp');
const { yup, formatYupErrors } = require('strapi-utils');
const { isoLocales } = require('../constants');
const allowedLocaleCodes = isoLocales.map(prop('code'));
const handleReject = error => Promise.reject(formatYupErrors(error));
const createLocaleSchema = yup
.object()
.shape({
name: yup
.string()
.min(1)
.max(50)
.nullable(),
code: yup
.string()
.oneOf(allowedLocaleCodes)
.required(),
})
.noUnknown();
const validateCreateLocaleInput = data => {
return createLocaleSchema.validate(data, { strict: true, abortEarly: false }).catch(handleReject);
};
const updateLocaleSchema = yup
.object()
.shape({
name: yup
.string()
.min(1)
.max(50)
.nullable()
.required(),
})
.noUnknown();
const validateUpdateLocaleInput = data => {
return updateLocaleSchema.validate(data, { strict: true, abortEarly: false }).catch(handleReject);
};
module.exports = {
validateCreateLocaleInput,
validateUpdateLocaleInput,
};

View File

@ -33,6 +33,7 @@ const contentTypes = require('./content-types');
const webhook = require('./webhook');
const env = require('./env-helper');
const relations = require('./relations');
const setCreatorFields = require('./set-creator-fields');
module.exports = {
yup,
@ -65,4 +66,5 @@ module.exports = {
webhook,
env,
relations,
setCreatorFields,
};

View File

@ -1,10 +1,7 @@
'use strict';
const { assign, assoc } = require('lodash/fp');
const {
CREATED_BY_ATTRIBUTE,
UPDATED_BY_ATTRIBUTE,
} = require('strapi-utils').contentTypes.constants;
const { CREATED_BY_ATTRIBUTE, UPDATED_BY_ATTRIBUTE } = require('./content-types').constants;
module.exports = ({ user, isEdition = false }) => data => {
if (isEdition) {

View File

@ -54,6 +54,7 @@ const generateTestApp = async ({ appName, database }) => {
'strapi-plugin-graphql',
'strapi-plugin-i18n',
'strapi-plugin-documentation',
'strapi-plugin-i18n',
],
additionalsDependencies: {},
};

View File

@ -18172,6 +18172,18 @@ stealthy-require@^1.1.1:
resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
strapi-utils@3.4.5:
version "3.4.5"
resolved "https://registry.yarnpkg.com/strapi-utils/-/strapi-utils-3.4.5.tgz#138665a4852c9c7c618bc537b2d65c70aaff3926"
integrity sha512-sRkCtnIt6BWc7rKJsg3e0bGFwS09phLYFU2qFEvMLuhGmPremjt+VyBn57cdung7bsMkJ5yQF5JA0e9w4LYYPQ==
dependencies:
"@sindresorhus/slugify" "1.1.0"
date-fns "^2.8.1"
lodash "4.17.20"
pino "^4.7.1"
pluralize "^8.0.0"
yup "0.29.3"
stream-browserify@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b"