2021-02-17 18:30:06 +01:00
|
|
|
'use strict';
|
|
|
|
|
2021-03-14 11:23:13 +01:00
|
|
|
const _ = require('lodash');
|
2021-03-19 10:12:51 +01:00
|
|
|
const { has, prop, pick, reduce, map, keys, toPath } = require('lodash/fp');
|
2021-03-14 11:23:13 +01: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
|
|
|
|
|
|
|
/**
|
2021-03-19 10:12:51 +01:00
|
|
|
* Returns all locales for an entry
|
|
|
|
* @param {object} entry
|
|
|
|
* @returns {string[]}
|
2021-02-17 18:30:06 +01:00
|
|
|
*/
|
2021-03-19 10:12:51 +01:00
|
|
|
const getAllLocales = entry => {
|
|
|
|
return [entry.locale, ...map(prop('locale'), entry.localizations)];
|
|
|
|
};
|
2021-03-14 11:23:13 +01:00
|
|
|
|
2021-03-19 10:12:51 +01:00
|
|
|
/**
|
|
|
|
* 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)
|
|
|
|
);
|
2021-03-14 11:23:13 +01:00
|
|
|
};
|
|
|
|
|
2021-03-19 10:12:51 +01:00
|
|
|
/**
|
|
|
|
* 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)
|
|
|
|
);
|
2021-03-14 11:23:13 +01:00
|
|
|
};
|
|
|
|
|
2021-03-19 10:12:51 +01:00
|
|
|
/**
|
|
|
|
* Sanitizes input data to keep only writable attributes
|
|
|
|
* @param {object} data - input data to sanitize
|
|
|
|
* @returns {object}
|
|
|
|
*/
|
|
|
|
const sanitizeInput = data => {
|
|
|
|
return pick(getAllowedAttributes(), data);
|
2021-03-14 11:23:13 +01:00
|
|
|
};
|
|
|
|
|
2021-03-19 10:12:51 +01:00
|
|
|
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);
|
|
|
|
|
2021-03-14 11:23:13 +01:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
}
|
|
|
|
|
2021-03-14 11:23:13 +01:00
|
|
|
const matchingLocale = await findByCode(data.locale);
|
|
|
|
if (!matchingLocale) {
|
2021-04-06 16:59:32 +02:00
|
|
|
throw strapi.errors.badRequest('locale.invalid');
|
2021-03-14 11:23:13 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const usedLocales = getAllLocales(entry);
|
|
|
|
if (usedLocales.includes(data.locale)) {
|
2021-04-06 16:59:32 +02:00
|
|
|
throw strapi.errors.badRequest('locale.already.used');
|
2021-03-14 11:23:13 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const sanitizedData = {
|
|
|
|
...copyNonLocalizedAttributes(contentType, entry),
|
|
|
|
...sanitizeInput(data),
|
|
|
|
locale: data.locale,
|
2021-03-19 10:12:51 +01:00
|
|
|
localizations: getAllLocalizationsIds(entry),
|
2021-03-14 11:23:13 +01:00
|
|
|
};
|
|
|
|
|
2021-03-19 10:12:51 +01:00
|
|
|
const sanitizedFiles = sanitizeInputFiles(files);
|
2021-03-14 11:23:13 +01:00
|
|
|
|
|
|
|
const newEntry = await strapi.entityService.create(
|
|
|
|
{ data: sanitizedData, files: sanitizedFiles },
|
|
|
|
{ model: contentType.uid }
|
|
|
|
);
|
|
|
|
|
|
|
|
ctx.body = sanitizeEntity(newEntry, { model: strapi.getModel(contentType.uid) });
|
|
|
|
};
|
|
|
|
|
|
|
|
if (isSingleType(contentType)) {
|
|
|
|
return async function(ctx) {
|
|
|
|
const entry = await strapi.query(contentType.uid).findOne();
|
|
|
|
|
|
|
|
if (!entry) {
|
2021-04-06 16:59:32 +02:00
|
|
|
throw strapi.errors.notFound('baseEntryId.invalid');
|
2021-03-14 11:23:13 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
await createFromBaseEntry(ctx, entry);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return async function(ctx) {
|
|
|
|
const { id: baseEntryId } = ctx.params;
|
|
|
|
|
|
|
|
const entry = await strapi.query(contentType.uid).findOne({ id: baseEntryId });
|
|
|
|
|
|
|
|
if (!entry) {
|
2021-04-06 16:59:32 +02:00
|
|
|
throw strapi.errors.notFound('baseEntryId.invalid');
|
2021-03-14 11:23:13 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
2021-03-14 11:23:13 +01:00
|
|
|
const routePrefix = getContentTypeRoutePrefix(contentType);
|
|
|
|
const routePath = isSingleType(contentType)
|
|
|
|
? `/${routePrefix}/localizations`
|
|
|
|
: `/${routePrefix}/:id/localizations`;
|
|
|
|
|
2021-02-17 18:30:06 +01:00
|
|
|
return {
|
|
|
|
method: 'POST',
|
2021-03-14 11:23:13 +01:00
|
|
|
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);
|
|
|
|
|
|
|
|
const coreApiControllerPath = `api.${apiName}.controllers.${modelName}.createLocalization`;
|
|
|
|
const handler = createLocalizationHandler(contentType);
|
|
|
|
|
|
|
|
strapi.config.routes.push(localizationRoute);
|
|
|
|
|
2021-03-14 11:23:13 +01:00
|
|
|
_.set(strapi, coreApiControllerPath, handler);
|
2021-02-17 18:30:06 +01:00
|
|
|
};
|
|
|
|
|
2021-04-07 12:06:32 +02: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
|
|
|
|
*/
|
|
|
|
const addGraphqlSchema = schema => {
|
|
|
|
_.mergeWith(strapi.plugins.i18n.config.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-04-07 12:06:32 +02:00
|
|
|
const { toSingular, toPlural } = strapi.plugins.graphql.services.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: {
|
2021-04-07 12:06:32 +02:00
|
|
|
Query: {
|
|
|
|
[queryName]: localeArgs,
|
|
|
|
},
|
2021-04-06 16:59:32 +02:00
|
|
|
Mutation: {
|
2021-04-07 12:06:32 +02:00
|
|
|
[`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;
|
2021-04-07 12:06:32 +02:00
|
|
|
const mutationName = `create${typeName}Localization`;
|
|
|
|
const mutationDef = `${mutationName}(input: update${typeName}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-07 12:06:32 +02:00
|
|
|
});
|
2021-04-06 16:59:32 +02:00
|
|
|
};
|
|
|
|
|
2021-02-17 18:30:06 +01:00
|
|
|
module.exports = {
|
|
|
|
addCreateLocalizationAction,
|
2021-04-06 16:59:32 +02:00
|
|
|
addGraphqlLocalizationAction,
|
2021-03-19 10:12:51 +01:00
|
|
|
createSanitizer,
|
2021-02-17 18:30:06 +01:00
|
|
|
};
|