309 lines
8.2 KiB
JavaScript
Raw Normal View History

2021-02-17 18:30:06 +01:00
'use strict';
const _ = require('lodash');
const { has, prop, pick, reduce, map, keys, toPath } = require('lodash/fp');
2021-04-29 13:51:12 +02:00
const { contentTypes, parseMultipartData, sanitizeEntity } = require('@strapi/utils');
const { getService } = require('../utils');
const { getContentTypeRoutePrefix, isSingleType, getWritableAttributes } = contentTypes;
/**
* Returns a parsed request body. It handles whether the body is multipart or not
* @param {object} ctx - Koa request context
* @returns {{ data: { [key: string]: any }, files: { [key: string]: any } }}
*/
const parseRequest = ctx => {
if (ctx.is('multipart')) {
return parseMultipartData(ctx);
} else {
return { data: ctx.request.body, files: {} };
}
};
2021-02-17 18:30:06 +01:00
/**
* Returns all locales for an entry
* @param {object} entry
* @returns {string[]}
2021-02-17 18:30:06 +01:00
*/
const getAllLocales = entry => {
return [entry.locale, ...map(prop('locale'), entry.localizations)];
};
/**
* Returns all localizations ids for an entry
* @param {object} entry
* @returns {any[]}
*/
const getAllLocalizationsIds = entry => {
return [entry.id, ...map(prop('id'), entry.localizations)];
};
/**
* Returns a sanitizer object with a data & a file sanitizer for a content type
* @param {object} contentType
* @returns {{
* sanitizeInput(data: object): object,
* sanitizeInputFiles(files: object): object
* }}
*/
const createSanitizer = contentType => {
/**
* Returns the writable attributes of a content type in the localization routes
* @returns {string[]}
*/
const getAllowedAttributes = () => {
return getWritableAttributes(contentType).filter(
attributeName => !['locale', 'localizations'].includes(attributeName)
);
};
/**
* Sanitizes uploaded files to keep only writable ones
* @param {object} files - input files to sanitize
* @returns {object}
*/
const sanitizeInputFiles = files => {
const allowedFields = getAllowedAttributes();
return reduce(
(acc, keyPath) => {
const [rootKey] = toPath(keyPath);
if (allowedFields.includes(rootKey)) {
acc[keyPath] = files[keyPath];
}
return acc;
},
{},
keys(files)
);
};
/**
* Sanitizes input data to keep only writable attributes
* @param {object} data - input data to sanitize
* @returns {object}
*/
const sanitizeInput = data => {
return pick(getAllowedAttributes(), data);
};
return { sanitizeInput, sanitizeInputFiles };
};
/**
* Returns a handler to handle localizations creation in the core api
* @param {object} contentType
* @returns {(object) => void}
*/
const createLocalizationHandler = contentType => {
const { copyNonLocalizedAttributes } = getService('content-types');
const { sanitizeInput, sanitizeInputFiles } = createSanitizer(contentType);
/**
* Create localized entry from another one
*/
const createFromBaseEntry = async (ctx, entry) => {
const { data, files } = parseRequest(ctx);
const { findByCode } = getService('locales');
2021-03-16 15:51:00 +01:00
if (!has('locale', data)) {
2021-04-06 16:59:32 +02:00
throw strapi.errors.badRequest('locale.missing');
2021-03-16 15:51:00 +01:00
}
const matchingLocale = await findByCode(data.locale);
if (!matchingLocale) {
2021-04-06 16:59:32 +02:00
throw strapi.errors.badRequest('locale.invalid');
}
const usedLocales = getAllLocales(entry);
if (usedLocales.includes(data.locale)) {
2021-04-06 16:59:32 +02:00
throw strapi.errors.badRequest('locale.already.used');
}
const sanitizedData = {
...copyNonLocalizedAttributes(contentType, entry),
...sanitizeInput(data),
locale: data.locale,
localizations: getAllLocalizationsIds(entry),
};
const sanitizedFiles = sanitizeInputFiles(files);
2021-06-30 20:00:03 +02:00
const newEntry = await strapi.entityService.create(contentType.uid, {
data: sanitizedData,
files: sanitizedFiles,
});
ctx.body = sanitizeEntity(newEntry, { model: strapi.getModel(contentType.uid) });
};
if (isSingleType(contentType)) {
return async function(ctx) {
2021-07-19 16:47:24 +02:00
const entry = await strapi.query(contentType.uid).findOne({ populate: ['localizations'] });
if (!entry) {
2021-04-06 16:59:32 +02:00
throw strapi.errors.notFound('baseEntryId.invalid');
}
await createFromBaseEntry(ctx, entry);
};
}
return async function(ctx) {
const { id: baseEntryId } = ctx.params;
2021-07-19 16:47:24 +02:00
const entry = await strapi
.query(contentType.uid)
.findOne({ where: { id: baseEntryId }, populate: ['localizations'] });
if (!entry) {
2021-04-06 16:59:32 +02:00
throw strapi.errors.notFound('baseEntryId.invalid');
}
await createFromBaseEntry(ctx, entry);
2021-02-17 18:30:06 +01:00
};
};
/**
* Returns a route config to handle localizations creation in the core api
* @param {object} contentType
* @returns {{ method: string, path: string, handler: string, config: { policies: string[] }}}
*/
const createLocalizationRoute = contentType => {
const { modelName } = contentType;
const routePrefix = getContentTypeRoutePrefix(contentType);
const routePath = isSingleType(contentType)
? `/${routePrefix}/localizations`
: `/${routePrefix}/:id/localizations`;
2021-02-17 18:30:06 +01:00
return {
method: 'POST',
path: routePath,
2021-02-17 18:30:06 +01:00
handler: `${modelName}.createLocalization`,
config: {
policies: [],
},
};
};
/**
* Adds a route & an action to the core api controller of a content type to allow creating new localizations
* @param {object} contentType
*/
const addCreateLocalizationAction = contentType => {
const { modelName, apiName } = contentType;
const localizationRoute = createLocalizationRoute(contentType);
strapi.config.routes.push(localizationRoute);
2021-08-13 15:35:19 +02:00
// TODO: to replace with:
// strapi.controllers.extends(`api::${apiName}.${modelName}`, (contr) => ({
// ...controller,
// createLocalization = createLocalizationHandler(contentType),
// }));
// OR
// strapi.api(apiName).controllers.extends(modelName, (contr) => ({
// ...controller,
// createLocalization = createLocalizationHandler(contentType),
// }));
const controller = strapi.container.get('controllers').get(`api::${apiName}.${modelName}`);
controller.createLocalization = createLocalizationHandler(contentType);
2021-02-17 18:30:06 +01:00
};
const mergeCustomizer = (dest, src) => {
if (typeof dest === 'string') {
return `${dest}\n${src}`;
}
};
/**
* Add a graphql schema to the plugin's global graphl schema to be processed
* @param {object} schema
*/
2021-08-17 19:28:10 +02:00
// TODO: to replace with V4 config getter
const addGraphqlSchema = schema => {
2021-08-19 22:27:00 +02:00
_.mergeWith(strapi.config.get('plugin.i18n.schema.graphql'), schema, mergeCustomizer);
};
2021-04-06 16:59:32 +02:00
/**
* Add localization mutation & filters to use with the graphql plugin
* @param {object} contentType
*/
const addGraphqlLocalizationAction = contentType => {
2021-04-08 18:08:06 +02:00
const { globalId, modelName } = contentType;
2021-04-06 16:59:32 +02:00
if (!strapi.plugins.graphql) {
return;
}
2021-08-19 22:27:00 +02:00
const { toSingular, toPlural } = strapi.plugin('graphql').service('naming');
// We use a string instead of an enum as the locales can be changed in the admin
// NOTE: We could use a custom scalar so the validation becomes dynamic
const localeArgs = {
args: {
locale: 'String',
},
};
// add locale arguments in the existing queries
if (isSingleType(contentType)) {
const queryName = toSingular(modelName);
const mutationSuffix = _.upperFirst(queryName);
addGraphqlSchema({
2021-04-06 16:59:32 +02:00
resolver: {
Query: {
[queryName]: localeArgs,
},
2021-04-06 16:59:32 +02:00
Mutation: {
[`update${mutationSuffix}`]: localeArgs,
[`delete${mutationSuffix}`]: localeArgs,
},
},
});
} else {
const queryName = toPlural(modelName);
addGraphqlSchema({
resolver: {
Query: {
[queryName]: localeArgs,
[`${queryName}Connection`]: localeArgs,
},
},
});
}
// add new mutation to create a localization
2021-04-08 18:08:06 +02:00
const typeName = globalId;
const capitalizedName = _.upperFirst(toSingular(modelName));
const mutationName = `create${capitalizedName}Localization`;
const mutationDef = `${mutationName}(input: update${capitalizedName}Input!): ${typeName}!`;
const actionName = `${contentType.uid}.createLocalization`;
addGraphqlSchema({
mutation: mutationDef,
resolver: {
Mutation: {
[mutationName]: {
resolver: actionName,
2021-04-06 16:59:32 +02:00
},
},
},
});
2021-04-06 16:59:32 +02:00
};
2021-06-08 10:39:45 +02:00
module.exports = () => ({
2021-02-17 18:30:06 +01:00
addCreateLocalizationAction,
2021-04-06 16:59:32 +02:00
addGraphqlLocalizationAction,
createSanitizer,
2021-06-08 10:39:45 +02:00
});