1862 lines
54 KiB
JavaScript
Raw Normal View History

'use strict';
/**
* Documentation.js service
*
* @description: A set of functions similar to controller's actions to avoid code duplication.
*/
const fs = require('fs');
const path = require('path');
const _ = require('lodash');
const moment = require('moment');
const pathToRegexp = require('path-to-regexp');
// FIXME
/* eslint-disable import/extensions */
2021-08-19 16:49:33 +02:00
const defaultSettings = require('../config/default-config');
const defaultComponents = require('./utils/components.json');
const form = require('./utils/forms.json');
const parametersOptions = require('./utils/parametersOptions.json');
// keys to pick from the extended config
const defaultSettingsKeys = Object.keys(defaultSettings);
const customIsEqual = (obj1, obj2) => _.isEqualWith(obj1, obj2, customComparator);
const customComparator = (value1, value2) => {
if (_.isArray(value1) && _.isArray(value2)) {
if (value1.length !== value2.length) {
return false;
}
return value1.every(el1 => value2.findIndex(el2 => customIsEqual(el1, el2)) >= 0);
}
};
2021-07-20 12:12:30 +02:00
module.exports = ({ strapi }) => ({
areObjectsEquals(obj1, obj2) {
// stringify to remove nested empty objects
return customIsEqual(this.cleanObject(obj1), this.cleanObject(obj2));
},
cleanObject: obj => JSON.parse(JSON.stringify(obj)),
arrayCustomizer(objValue, srcValue) {
if (_.isArray(objValue)) return objValue.concat(srcValue);
},
checkIfAPIDocNeedsUpdate(apiName) {
const prevDocumentation = this.createDocObject(this.retrieveDocumentation(apiName));
const currentDocumentation = this.createDocObject(this.createDocumentationFile(apiName, false));
return !this.areObjectsEquals(prevDocumentation, currentDocumentation);
},
/**
* Check if the documentation folder with its related version of an API exists
* @param {String} apiName
*/
checkIfDocumentationFolderExists(apiName) {
try {
fs.accessSync(this.getDocumentationPath(apiName));
return true;
} catch (err) {
return false;
}
},
checkIfPluginDocumentationFolderExists(pluginName) {
try {
fs.accessSync(this.getPluginDocumentationPath(pluginName));
return true;
} catch (err) {
return false;
}
},
checkIfPluginDocNeedsUpdate(pluginName) {
const prevDocumentation = this.createDocObject(this.retrieveDocumentation(pluginName, true));
const currentDocumentation = this.createDocObject(
this.createPluginDocumentationFile(pluginName, false)
);
return !this.areObjectsEquals(prevDocumentation, currentDocumentation);
},
checkIfApiDefaultDocumentationFileExist(apiName, docName) {
try {
fs.accessSync(this.getAPIOverrideDocumentationPath(apiName, docName));
return true;
} catch (err) {
return false;
}
},
checkIfPluginDefaultDocumentFileExists(pluginName, docName) {
try {
fs.accessSync(this.getPluginOverrideDocumentationPath(pluginName, docName));
return true;
} catch (err) {
return false;
}
},
/**
* Check if the documentation folder exists in the documentation plugin
* @returns {Boolean}
*/
checkIfMergedDocumentationFolderExists() {
try {
fs.accessSync(this.getMergedDocumentationPath());
return true;
} catch (err) {
return false;
}
},
/**
* Recursively create missing directories
2018-12-13 16:29:57 +01:00
* @param {String} targetDir
*
*/
createDocumentationDirectory(targetDir) {
const sep = path.sep;
const initDir = path.isAbsolute(targetDir) ? sep : '';
const baseDir = '.';
return targetDir.split(sep).reduce((parentDir, childDir) => {
const curDir = path.resolve(baseDir, parentDir, childDir);
try {
fs.mkdirSync(curDir);
} catch (err) {
if (err.code === 'EEXIST') {
// curDir already exists!
return curDir;
}
// To avoid `EISDIR` error on Mac and `EACCES`-->`ENOENT` and `EPERM` on Windows.
if (err.code === 'ENOENT') {
// Throw the original parentDir error on curDir `ENOENT` failure.
throw new Error(
`Impossible to create the documentation folder in '${parentDir}', please check the permissions.`
);
}
const caughtErr = ['EACCES', 'EPERM', 'EISDIR'].indexOf(err.code) > -1;
if (!caughtErr || (caughtErr && targetDir === curDir)) {
throw err; // Throw if it's just the last created dir.
}
}
return curDir;
}, initDir);
},
/**
* Create the apiName.json and unclassified.json files inside an api's documentation/version folder
* @param {String} apiName
*/
createDocumentationFile(apiName, writeFile = true) {
// Retrieve all the routes from an API
const apiRoutes = this.getApiRoutes(apiName);
const apiDocumentation = this.generateApiDocumentation(apiName, apiRoutes);
return Object.keys(apiDocumentation).reduce((acc, docName) => {
const targetFile = path.resolve(this.getDocumentationPath(apiName), `${docName}.json`);
// Create the components object in each documentation file when we can create it
const components =
strapi.models[docName] !== undefined ? this.generateResponseComponent(docName) : {};
const tags = docName.split('-').length > 1 ? [] : this.generateTags(apiName, docName);
const documentation = Object.assign(apiDocumentation[docName], components, { tags });
try {
if (writeFile) {
return fs.writeFileSync(targetFile, JSON.stringify(documentation, null, 2), 'utf8');
} else {
return acc.concat(documentation);
}
} catch (err) {
return acc;
}
}, []);
},
createPluginDocumentationFile(pluginName, writeFile = true) {
const pluginRoutes = this.getPluginRoutesWithDescription(pluginName);
const pluginDocumentation = this.generatePluginDocumentation(pluginName, pluginRoutes);
return Object.keys(pluginDocumentation).reduce((acc, docName) => {
const targetFile = path.resolve(
this.getPluginDocumentationPath(pluginName),
`${docName}.json`
);
const components =
_.get(strapi, this.getModelForPlugin(docName, pluginName)) !== undefined &&
pluginName !== 'upload'
? this.generateResponseComponent(docName, pluginName, true)
: {};
const [plugin, name] = this.getModelAndNameForPlugin(docName, pluginName);
const tags =
docName !== 'unclassified'
? this.generateTags(plugin, docName, _.upperFirst(this.formatTag(plugin, name)), true)
: [];
const documentation = Object.assign(pluginDocumentation[docName], components, { tags });
try {
if (writeFile) {
return fs.writeFileSync(targetFile, JSON.stringify(documentation, null, 2), 'utf8');
} else {
return acc.concat(documentation);
}
} catch (err) {
// Silent
}
}, []);
},
createDocObject(array) {
// use custom merge for arrays
return array.reduce((acc, curr) => _.mergeWith(acc, curr, this.arrayCustomizer), {});
},
async deleteDocumentation(version = this.getDocumentationVersion()) {
const recursiveDeleteFiles = async (folderPath, removeCompleteFolder = true) => {
// Check if folderExist
try {
const arrayOfPromises = [];
fs.accessSync(folderPath);
const items = fs.readdirSync(folderPath).filter(x => x[0] !== '.');
items.forEach(item => {
const itemPath = path.join(folderPath, item);
// Check if directory
if (fs.lstatSync(itemPath).isDirectory()) {
if (removeCompleteFolder) {
return arrayOfPromises.push(recursiveDeleteFiles(itemPath), removeCompleteFolder);
} else if (!itemPath.includes('overrides')) {
return arrayOfPromises.push(recursiveDeleteFiles(itemPath), removeCompleteFolder);
}
} else {
// Delete all files
try {
fs.unlinkSync(itemPath);
} catch (err) {
console.log('Cannot delete file', err);
}
}
});
await Promise.all(arrayOfPromises);
try {
if (removeCompleteFolder) {
fs.rmdirSync(folderPath);
}
} catch (err) {
// console.log(err);
}
} catch (err) {
// console.log('The folder does not exist');
}
};
const arrayOfPromises = [];
// Delete api's documentation
const apis = this.getApis();
const plugins = this.getPluginsWithDocumentationNeeded();
apis.forEach(api => {
const apiPath = path.join(strapi.config.appPath, 'api', api, 'documentation', version);
arrayOfPromises.push(recursiveDeleteFiles(apiPath));
});
plugins.forEach(plugin => {
const pluginPath = path.join(
strapi.config.appPath,
'extensions',
plugin,
'documentation',
version
);
if (version !== '1.0.0') {
arrayOfPromises.push(recursiveDeleteFiles(pluginPath));
} else {
arrayOfPromises.push(recursiveDeleteFiles(pluginPath, false));
}
});
const fullDocPath = path.join(
strapi.config.appPath,
'extensions',
'documentation',
'documentation',
version
);
arrayOfPromises.push(recursiveDeleteFiles(fullDocPath));
return Promise.all(arrayOfPromises);
},
/**
*
* Wrap endpoints variables in curly braces
* @param {String} endPoint
* @returns {String} (/products/{id})
*/
formatApiEndPoint(endPoint) {
return pathToRegexp
.parse(endPoint)
.map(token => {
if (_.isObject(token)) {
return token.prefix + '{' + token.name + '}'; // eslint-disable-line prefer-template
}
return token;
})
.join('');
},
/**
* Format a plugin model for example users-permissions, user => Users-Permissions - User
* @param {Sting} plugin
* @param {String} name
* @param {Boolean} withoutSpace
* @return {String}
*/
formatTag(plugin, name, withoutSpace = false) {
const formattedPluginName = plugin
.split('-')
.map(i => _.upperFirst(i))
Make plugins documentation generation optional (#5465) * Make plugins documentation generation optional Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Removing "-" character from schema names for generated plugin documentation so it becomes compatible with AWS API gateway import API feature using OAS file. Adding only one property to the settings file pluginsForWhichToGenerateDoc so that the user can choose for which plugin he wishes to generate documentation. Adding a parameter to generate or not the default response (this can now be set to false to have a documentation that is compatible with AWS API Gateway). Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Updating settings.json to start without the pluginsForWhichToGenerateDoc key so that all plugins documentation gets generated by default. Updated the documentation to reflect this change. Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Updating documentation to correct typos and rephrase some sentences to make them more clear. Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Destructuring the config to the var pluginsForWhichToGenerateDoc instead of renaming it to take into account comment from Alexandre Bodin Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Destructuring generateDefaultResponse and pluginsForWhichToGenerateDoc from strapi config object based on feedback from Alexandre Bodin Signed-off-by: Ralph Maroun <rmaroun@outlook.com> Co-authored-by: Alexandre BODIN <alexandrebodin@users.noreply.github.com>
2020-03-20 14:03:51 +01:00
.join('');
const formattedName = _.upperFirst(name);
if (withoutSpace) {
return `${formattedPluginName}${formattedName}`;
}
return `${formattedPluginName} - ${formattedName}`;
},
generateAssociationSchema(attributes, getter) {
return Object.keys(attributes).reduce(
(acc, curr) => {
const attribute = attributes[curr];
const isField = !_.has(attribute, 'model') && !_.has(attribute, 'collection');
if (attribute.required) {
acc.required.push(curr);
}
if (isField) {
acc.properties[curr] = { type: this.getType(attribute.type), enum: attribute.enum };
} else {
const newGetter = getter.slice();
newGetter.splice(newGetter.length - 1, 1, 'associations');
const relationNature = _.get(strapi, newGetter).filter(
association => association.alias === curr
)[0].nature;
switch (relationNature) {
case 'manyToMany':
case 'oneToMany':
case 'manyWay':
case 'manyToManyMorph':
acc.properties[curr] = {
type: 'array',
items: { type: 'string' },
};
break;
default:
acc.properties[curr] = { type: 'string' };
}
}
return acc;
},
{ required: ['id'], properties: { id: { type: 'string' } } }
);
},
/**
2019-07-05 03:05:36 +02:00
* Creates the paths object with all the needed information
* The object has the following structure { apiName: { paths: {} }, knownTag1: { paths: {} }, unclassified: { paths: {} } }
* Each key will create a documentation.json file
*
* @param {String} apiName
* @param {Array} routes
* @returns {Object}
*/
generateApiDocumentation(apiName, routes) {
return routes.reduce((acc, current) => {
const [controllerName, controllerMethod] = current.handler.split('.');
// Retrieve the tag key in the config object
const routeTagConfig = _.get(current, ['config', 'tag']);
// Add curly braces between dynamic params
const endPoint = this.formatApiEndPoint(current.path);
let verb;
if (Array.isArray(current.method)) {
verb = current.method.map(method => method.toLowerCase());
} else {
verb = current.method.toLowerCase();
}
// The key corresponds to firsts keys of the returned object
let key;
let tags;
if (controllerName.toLowerCase() === apiName && !_.isObject(routeTagConfig)) {
key = apiName;
} else if (routeTagConfig !== undefined) {
if (_.isObject(routeTagConfig)) {
const { name, plugin } = routeTagConfig;
2018-12-13 16:29:57 +01:00
const referencePlugin = !_.isEmpty(plugin);
key = referencePlugin ? `${plugin}-${name}` : name.toLowerCase();
tags = referencePlugin ? this.formatTag(plugin, name) : _.upperFirst(name);
} else {
key = routeTagConfig.toLowerCase();
}
} else {
key = 'unclassified';
}
const verbObject = {
deprecated: false,
description: this.generateVerbDescription(
verb,
current.handler,
key,
endPoint.split('/')[1],
_.get(current, 'config.description')
),
responses: this.generateResponses(verb, current, key),
summary: '',
tags: _.isEmpty(tags) ? [_.upperFirst(key)] : [_.upperFirst(tags)],
};
// Swagger is not support key with ',' symbol, for array of methods need generate documentation for each method
if (Array.isArray(verb)) {
verb.forEach(method => {
_.set(acc, [key, 'paths', endPoint, method], verbObject);
});
} else {
_.set(acc, [key, 'paths', endPoint, verb], verbObject);
}
if (verb.includes('post') || verb.includes('put')) {
let requestBody;
if (controllerMethod === 'create' || controllerMethod === 'update') {
requestBody = {
description: '',
required: true,
content: {
'application/json': {
schema: {
$ref: `#/components/schemas/New${_.upperFirst(key)}`,
},
},
},
};
} else {
requestBody = {
description: '',
required: true,
content: {
'application/json': {
schema: {
properties: {
foo: {
type: 'string',
},
},
},
},
},
};
}
if (Array.isArray(verb)) {
verb.forEach(method => {
_.set(acc, [key, 'paths', endPoint, method, 'requestBody'], requestBody);
});
} else {
_.set(acc, [key, 'paths', endPoint, verb, 'requestBody'], requestBody);
}
}
// Refer to https://swagger.io/specification/#pathItemObject
const parameters = this.generateVerbParameters(verb, controllerMethod, current.path);
if (!verb.includes('post')) {
if (Array.isArray(verb)) {
verb.forEach(method => {
_.set(acc, [key, 'paths', endPoint, method, 'parameters'], parameters);
});
} else {
_.set(acc, [key, 'paths', endPoint, verb, 'parameters'], parameters);
}
}
return acc;
}, {});
},
generateFullDoc(version = this.getDocumentationVersion()) {
const apisDoc = this.retrieveDocumentationFiles(false, version);
const pluginsDoc = this.retrieveDocumentationFiles(true, version);
const appDoc = [...apisDoc, ...pluginsDoc];
const defaultSettings = _.cloneDeep(
_.pick(strapi.plugins.documentation.config, defaultSettingsKeys)
);
_.set(defaultSettings, ['info', 'x-generation-date'], moment().format('L LTS'));
_.set(defaultSettings, ['info', 'version'], version);
const tags = appDoc.reduce((acc, current) => {
const tags = current.tags.filter(el => {
return _.findIndex(acc, ['name', el.name || '']) === -1;
});
return acc.concat(tags);
}, []);
const fullDoc = _.merge(
appDoc.reduce((acc, current) => {
return _.merge(acc, current);
}, defaultSettings),
defaultComponents
// { tags },
);
fullDoc.tags = tags;
return fullDoc;
},
/**
* Generate the main component that has refs to sub components
* @param {Object} attributes
* @param {Array} associations
* @returns {Object}
*/
generateMainComponent(attributes, associations) {
return Object.keys(attributes).reduce(
(acc, current) => {
const attribute = attributes[current];
// Refer to https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#dataTypes
const type = this.getType(attribute.type);
const {
description,
default: defaultValue,
minimum,
maxmimun,
maxLength,
minLength,
enum: enumeration,
} = attribute;
if (attribute.required === true) {
acc.required.push(current);
}
if (attribute.model || attribute.collection) {
const currentAssociation = associations.filter(
association => association.alias === current
)[0];
const relationNature = currentAssociation.nature;
const name = currentAssociation.model || currentAssociation.collection;
const getter =
currentAssociation.plugin !== undefined
? currentAssociation.plugin === 'admin'
? ['admin', 'models', name, 'attributes']
: ['plugins', currentAssociation.plugin, 'models', name, 'attributes']
2019-02-28 21:41:46 +02:00
: ['models', name.toLowerCase(), 'attributes'];
const associationAttributes = _.get(strapi, getter);
const associationSchema = this.generateAssociationSchema(associationAttributes, getter);
switch (relationNature) {
case 'manyToMany':
case 'oneToMany':
case 'manyWay':
case 'manyToManyMorph':
acc.properties[current] = {
type: 'array',
items: associationSchema,
};
break;
default:
acc.properties[current] = associationSchema;
}
2019-10-22 18:01:03 +02:00
} else if (type === 'component') {
const { repeatable, component, min, max } = attribute;
2019-09-10 16:52:51 +02:00
const cmp = this.generateMainComponent(
2019-10-22 18:01:03 +02:00
strapi.components[component].attributes,
strapi.components[component].associations
2019-09-10 16:52:51 +02:00
);
if (repeatable) {
acc.properties[current] = {
type: 'array',
items: {
type: 'object',
...cmp,
},
minItems: min,
maxItems: max,
};
} else {
acc.properties[current] = {
type: 'object',
...cmp,
description,
};
}
} else if (type === 'dynamiczone') {
const { components, min, max } = attribute;
const cmps = components.map(component => {
const schema = this.generateMainComponent(
strapi.components[component].attributes,
strapi.components[component].associations
);
return _.merge(
{
properties: {
__component: {
type: 'string',
enum: components,
},
},
},
schema
);
});
acc.properties[current] = {
type: 'array',
items: {
oneOf: cmps,
},
minItems: min,
maxItems: max,
};
} else {
acc.properties[current] = {
type,
format: this.getFormat(attribute.type),
description,
default: defaultValue,
minimum,
maxmimun,
maxLength,
minLength,
enum: enumeration,
};
}
return acc;
},
{ required: ['id'], properties: { id: { type: 'string' } } }
);
},
generatePluginDocumentation(pluginName, routes) {
return routes.reduce((acc, current) => {
const {
config: { description, prefix },
} = current;
const endPoint =
prefix === undefined
? this.formatApiEndPoint(`/${pluginName}${current.path}`)
: this.formatApiEndPoint(`${prefix}${current.path}`);
let verb;
if (Array.isArray(current.method)) {
verb = current.method.map(method => method.toLowerCase());
} else {
verb = current.method.toLowerCase();
}
const actionType = _.get(current, ['config', 'tag', 'actionType'], '');
let key;
let tags;
if (_.isObject(current.config.tag)) {
const { name, plugin } = current.config.tag;
key = plugin ? `${plugin}-${name}` : name;
tags = plugin ? [this.formatTag(plugin, name)] : [name];
} else {
const tag = current.config.tag;
key = !_.isEmpty(tag) ? tag : 'unclassified';
tags = !_.isEmpty(tag) ? [tag] : ['Unclassified'];
}
const hasDefaultDocumentation = this.checkIfPluginDefaultDocumentFileExists(pluginName, key);
const defaultDocumentation = hasDefaultDocumentation
? this.getPluginDefaultVerbDocumentation(pluginName, key, endPoint, verb)
: null;
const verbObject = {
deprecated: false,
description,
responses: this.generatePluginVerbResponses(current),
summary: '',
tags,
};
_.set(acc, [key, 'paths', endPoint, verb], verbObject);
const parameters = this.generateVerbParameters(
verb,
actionType,
`/${pluginName}${current.path}`
);
if (_.isEmpty(defaultDocumentation)) {
if (!verb.includes('post')) {
if (Array.isArray(verb)) {
verb.forEach(method => {
_.set(acc, [key, 'paths', endPoint, method, 'parameters'], parameters);
});
} else {
_.set(acc, [key, 'paths', endPoint, verb, 'parameters'], parameters);
}
}
if (verb.includes('post') || verb.includes('put')) {
let requestBody;
if (actionType === 'create' || actionType === 'update') {
const { name, plugin } = _.isObject(current.config.tag)
? current.config.tag
: { tag: current.config.tag };
const $ref = plugin
? `#/components/schemas/New${this.formatTag(plugin, name, true)}`
: `#/components/schemas/New${_.upperFirst(name)}`;
requestBody = {
description: '',
required: true,
content: {
'application/json': {
schema: {
$ref,
},
},
},
};
} else {
requestBody = {
description: '',
required: true,
content: {
'application/json': {
schema: {
properties: {
foo: {
type: 'string',
},
},
},
},
},
};
}
if (Array.isArray(verb)) {
verb.forEach(method => {
_.set(acc, [key, 'paths', endPoint, method, 'requestBody'], requestBody);
});
} else {
_.set(acc, [key, 'paths', endPoint, verb, 'requestBody'], requestBody);
}
}
}
return acc;
}, {});
},
generatePluginResponseSchema(tag) {
const { actionType, name, plugin } = _.isObject(tag) ? tag : { tag };
const getter = plugin ? ['plugins', plugin, 'models', name.toLowerCase()] : ['models', name];
const isModelRelated =
_.get(strapi, getter) !== undefined &&
['find', 'findOne', 'create', 'search', 'update', 'destroy', 'count'].includes(actionType);
const $ref = plugin
? `#/components/schemas/${this.formatTag(plugin, name, true)}`
: `#/components/schemas/${_.upperFirst(name)}`;
if (isModelRelated) {
switch (actionType) {
case 'find':
return {
type: 'array',
items: {
$ref,
},
};
case 'count':
return {
properties: {
count: {
type: 'integer',
},
},
};
case 'findOne':
case 'update':
case 'create':
return {
$ref,
};
default:
return {
properties: {
foo: {
type: 'string',
},
},
};
}
}
return {
properties: {
foo: {
type: 'string',
},
},
};
},
generatePluginVerbResponses(routeObject) {
const {
config: { tag },
} = routeObject;
const actionType = _.get(tag, 'actionType');
let schema;
if (!tag || !actionType) {
schema = {
properties: {
foo: {
type: 'string',
},
},
};
} else {
schema = this.generatePluginResponseSchema(tag);
}
Make plugins documentation generation optional (#5465) * Make plugins documentation generation optional Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Removing "-" character from schema names for generated plugin documentation so it becomes compatible with AWS API gateway import API feature using OAS file. Adding only one property to the settings file pluginsForWhichToGenerateDoc so that the user can choose for which plugin he wishes to generate documentation. Adding a parameter to generate or not the default response (this can now be set to false to have a documentation that is compatible with AWS API Gateway). Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Updating settings.json to start without the pluginsForWhichToGenerateDoc key so that all plugins documentation gets generated by default. Updated the documentation to reflect this change. Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Updating documentation to correct typos and rephrase some sentences to make them more clear. Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Destructuring the config to the var pluginsForWhichToGenerateDoc instead of renaming it to take into account comment from Alexandre Bodin Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Destructuring generateDefaultResponse and pluginsForWhichToGenerateDoc from strapi config object based on feedback from Alexandre Bodin Signed-off-by: Ralph Maroun <rmaroun@outlook.com> Co-authored-by: Alexandre BODIN <alexandrebodin@users.noreply.github.com>
2020-03-20 14:03:51 +01:00
const response = {
200: {
description: 'response',
content: {
'application/json': {
schema,
},
},
},
403: {
description: 'Forbidden',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Error',
},
},
},
},
404: {
description: 'Not found',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Error',
},
},
},
},
};
Make plugins documentation generation optional (#5465) * Make plugins documentation generation optional Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Removing "-" character from schema names for generated plugin documentation so it becomes compatible with AWS API gateway import API feature using OAS file. Adding only one property to the settings file pluginsForWhichToGenerateDoc so that the user can choose for which plugin he wishes to generate documentation. Adding a parameter to generate or not the default response (this can now be set to false to have a documentation that is compatible with AWS API Gateway). Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Updating settings.json to start without the pluginsForWhichToGenerateDoc key so that all plugins documentation gets generated by default. Updated the documentation to reflect this change. Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Updating documentation to correct typos and rephrase some sentences to make them more clear. Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Destructuring the config to the var pluginsForWhichToGenerateDoc instead of renaming it to take into account comment from Alexandre Bodin Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Destructuring generateDefaultResponse and pluginsForWhichToGenerateDoc from strapi config object based on feedback from Alexandre Bodin Signed-off-by: Ralph Maroun <rmaroun@outlook.com> Co-authored-by: Alexandre BODIN <alexandrebodin@users.noreply.github.com>
2020-03-20 14:03:51 +01:00
2021-08-06 18:09:49 +02:00
const { generateDefaultResponse } = strapi.config.get('plugin.documentation.x-strapi-config');
Make plugins documentation generation optional (#5465) * Make plugins documentation generation optional Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Removing "-" character from schema names for generated plugin documentation so it becomes compatible with AWS API gateway import API feature using OAS file. Adding only one property to the settings file pluginsForWhichToGenerateDoc so that the user can choose for which plugin he wishes to generate documentation. Adding a parameter to generate or not the default response (this can now be set to false to have a documentation that is compatible with AWS API Gateway). Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Updating settings.json to start without the pluginsForWhichToGenerateDoc key so that all plugins documentation gets generated by default. Updated the documentation to reflect this change. Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Updating documentation to correct typos and rephrase some sentences to make them more clear. Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Destructuring the config to the var pluginsForWhichToGenerateDoc instead of renaming it to take into account comment from Alexandre Bodin Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Destructuring generateDefaultResponse and pluginsForWhichToGenerateDoc from strapi config object based on feedback from Alexandre Bodin Signed-off-by: Ralph Maroun <rmaroun@outlook.com> Co-authored-by: Alexandre BODIN <alexandrebodin@users.noreply.github.com>
2020-03-20 14:03:51 +01:00
if (generateDefaultResponse) {
response.default = {
description: 'unexpected error',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Error',
},
},
},
};
Make plugins documentation generation optional (#5465) * Make plugins documentation generation optional Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Removing "-" character from schema names for generated plugin documentation so it becomes compatible with AWS API gateway import API feature using OAS file. Adding only one property to the settings file pluginsForWhichToGenerateDoc so that the user can choose for which plugin he wishes to generate documentation. Adding a parameter to generate or not the default response (this can now be set to false to have a documentation that is compatible with AWS API Gateway). Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Updating settings.json to start without the pluginsForWhichToGenerateDoc key so that all plugins documentation gets generated by default. Updated the documentation to reflect this change. Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Updating documentation to correct typos and rephrase some sentences to make them more clear. Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Destructuring the config to the var pluginsForWhichToGenerateDoc instead of renaming it to take into account comment from Alexandre Bodin Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Destructuring generateDefaultResponse and pluginsForWhichToGenerateDoc from strapi config object based on feedback from Alexandre Bodin Signed-off-by: Ralph Maroun <rmaroun@outlook.com> Co-authored-by: Alexandre BODIN <alexandrebodin@users.noreply.github.com>
2020-03-20 14:03:51 +01:00
}
return response;
},
/**
* Create the response object https://swagger.io/specification/#responsesObject
* @param {String} verb
* @param {Object} routeObject
* @param {String} tag
* @returns {Object}
*/
generateResponses(verb, routeObject, tag) {
const endPoint = routeObject.path.split('/')[1];
const description = this.generateResponseDescription(verb, tag, endPoint);
const schema = this.generateResponseSchema(verb, routeObject, tag, endPoint);
Make plugins documentation generation optional (#5465) * Make plugins documentation generation optional Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Removing "-" character from schema names for generated plugin documentation so it becomes compatible with AWS API gateway import API feature using OAS file. Adding only one property to the settings file pluginsForWhichToGenerateDoc so that the user can choose for which plugin he wishes to generate documentation. Adding a parameter to generate or not the default response (this can now be set to false to have a documentation that is compatible with AWS API Gateway). Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Updating settings.json to start without the pluginsForWhichToGenerateDoc key so that all plugins documentation gets generated by default. Updated the documentation to reflect this change. Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Updating documentation to correct typos and rephrase some sentences to make them more clear. Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Destructuring the config to the var pluginsForWhichToGenerateDoc instead of renaming it to take into account comment from Alexandre Bodin Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Destructuring generateDefaultResponse and pluginsForWhichToGenerateDoc from strapi config object based on feedback from Alexandre Bodin Signed-off-by: Ralph Maroun <rmaroun@outlook.com> Co-authored-by: Alexandre BODIN <alexandrebodin@users.noreply.github.com>
2020-03-20 14:03:51 +01:00
const response = {
200: {
description,
content: {
'application/json': {
schema,
},
},
},
403: {
description: 'Forbidden',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Error',
},
},
},
},
404: {
description: 'Not found',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Error',
},
},
},
},
};
Make plugins documentation generation optional (#5465) * Make plugins documentation generation optional Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Removing "-" character from schema names for generated plugin documentation so it becomes compatible with AWS API gateway import API feature using OAS file. Adding only one property to the settings file pluginsForWhichToGenerateDoc so that the user can choose for which plugin he wishes to generate documentation. Adding a parameter to generate or not the default response (this can now be set to false to have a documentation that is compatible with AWS API Gateway). Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Updating settings.json to start without the pluginsForWhichToGenerateDoc key so that all plugins documentation gets generated by default. Updated the documentation to reflect this change. Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Updating documentation to correct typos and rephrase some sentences to make them more clear. Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Destructuring the config to the var pluginsForWhichToGenerateDoc instead of renaming it to take into account comment from Alexandre Bodin Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Destructuring generateDefaultResponse and pluginsForWhichToGenerateDoc from strapi config object based on feedback from Alexandre Bodin Signed-off-by: Ralph Maroun <rmaroun@outlook.com> Co-authored-by: Alexandre BODIN <alexandrebodin@users.noreply.github.com>
2020-03-20 14:03:51 +01:00
2021-08-06 18:09:49 +02:00
const { generateDefaultResponse } = strapi.config.get('plugin.documentation.x-strapi-config');
Make plugins documentation generation optional (#5465) * Make plugins documentation generation optional Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Removing "-" character from schema names for generated plugin documentation so it becomes compatible with AWS API gateway import API feature using OAS file. Adding only one property to the settings file pluginsForWhichToGenerateDoc so that the user can choose for which plugin he wishes to generate documentation. Adding a parameter to generate or not the default response (this can now be set to false to have a documentation that is compatible with AWS API Gateway). Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Updating settings.json to start without the pluginsForWhichToGenerateDoc key so that all plugins documentation gets generated by default. Updated the documentation to reflect this change. Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Updating documentation to correct typos and rephrase some sentences to make them more clear. Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Destructuring the config to the var pluginsForWhichToGenerateDoc instead of renaming it to take into account comment from Alexandre Bodin Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Destructuring generateDefaultResponse and pluginsForWhichToGenerateDoc from strapi config object based on feedback from Alexandre Bodin Signed-off-by: Ralph Maroun <rmaroun@outlook.com> Co-authored-by: Alexandre BODIN <alexandrebodin@users.noreply.github.com>
2020-03-20 14:03:51 +01:00
if (generateDefaultResponse) {
response.default = {
description: 'unexpected error',
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/Error',
},
},
},
};
Make plugins documentation generation optional (#5465) * Make plugins documentation generation optional Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Removing "-" character from schema names for generated plugin documentation so it becomes compatible with AWS API gateway import API feature using OAS file. Adding only one property to the settings file pluginsForWhichToGenerateDoc so that the user can choose for which plugin he wishes to generate documentation. Adding a parameter to generate or not the default response (this can now be set to false to have a documentation that is compatible with AWS API Gateway). Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Updating settings.json to start without the pluginsForWhichToGenerateDoc key so that all plugins documentation gets generated by default. Updated the documentation to reflect this change. Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Updating documentation to correct typos and rephrase some sentences to make them more clear. Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Destructuring the config to the var pluginsForWhichToGenerateDoc instead of renaming it to take into account comment from Alexandre Bodin Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Destructuring generateDefaultResponse and pluginsForWhichToGenerateDoc from strapi config object based on feedback from Alexandre Bodin Signed-off-by: Ralph Maroun <rmaroun@outlook.com> Co-authored-by: Alexandre BODIN <alexandrebodin@users.noreply.github.com>
2020-03-20 14:03:51 +01:00
}
return response;
},
/**
* Retrieve all privates attributes from a model
* @param {Object} attributes
*/
getPrivateAttributes(attributes) {
const privateAttributes = Object.keys(attributes).reduce((acc, current) => {
if (attributes[current].private === true) {
acc.push(current);
}
return acc;
}, []);
return privateAttributes;
},
/**
* Create a component object with the model's attributes and relations
* Refer to https://swagger.io/docs/specification/components/
* @param {String} tag
* @returns {Object}
*/
generateResponseComponent(tag, pluginName = '', isPlugin = false) {
// The component's name have to be capitalised
const [plugin, name] = isPlugin ? this.getModelAndNameForPlugin(tag, pluginName) : [null, null];
const upperFirstTag = isPlugin ? this.formatTag(plugin, name, true) : _.upperFirst(tag);
const attributesGetter = isPlugin
? [...this.getModelForPlugin(tag, plugin), 'attributes']
: ['models', tag, 'attributes'];
const associationGetter = isPlugin
? [...this.getModelForPlugin(tag, plugin), 'associations']
: ['models', tag, 'associations'];
const attributesObject = _.get(strapi, attributesGetter);
const privateAttributes = this.getPrivateAttributes(attributesObject);
const modelAssociations = _.get(strapi, associationGetter);
const { attributes } = this.getModelAttributes(attributesObject);
const associationsWithUpload = modelAssociations
.filter(association => {
return association.plugin === 'upload';
})
.map(obj => obj.alias);
// We always create two nested components from the main one
const mainComponent = this.generateMainComponent(attributes, modelAssociations, upperFirstTag);
// Get Component that doesn't display the privates attributes since a mask is applied
// Please refer https://github.com/strapi/strapi/blob/585800b7b98093f596759b296a43f89c491d4f4f/packages/strapi/lib/middlewares/mask/index.js#L92-L100
const getComponent = Object.keys(mainComponent.properties).reduce(
(acc, current) => {
if (privateAttributes.indexOf(current) === -1) {
acc.properties[current] = mainComponent.properties[current];
}
return acc;
},
{ required: mainComponent.required, properties: {} }
);
// Special component only for POST || PUT verbs since the upload is made with a different route
const postComponent = Object.keys(mainComponent).reduce((acc, current) => {
if (current === 'required') {
const required = mainComponent.required.slice().filter(attr => {
return associationsWithUpload.indexOf(attr) === -1 && attr !== 'id' && attr !== '_id';
});
if (required.length > 0) {
acc.required = required;
}
}
if (current === 'properties') {
const properties = Object.keys(mainComponent.properties).reduce((acc, current) => {
if (
associationsWithUpload.indexOf(current) === -1 &&
current !== 'id' &&
current !== '_id'
) {
// The post request shouldn't include nested relations of type 2
// For instance if a product has many tags
// we expect to find an array of tags objects containing other relations in the get response
// and since we use to getComponent to generate this one we need to
// remove this object since we only send an array of tag ids.
if (_.find(modelAssociations, ['alias', current])) {
const isArrayProperty =
_.get(mainComponent, ['properties', current, 'type']) !== undefined;
if (isArrayProperty) {
acc[current] = { type: 'array', items: { type: 'string' } };
2018-12-07 00:14:59 +01:00
} else {
acc[current] = { type: 'string' };
2018-12-07 00:14:59 +01:00
}
} else {
// If the field is not an association we take the one from the component
acc[current] = mainComponent.properties[current];
2018-12-07 00:14:59 +01:00
}
}
return acc;
}, {});
acc.properties = properties;
}
return acc;
}, {});
return {
components: {
schemas: {
[upperFirstTag]: getComponent,
[`New${upperFirstTag}`]: postComponent,
},
},
};
},
/**
* Generate a better description for a response when we can guess what's the user is going to retrieve
* @param {String} verb
* @param {String} tag
* @param {String} endPoint
* @returns {String}
*/
generateResponseDescription(verb, tag, endPoint) {
const isModelRelated = strapi.models[tag] !== undefined && tag === endPoint;
if (Array.isArray(verb)) {
verb = verb.map(method => method.toLocaleLowerCase());
}
if (verb.includes('get') || verb.includes('put') || verb.includes('post')) {
2019-03-05 21:12:35 +02:00
return isModelRelated ? `Retrieve ${tag} document(s)` : 'response';
} else if (verb.includes('delete')) {
return isModelRelated
? `deletes a single ${tag} based on the ID supplied`
: 'deletes a single record based on the ID supplied';
} else {
return 'response';
}
},
/**
* For each response generate its schema
* Its schema is either a component when we know what the routes returns otherwise, it returns a dummy schema
* that the user will modify later
* @param {String} verb
2018-12-13 16:29:57 +01:00
* @param {Object} route
* @param {String} tag
* @param {String} endPoint
* @returns {Object}
*/
generateResponseSchema(verb, routeObject, tag) {
2018-12-13 16:29:57 +01:00
const { handler } = routeObject;
let [controller, handlerMethod] = handler.split('.');
let upperFirstTag = _.upperFirst(tag);
if (verb === 'delete') {
return {
type: 'integer',
format: 'int64',
};
}
2018-12-13 16:29:57 +01:00
// A tag key might be added to a route to tell if a custom endPoint in an api/<model>/config/routes.json
// Retrieves data from another model it is a faster way to generate the response
const routeReferenceTag = _.get(routeObject, ['config', 'tag']);
let isModelRelated = false;
const shouldCheckIfACustomEndPointReferencesAnotherModel =
_.isObject(routeReferenceTag) && !_.isEmpty(_.get(routeReferenceTag, 'name'));
2018-12-13 16:29:57 +01:00
if (shouldCheckIfACustomEndPointReferencesAnotherModel) {
const { actionType, name, plugin } = routeReferenceTag;
// A model could be in either a plugin or the api folder
// The path is different depending on the case
const getter = !_.isEmpty(plugin)
? ['plugins', plugin, 'models', name.toLowerCase()]
: ['models', name.toLowerCase()];
// An actionType key might be added to the tag object to guide the algorithm is generating an automatic response
const isKnownAction = [
'find',
'findOne',
'create',
'search',
'update',
'destroy',
'count',
].includes(actionType);
// Check if a route points to a model
isModelRelated = _.get(strapi, getter) !== undefined && isKnownAction;
if (isModelRelated && isKnownAction) {
// We need to change the handlerMethod name if it is know to generate the good schema
handlerMethod = actionType;
// This is to retrieve the correct component if a custom endpoints references a plugin model
if (!_.isEmpty(plugin)) {
upperFirstTag = this.formatTag(plugin, name, true);
}
}
} else {
// Normal way there's no tag object
isModelRelated = strapi.models[tag] !== undefined && tag === _.lowerCase(controller);
2018-12-13 16:29:57 +01:00
}
// We create a component when we are sure that we can 'guess' what's needed to be sent
// https://swagger.io/specification/#referenceObject
if (isModelRelated) {
switch (handlerMethod) {
case 'find':
return {
type: 'array',
items: {
$ref: `#/components/schemas/${upperFirstTag}`,
},
};
case 'count':
return {
properties: {
count: {
type: 'integer',
},
},
};
case 'findOne':
case 'update':
case 'create':
return {
$ref: `#/components/schemas/${upperFirstTag}`,
};
default:
return {
properties: {
foo: {
type: 'string',
},
},
};
}
}
return {
properties: {
foo: {
type: 'string',
},
},
};
},
generateTags(name, docName, tag = '', isPlugin = false) {
return [
{
name: isPlugin ? tag : _.upperFirst(docName),
},
];
},
/**
* Add a default description when it's implied
*
* @param {String} verb
* @param {String} handler
* @param {String} tag
* @param {String} endPoint
* @returns {String}
*/
generateVerbDescription(verb, handler, tag, endPoint, description) {
const isModelRelated = strapi.models[tag] !== undefined && tag === endPoint;
if (description) {
return description;
}
if (Array.isArray(verb)) {
const [, controllerMethod] = handler.split('.');
if ((verb.includes('get') && verb.includes('post')) || controllerMethod === 'findOrCreate') {
return `Find or create ${tag} record`;
}
if (
(verb.includes('put') && verb.includes('post')) ||
controllerMethod === 'createOrUpdate'
) {
return `Create or update ${tag} record`;
}
return '';
}
switch (verb) {
case 'get': {
const [, controllerMethod] = handler.split('.');
if (isModelRelated) {
switch (controllerMethod) {
case 'count':
return `Retrieve the number of ${tag} documents`;
case 'findOne':
return `Find one ${tag} record`;
case 'find':
return `Find all the ${tag}'s records`;
default:
return '';
}
}
return '';
}
case 'delete':
return isModelRelated ? `Delete a single ${tag} record` : 'Delete a record';
case 'post':
return isModelRelated ? `Create a new ${tag} record` : 'Create a new record';
case 'put':
return isModelRelated ? `Update a single ${tag} record` : 'Update a record';
case 'patch':
return '';
case 'head':
return '';
default:
return '';
}
},
/**
* Generate the verb parameters object
* Refer to https://swagger.io/specification/#pathItemObject
* @param {Sting} verb
* @param {String} controllerMethod
* @param {String} endPoint
*/
generateVerbParameters(verb, controllerMethod, endPoint) {
const params = pathToRegexp
.parse(endPoint)
.filter(token => _.isObject(token))
.reduce((acc, current) => {
const param = {
name: current.name,
in: 'path',
description: '',
deprecated: false,
required: true,
schema: { type: 'string' },
};
return acc.concat(param);
}, []);
if (verb === 'get' && controllerMethod === 'find') {
// parametersOptions corresponds to this section
// of the documentation https://strapi.io/documentation/developer-docs/latest/developer-resources/content-api/content-api.html#filters
return [...params, ...parametersOptions];
}
return params;
},
/**
* Retrieve the apis in /api
* @returns {Array}
*/
getApis() {
return Object.keys(strapi.api || {});
},
getAPIOverrideComponentsDocumentation(apiName, docName) {
try {
const documentation = JSON.parse(
fs.readFileSync(this.getAPIOverrideDocumentationPath(apiName, docName), 'utf8')
);
return _.get(documentation, 'components', null);
} catch (err) {
return null;
}
},
getAPIDefaultTagsDocumentation(name, docName) {
try {
const documentation = JSON.parse(
fs.readFileSync(this.getAPIOverrideDocumentationPath(name, docName), 'utf8')
);
return _.get(documentation, 'tags', null);
} catch (err) {
return null;
}
},
getAPIDefaultVerbDocumentation(apiName, docName, routePath, verb) {
try {
const documentation = JSON.parse(
fs.readFileSync(this.getAPIOverrideDocumentationPath(apiName, docName), 'utf8')
);
return _.get(documentation, ['paths', routePath, verb], null);
} catch (err) {
return null;
}
},
getAPIOverrideDocumentationPath(apiName, docName) {
return path.join(
strapi.config.appPath,
'api',
apiName,
'documentation',
'overrides',
this.getDocumentationVersion(),
`${docName}.json`
);
},
/**
* Given an api retrieve its endpoints
* @param {String}
* @returns {Array}
*/
getApiRoutes(apiName) {
return _.get(strapi, ['api', apiName, 'config', 'routes'], []);
},
getDocumentationOverridesPath(apiName) {
return path.join(
strapi.config.appPath,
'api',
apiName,
'documentation',
this.getDocumentationVersion(),
'overrides'
);
},
/**
* Given an api from /api retrieve its version directory
* @param {String} apiName
* @returns {Path}
*/
getDocumentationPath(apiName) {
return path.join(
strapi.config.appPath,
'api',
apiName,
'documentation',
this.getDocumentationVersion()
);
},
getFullDocumentationPath() {
return path.join(strapi.config.appPath, 'extensions', 'documentation', 'documentation');
},
/**
* Retrieve the plugin's configuration version
*/
getDocumentationVersion() {
2021-08-06 18:09:49 +02:00
const version = strapi.config.get('plugin.documentation.info.version');
return version;
},
/**
* Retrieve the documentation plugin documentation directory
*/
getMergedDocumentationPath(version = this.getDocumentationVersion()) {
return path.join(
strapi.config.appPath,
'extensions',
'documentation',
'documentation',
version
);
},
/**
* Retrieve the model's attributes
* @param {Objet} modelAttributes
* @returns {Object} { associations: [{ name: 'foo', getter: [], tag: 'foos' }], attributes }
*/
getModelAttributes(modelAttributes) {
const associations = [];
const attributes = Object.keys(modelAttributes)
.map(attr => {
const attribute = modelAttributes[attr];
const isField = !_.has(attribute, 'model') && !_.has(attribute, 'collection');
if (!isField) {
const name = attribute.model || attribute.collection;
const getter =
attribute.plugin !== undefined
? ['plugins', attribute.plugin, 'models', name, 'attributes']
: ['models', name, 'attributes'];
associations.push({ name, getter, tag: attr });
}
return attr;
})
.reduce((acc, current) => {
acc[current] = modelAttributes[current];
return acc;
}, {});
return { associations, attributes };
},
/**
* Refer to https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#dataTypes
* @param {String} type
* @returns {String}
*/
getType(type) {
switch (type) {
case 'string':
case 'byte':
case 'binary':
case 'password':
case 'email':
case 'text':
case 'enumeration':
case 'date':
case 'datetime':
case 'time':
2019-09-10 16:27:38 +02:00
case 'richtext':
return 'string';
case 'float':
case 'decimal':
case 'double':
return 'number';
case 'integer':
2019-01-21 05:11:48 -07:00
case 'biginteger':
case 'long':
return 'integer';
2019-09-10 16:52:51 +02:00
case 'json':
return 'object';
default:
return type;
}
},
/**
* Refer to https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#dataTypes
* @param {String} type
* @returns {String}
*/
getFormat(type) {
switch (type) {
case 'date':
return 'date';
case 'datetime':
return 'date-time';
case 'password':
return 'password';
default:
return undefined;
}
},
getPluginDefaultVerbDocumentation(pluginName, docName, routePath, verb) {
try {
const documentation = JSON.parse(
fs.readFileSync(this.getPluginOverrideDocumentationPath(pluginName, docName), 'utf8')
);
return _.get(documentation, ['paths', routePath, verb], null);
} catch (err) {
return null;
}
},
getPluginDefaultTagsDocumentation(pluginName, docName) {
try {
const documentation = JSON.parse(
fs.readFileSync(this.getPluginOverrideDocumentationPath(pluginName, docName), 'utf8')
);
return _.get(documentation, ['tags'], null);
} catch (err) {
return null;
}
},
getPluginOverrideComponents(pluginName, docName) {
try {
const documentation = JSON.parse(
fs.readFileSync(this.getPluginOverrideDocumentationPath(pluginName, docName), 'utf8')
);
return _.get(documentation, 'components', null);
} catch (err) {
return null;
}
},
getPluginOverrideDocumentationPath(pluginName, docName) {
const defaultPath = path.join(
strapi.config.appPath,
'extensions',
pluginName,
'documentation',
this.getDocumentationVersion(),
'overrides'
);
if (docName) {
return path.resolve(defaultPath, `${docName.json}`);
} else {
return defaultPath;
}
},
/**
* Given a plugin retrieve its documentation version
*/
getPluginDocumentationPath(pluginName) {
return path.join(
strapi.config.appPath,
'extensions',
pluginName,
'documentation',
this.getDocumentationVersion()
);
},
/**
* Retrieve all plugins that have a description inside one of its route
* @return {Arrray}
*/
getPluginsWithDocumentationNeeded() {
return Object.keys(strapi.plugins).reduce((acc, current) => {
const isDocumentationNeeded = this.isPluginDocumentationNeeded(current);
if (isDocumentationNeeded) {
return acc.concat(current);
}
return acc;
}, []);
},
/**
* Retrieve all the routes that have a description from a plugin
* @param {String} pluginName
* @returns {Array}
*/
getPluginRoutesWithDescription(pluginName) {
return _.get(strapi, ['plugins', pluginName, 'config', 'routes'], []).filter(
route => _.get(route, ['config', 'description']) !== undefined
);
},
/**
* Given a string and a pluginName retrieve the model and the pluginName
* @param {String} string
* @param {Sting} pluginName
* @returns {Array}
*/
getModelAndNameForPlugin(string, pluginName) {
return _.replace(string, `${pluginName}-`, `${pluginName}.`).split('.');
},
/**
* Retrieve the path needed to get a model from a plugin
* @param (String) string
* @param {String} plugin
* @returns {Array}
*/
getModelForPlugin(string, pluginName) {
const [plugin, model] = this.getModelAndNameForPlugin(string, pluginName);
return ['plugins', plugin, 'models', _.lowerCase(model)];
},
/**
* Check whether or not a plugin needs documentation
* @param {String} pluginName
* @returns {Boolean}
*/
isPluginDocumentationNeeded(pluginName) {
2021-07-20 12:12:30 +02:00
const { pluginsForWhichToGenerateDoc } = strapi.config.get(
'plugins.documentation.x-strapi-config'
);
if (
Array.isArray(pluginsForWhichToGenerateDoc) &&
!pluginsForWhichToGenerateDoc.includes(pluginName)
) {
return false;
Make plugins documentation generation optional (#5465) * Make plugins documentation generation optional Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Removing "-" character from schema names for generated plugin documentation so it becomes compatible with AWS API gateway import API feature using OAS file. Adding only one property to the settings file pluginsForWhichToGenerateDoc so that the user can choose for which plugin he wishes to generate documentation. Adding a parameter to generate or not the default response (this can now be set to false to have a documentation that is compatible with AWS API Gateway). Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Updating settings.json to start without the pluginsForWhichToGenerateDoc key so that all plugins documentation gets generated by default. Updated the documentation to reflect this change. Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Updating documentation to correct typos and rephrase some sentences to make them more clear. Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Destructuring the config to the var pluginsForWhichToGenerateDoc instead of renaming it to take into account comment from Alexandre Bodin Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Destructuring generateDefaultResponse and pluginsForWhichToGenerateDoc from strapi config object based on feedback from Alexandre Bodin Signed-off-by: Ralph Maroun <rmaroun@outlook.com> Co-authored-by: Alexandre BODIN <alexandrebodin@users.noreply.github.com>
2020-03-20 14:03:51 +01:00
} else {
return this.getPluginRoutesWithDescription(pluginName).length > 0;
Make plugins documentation generation optional (#5465) * Make plugins documentation generation optional Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Removing "-" character from schema names for generated plugin documentation so it becomes compatible with AWS API gateway import API feature using OAS file. Adding only one property to the settings file pluginsForWhichToGenerateDoc so that the user can choose for which plugin he wishes to generate documentation. Adding a parameter to generate or not the default response (this can now be set to false to have a documentation that is compatible with AWS API Gateway). Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Updating settings.json to start without the pluginsForWhichToGenerateDoc key so that all plugins documentation gets generated by default. Updated the documentation to reflect this change. Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Updating documentation to correct typos and rephrase some sentences to make them more clear. Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Destructuring the config to the var pluginsForWhichToGenerateDoc instead of renaming it to take into account comment from Alexandre Bodin Signed-off-by: Ralph Maroun <rmaroun@outlook.com> * Destructuring generateDefaultResponse and pluginsForWhichToGenerateDoc from strapi config object based on feedback from Alexandre Bodin Signed-off-by: Ralph Maroun <rmaroun@outlook.com> Co-authored-by: Alexandre BODIN <alexandrebodin@users.noreply.github.com>
2020-03-20 14:03:51 +01:00
}
},
/**
* Merge two components by replacing the default ones by the overides and keeping the others
* @param {Object} initObj
* @param {Object} srcObj
* @returns {Object}
*/
mergeComponents(initObj, srcObj) {
const cleanedObj = Object.keys(_.get(initObj, 'schemas', {})).reduce((acc, current) => {
const targetObj = _.has(_.get(srcObj, ['schemas'], {}), current) ? srcObj : initObj;
_.set(acc, ['schemas', current], _.get(targetObj, ['schemas', current], {}));
return acc;
}, {});
return _.merge(cleanedObj, srcObj);
},
mergePaths(initObj, srcObj) {
return Object.keys(initObj.paths).reduce((acc, current) => {
if (_.has(_.get(srcObj, ['paths'], {}), current)) {
const verbs = Object.keys(initObj.paths[current]).reduce((acc1, curr) => {
const verb = this.mergeVerbObject(
initObj.paths[current][curr],
_.get(srcObj, ['paths', current, curr], {})
);
_.set(acc1, [curr], verb);
return acc1;
}, {});
_.set(acc, ['paths', current], verbs);
} else {
_.set(acc, ['paths', current], _.get(initObj, ['paths', current], {}));
}
return acc;
}, {});
},
mergeTags(initObj, srcObj) {
return _.get(srcObj, 'tags', _.get(initObj, 'tags', []));
},
/**
* Merge two verb objects with a customizer
* @param {Object} initObj
* @param {Object} srcObj
* @returns {Object}
*/
mergeVerbObject(initObj, srcObj) {
return _.mergeWith(initObj, srcObj, (objValue, srcValue) => {
if (_.isPlainObject(objValue)) {
return Object.assign(objValue, srcValue);
}
return srcValue;
});
},
retrieveDocumentation(name, isPlugin = false) {
const documentationPath = isPlugin
? [strapi.config.appPath, 'extensions', name, 'documentation', this.getDocumentationVersion()]
: [strapi.config.appPath, 'api', name, 'documentation', this.getDocumentationVersion()];
try {
const documentationFiles = fs
.readdirSync(path.resolve(documentationPath.join('/')))
.filter(el => el.includes('.json'));
return documentationFiles.reduce((acc, current) => {
try {
const doc = JSON.parse(
fs.readFileSync(path.resolve([...documentationPath, current].join('/')), 'utf8')
);
acc.push(doc);
} catch (err) {
// console.log(path.resolve([...documentationPath, current].join('/')), err);
}
return acc;
}, []);
} catch (err) {
return [];
}
},
/**
* Retrieve all documentation files from either the APIs or the plugins
* @param {Boolean} isPlugin
* @returns {Array}
*/
retrieveDocumentationFiles(isPlugin = false, version = this.getDocumentationVersion()) {
const array = isPlugin ? this.getPluginsWithDocumentationNeeded() : this.getApis();
return array.reduce((acc, current) => {
const documentationPath = isPlugin
? [strapi.config.appPath, 'extensions', current, 'documentation', version]
: [strapi.config.appPath, 'api', current, 'documentation', version];
try {
const documentationFiles = fs
.readdirSync(path.resolve(documentationPath.join('/')))
.filter(el => el.includes('.json'));
documentationFiles.forEach(el => {
try {
let documentation = JSON.parse(
fs.readFileSync(path.resolve([...documentationPath, el].join('/')), 'utf8')
);
2018-12-07 00:14:59 +01:00
/* eslint-disable indent */
const overrideDocumentationPath = isPlugin
? path.resolve(
2018-12-07 00:14:59 +01:00
strapi.config.appPath,
2020-01-09 17:19:48 +01:00
'extensions',
2018-12-07 00:14:59 +01:00
current,
'documentation',
version,
'overrides',
el
2018-12-07 00:14:59 +01:00
)
: path.resolve(
2018-12-07 00:14:59 +01:00
strapi.config.appPath,
'api',
current,
'documentation',
version,
'overrides',
el
2018-12-07 00:14:59 +01:00
);
/* eslint-enable indent */
let overrideDocumentation;
try {
overrideDocumentation = JSON.parse(
fs.readFileSync(overrideDocumentationPath, 'utf8')
);
} catch (err) {
overrideDocumentation = null;
}
if (!_.isEmpty(overrideDocumentation)) {
documentation.paths = this.mergePaths(documentation, overrideDocumentation).paths;
documentation.tags = _.cloneDeep(
this.mergeTags(documentation, overrideDocumentation)
);
const documentationComponents = _.get(documentation, 'components', {});
const overrideComponents = _.get(overrideDocumentation, 'components', {});
const mergedComponents = this.mergeComponents(
documentationComponents,
overrideComponents
);
if (!_.isEmpty(mergedComponents)) {
documentation.components = mergedComponents;
}
}
acc.push(documentation);
} catch (err) {
strapi.log.error(err);
console.log(
`Unable to access the documentation for ${[...documentationPath, el].join('/')}`
);
}
});
} catch (err) {
strapi.log.error(err);
console.log(
`Unable to retrieve documentation for the ${isPlugin ? 'plugin' : 'api'} ${current}`
);
}
return acc;
}, []);
},
retrieveDocumentationVersions() {
return fs
.readdirSync(this.getFullDocumentationPath())
.map(version => {
try {
const doc = JSON.parse(
fs.readFileSync(
path.resolve(this.getFullDocumentationPath(), version, 'full_documentation.json')
)
);
const generatedDate = _.get(doc, ['info', 'x-generation-date'], null);
return { version, generatedDate, url: '' };
} catch (err) {
return null;
}
})
.filter(x => x);
},
async retrieveFrontForm() {
const config = await strapi
.store({ type: 'plugin', name: 'documentation', key: 'config' })
.get();
const forms = JSON.parse(JSON.stringify(form));
_.set(forms, [0, 0, 'value'], config.restrictedAccess);
_.set(forms, [0, 1, 'value'], config.password || '');
return forms;
},
2021-07-20 12:12:30 +02:00
});