migrate backend to v4

This commit is contained in:
Mark Kaylor 2021-09-02 11:25:24 +02:00
parent 41a7f31e3e
commit 3e9731a2af
22 changed files with 671 additions and 1962 deletions

View File

@ -5,7 +5,7 @@ module.exports = {
routes: [
{
method: 'GET',
path: '/',
path: '/restaurants',
handler: 'restaurant.find',
config: {
policies: [],
@ -13,7 +13,7 @@ module.exports = {
},
{
method: 'GET',
path: '/:id',
path: '/restaurants/:id',
handler: 'restaurant.findOne',
config: {
policies: [],
@ -21,7 +21,7 @@ module.exports = {
},
{
method: 'POST',
path: '/',
path: '/restaurants',
handler: 'restaurant.create',
config: {
policies: [],
@ -29,7 +29,7 @@ module.exports = {
},
{
method: 'PUT',
path: '/:id',
path: '/restaurants/:id',
handler: 'restaurant.update',
config: {
policies: [],
@ -37,7 +37,7 @@ module.exports = {
},
{
method: 'DELETE',
path: '/:id',
path: '/restaurants/:id',
handler: 'restaurant.delete',
config: {
policies: [],

View File

@ -19,7 +19,8 @@
"koa-static": "^5.0.0",
"lodash": "4.17.21",
"moment": "^2.29.1",
"path-to-regexp": "^3.1.0",
"path-to-regexp": "6.2.0",
"pluralize": "8.0.0",
"react": "^17.0.2",
"react-copy-to-clipboard": "^5.0.3",
"react-dom": "^17.0.2",

View File

@ -1,10 +1,6 @@
/* eslint-disable no-unreachable */
'use strict';
const fs = require('fs');
const path = require('path');
const _ = require('lodash');
// Add permissions
const RBAC_ACTIONS = [
{
@ -29,11 +25,13 @@ const RBAC_ACTIONS = [
},
];
/**
*
* @param {{strapi: import("@strapi/strapi").Strapi}} args
*/
module.exports = async ({ strapi }) => {
await strapi.admin.services.permission.actionProvider.registerMany(RBAC_ACTIONS);
return;
// Check if the plugin users-permissions is installed because the documentation needs it
if (Object.keys(strapi.plugins).indexOf('users-permissions') === -1) {
throw new Error(
@ -41,7 +39,11 @@ module.exports = async ({ strapi }) => {
);
}
const pluginStore = strapi.store({ type: 'plugin', name: 'documentation' });
const pluginStore = strapi.store({
environment: '',
type: 'plugin',
name: 'documentation',
});
const restrictedAccess = await pluginStore.get({ key: 'config' });
@ -49,91 +51,8 @@ module.exports = async ({ strapi }) => {
pluginStore.set({ key: 'config', value: { restrictedAccess: false } });
}
let shouldUpdateFullDoc = false;
const services = strapi.plugins['documentation'].services.documentation;
// Generate plugins' documentation
const pluginsWithDocumentationNeeded = services.getPluginsWithDocumentationNeeded();
pluginsWithDocumentationNeeded.forEach(plugin => {
const isDocExisting = services.checkIfPluginDocumentationFolderExists(plugin);
if (!isDocExisting) {
services.createDocumentationDirectory(services.getPluginDocumentationPath(plugin));
// create the overrides directory
services.createDocumentationDirectory(services.getPluginOverrideDocumentationPath(plugin));
services.createPluginDocumentationFile(plugin);
shouldUpdateFullDoc = true;
} else {
const needToUpdatePluginDoc = services.checkIfPluginDocNeedsUpdate(plugin);
if (needToUpdatePluginDoc) {
services.createPluginDocumentationFile(plugin);
shouldUpdateFullDoc = true;
}
}
});
// Retrieve all the apis from the apis directory
const apis = services.getApis();
// Generate APIS' documentation
apis.forEach(api => {
const isDocExisting = services.checkIfDocumentationFolderExists(api);
if (!isDocExisting) {
// If the documentation directory doesn't exist create it
services.createDocumentationDirectory(services.getDocumentationPath(api));
// Create the overrides directory
services.createDocumentationDirectory(services.getDocumentationOverridesPath(api));
// Create the documentation files per version
services.createDocumentationFile(api); // Then create the {api}.json documentation file
shouldUpdateFullDoc = true;
} else {
const needToUpdateAPIDoc = services.checkIfAPIDocNeedsUpdate(api);
if (needToUpdateAPIDoc) {
services.createDocumentationFile(api);
shouldUpdateFullDoc = true;
}
}
});
const fullDoc = services.generateFullDoc();
// Verify that the correct documentation folder exists in the documentation plugin
const isMergedDocumentationExists = services.checkIfMergedDocumentationFolderExists();
const documentationPath = services.getMergedDocumentationPath();
if (isMergedDocumentationExists) {
/**
* Retrieve all tags from the documentation and join them
* @param {Object} documentation
* @returns {String}
*/
const getDocTagsToString = documentation => {
return _.get(documentation, 'tags', [])
.map(tag => {
return tag.name.toLowerCase();
})
.sort((a, b) => a - b)
.join('.');
};
const oldDoc = require(path.resolve(documentationPath, 'full_documentation.json'));
const oldDocTags = getDocTagsToString(oldDoc);
const currentDocTags = getDocTagsToString(fullDoc);
// If the tags are different (an api has been deleted) we need to rebuild the documentation
if (oldDocTags !== currentDocTags) {
shouldUpdateFullDoc = true;
}
}
if (!isMergedDocumentationExists || shouldUpdateFullDoc) {
// Create the folder
services.createDocumentationDirectory(documentationPath);
// Write the file
fs.writeFileSync(
path.resolve(documentationPath, 'full_documentation.json'),
JSON.stringify(fullDoc, null, 2),
'utf8'
);
}
await strapi
.plugin('documentation')
.service('documentation')
.generateFullDoc();
};

View File

@ -21,10 +21,11 @@ module.exports = {
path: '/documentation',
showGeneratedFiles: true,
generateDefaultResponse: true,
plugins: ['email', 'upload', 'users-permissions'],
},
servers: [
{
url: 'http://localhost:1337',
url: 'http://localhost:1337/api',
description: 'Development server',
},
{
@ -45,4 +46,13 @@ module.exports = {
bearerAuth: [],
},
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
},
},
};

View File

@ -17,14 +17,14 @@ const koaStatic = require('koa-static');
module.exports = {
async getInfos(ctx) {
try {
const service = strapi.plugins.documentation.services.documentation;
const docVersions = service.retrieveDocumentationVersions();
const form = await service.retrieveFrontForm();
const docService = strapi.plugin('documentation').service('documentation');
const docVersions = docService.getDocumentationVersions();
const form = await docService.getFrontendForm();
ctx.send({
docVersions,
currentVersion: service.getDocumentationVersion(),
prefix: strapi.config.get('plugin.documentation.x-strapi-config').path,
currentVersion: docService.getDocumentationVersion(),
prefix: strapi.plugin('documentation').config('x-strapi-config').path,
form,
});
} catch (err) {
@ -45,6 +45,7 @@ module.exports = {
: strapi.plugins.documentation.config.info.version;
const openAPISpecsPath = path.join(
strapi.config.appPath,
'src',
'extensions',
'documentation',
'documentation',
@ -66,6 +67,7 @@ module.exports = {
try {
const layoutPath = path.resolve(
strapi.config.appPath,
'src',
'extensions',
'documentation',
'public',
@ -80,6 +82,7 @@ module.exports = {
try {
const staticFolder = path.resolve(
strapi.config.appPath,
'src',
'extensions',
'documentation',
'public'
@ -119,6 +122,7 @@ module.exports = {
try {
const layoutPath = path.resolve(
strapi.config.appPath,
'src',
'extensions',
'documentation',
'public',
@ -132,6 +136,7 @@ module.exports = {
try {
const staticFolder = path.resolve(
strapi.config.appPath,
'src',
'extensions',
'documentation',
'public'
@ -176,8 +181,9 @@ module.exports = {
},
async regenerateDoc(ctx) {
const service = strapi.plugins.documentation.services.documentation;
const documentationVersions = service.retrieveDocumentationVersions().map(el => el.version);
const service = strapi.plugin('documentation').service('documentation');
const documentationVersions = service.getDocumentationVersions().map(el => el.version);
const {
request: {
body: { version },
@ -211,6 +217,7 @@ module.exports = {
JSON.stringify(fullDoc, null, 2),
'utf8'
);
ctx.send({ ok: true });
} catch (err) {
ctx.badRequest(null, admin ? 'documentation.error.regenerateDoc' : 'An error occured');
@ -221,8 +228,8 @@ module.exports = {
async deleteDoc(ctx) {
strapi.reload.isWatching = false;
const service = strapi.plugins.documentation.services.documentation;
const documentationVersions = service.retrieveDocumentationVersions().map(el => el.version);
const service = strapi.plugin('documentation').service('documentation');
const documentationVersions = service.getDocumentationVersions().map(el => el.version);
const {
params: { version },

View File

@ -54,6 +54,9 @@ module.exports = {
defer: true,
})(ctx, next);
},
config: {
auth: false,
},
},
]);
},

View File

@ -28,5 +28,5 @@ module.exports = async (ctx, next) => {
}
// Execute the action.
await next();
return next();
};

View File

@ -1,7 +0,0 @@
'use strict';
const index = require('./index-policy');
module.exports = {
index,
};

View File

@ -6,10 +6,11 @@ module.exports = [
path: '/',
handler: 'documentation.index',
config: {
policies: [
'plugin::documentation.index',
{ name: 'admin::hasPermissions', config: { actions: ['plugin::documentation.read'] } },
],
auth: false,
// middlewares: [restrictAccess],
// policies: [
// { name: 'admin::hasPermissions', options: { actions: ['plugin::documentation.read'] } },
// ],
},
},
{
@ -17,10 +18,11 @@ module.exports = [
path: '/v:major(\\d+).:minor(\\d+).:patch(\\d+)',
handler: 'documentation.index',
config: {
policies: [
'plugin::documentation.index',
{ name: 'admin::hasPermissions', config: { actions: ['plugin::documentation.read'] } },
],
auth: false,
// middlewares: [restrictAccess],
// policies: [
// { name: 'admin::hasPermissions', options: { actions: ['plugin::documentation.read'] } },
// ],
},
},
{

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,131 @@
'use strict';
const _ = require('lodash');
const pathToRegexp = require('path-to-regexp');
const queryParams = require('../query-params');
const buildApiRequests = require('./build-api-requests');
const buildApiResponses = require('./build-api-responses');
/**
* @description Parses a route with ':variable'
*
* @param {string} routePath - The route's path property
* @returns '{variable}'
*/
const parsePathWithVariables = routePath => {
return pathToRegexp
.parse(routePath)
.map(token => {
if (_.isObject(token)) {
return token.prefix + '{' + token.name + '}';
}
return token;
})
.join('');
};
/**
* @description Builds the required object for a path parameter
*
* @param {string} routePath - The route's path property
*
* @returns Swagger path params object
*/
const getPathParams = routePath => {
return pathToRegexp
.parse(routePath)
.filter(token => _.isObject(token))
.map(param => {
return {
name: param.name,
in: 'path',
description: '',
deprecated: false,
required: true,
schema: { type: 'string' },
};
});
};
/**
*
* @param {array} routes - The routes for a given api or plugin
* @param {object} attributes - The attributes for a given api or plugin
* @param {string} tag - A descriptor for OpenAPI
*
* @returns object of OpenAPI paths for each route
*/
const getPaths = (routes, attributes, tag) => {
const paths = routes.reduce(
(acc, route) => {
// TODO: Find a more reliable way to determine list of entities vs a single entity
const isListOfEntities = route.handler.split('.').pop() === 'find';
const hasPathParams = route.path.includes('/:');
const methodVerb = route.method.toLowerCase();
const routePath = hasPathParams ? parsePathWithVariables(route.path) : route.path;
const { responses } = buildApiResponses(attributes, route, isListOfEntities);
_.set(acc.paths, `${routePath}.${methodVerb}.responses`, responses);
_.set(acc.paths, `${routePath}.${methodVerb}.tags`, [_.upperFirst(tag)]);
if (isListOfEntities) {
_.set(acc.paths, `${routePath}.${methodVerb}.parameters`, queryParams);
}
if (hasPathParams) {
const pathParams = getPathParams(route.path);
_.set(acc.paths, `${routePath}.${methodVerb}.parameters`, pathParams);
}
if (methodVerb === 'post' || methodVerb === 'put') {
const { requestBody } = buildApiRequests(attributes, route);
_.set(acc.paths, `${routePath}.${methodVerb}.requestBody`, requestBody);
}
return acc;
},
{ paths: {} }
);
return paths;
};
/**
* @description - Builds the Swagger paths object for each api
*
* @param {object} api - Information about the current api
* @property {string} api.name - The name of the api
* @property {string} api.getter - The getter for the api (api | plugin)
* @property {array} api.ctNames - The name of all contentTypes found on the api
*
* @returns
*/
module.exports = api => {
if (!api.ctNames.length && api.getter === 'plugin') {
// Set arbitrary attributes
const attributes = { foo: { type: 'string' } };
const routes = strapi.plugin(api.name).routes['admin'].routes;
return getPaths(routes, attributes, api.name);
}
// An api could have multiple contentTypes
for (const contentTypeName of api.ctNames) {
// Get the attributes found on the api's contentType
const attributes = strapi.contentType(`${api.getter}::${api.name}.${contentTypeName}`)
.attributes;
// Get the routes for the current api
const routes =
api.getter === 'plugin'
? strapi.plugin(api.name).routes['content-api'].routes
: strapi.api[api.name].routes[contentTypeName].routes;
// Parse an identifier for OpenAPI tag if the api name and contentType name don't match
const tag = api.name === contentTypeName ? api.name : `${api.name} - ${contentTypeName}`;
return getPaths(routes, attributes, tag);
}
};

View File

@ -0,0 +1,43 @@
'use strict';
const cleanSchemaAttributes = require('../clean-schema-attributes');
/**
*
* @param {object} attributes - The attributes found on a contentType
* @param {object} route - The current route
*
* @returns The Swagger requestBody
*/
module.exports = (attributes, route) => {
const requiredAttributes = Object.entries(attributes)
.filter(([, val]) => {
return val.required;
})
.map(([attr, val]) => {
return { [attr]: val };
});
const requestAttributes =
route.method === 'POST' && requiredAttributes.length
? Object.assign({}, ...requiredAttributes)
: attributes;
return {
requestBody: {
required: true,
content: {
'application/json': {
schema: {
properties: {
data: {
type: 'object',
properties: cleanSchemaAttributes(requestAttributes),
},
},
},
},
},
},
};
};

View File

@ -0,0 +1,83 @@
'use strict';
const getSchemaData = require('../get-schema-data');
const cleanSchemaAttributes = require('../clean-schema-attributes');
const errorResponse = require('../error-response');
/**
*
* @param {boolean} isSingleEntity - Checks for a single entity
* @returns {object} The correctly formatted meta object
*/
const getMeta = isListOfEntities => {
if (isListOfEntities) {
return {
properties: {
pagination: {
properties: {
page: { type: 'integer' },
pageSize: { type: 'integer', minimum: 25 },
pageCount: { type: 'integer', maximum: 1 },
total: { type: 'integer' },
},
},
},
};
}
return { type: 'object' };
};
/**
* @description - Builds the Swagger response object for a given api
*
* @param {object} attributes - The attributes found on a contentType
* @param {object} route - The current route
* @param {boolean} isListOfEntities - Checks for a list of entitities
*
* @returns The Swagger responses
*/
module.exports = (attributes, route, isListOfEntities = false) => {
let schema;
if (route.method === 'DELETE') {
schema = {
type: 'integer',
format: 'int64',
};
} else {
schema = {
properties: {
data: getSchemaData(isListOfEntities, cleanSchemaAttributes(attributes)),
meta: getMeta(isListOfEntities),
},
};
}
return {
responses: {
'200': {
content: {
'application/json': {
schema,
},
},
},
'403': {
description: 'Forbidden',
content: {
'application/json': {
schema: errorResponse.Error,
},
},
},
'404': {
description: 'Not found',
content: {
'application/json': {
schema: errorResponse.Error,
},
},
},
},
};
};

View File

@ -0,0 +1,11 @@
'use strict';
const buildApiResponses = require('./build-api-responses');
const buildApiRequests = require('./build-api-requests');
const builApiEndpointPath = require('./build-api-endpoint-path');
module.exports = {
buildApiResponses,
buildApiRequests,
builApiEndpointPath,
};

View File

@ -0,0 +1,86 @@
'use strict';
const _ = require('lodash');
const getSchemaData = require('./get-schema-data');
/**
* @description - Converts types found on attributes to OpenAPI specific data types
*
* @param {object} attributes - The attributes found on a contentType
* @returns Attributes using OpenAPI acceptable data types
*/
const cleanSchemaAttributes = attributes => {
const attributesCopy = _.cloneDeep(attributes);
for (const prop in attributesCopy) {
const attribute = attributesCopy[prop];
if (attribute.default) {
delete attributesCopy[prop].default;
}
switch (attribute.type) {
case 'datetime': {
attributesCopy[prop] = { type: 'string' };
break;
}
case 'decimal': {
attributesCopy[prop] = { type: 'number', format: 'float' };
break;
}
case 'integer': {
attributesCopy[prop] = { type: 'integer' };
break;
}
case 'json': {
attributesCopy[prop] = {};
break;
}
case 'uid': {
attributesCopy[prop] = { type: 'string', format: 'uuid' };
break;
}
case 'media': {
const imageAttributes = strapi.plugin('upload').contentType('file').attributes;
const isListOfEntities = attribute.multiple;
attributesCopy[prop] = {
type: 'object',
properties: {
data: getSchemaData(isListOfEntities, cleanSchemaAttributes(imageAttributes)),
},
};
break;
}
case 'component': {
const componentAttributes = strapi.components[attribute.component].attributes;
const isListOfEntities = attribute.repeatable;
attributesCopy[prop] = {
type: 'object',
properties: {
data: getSchemaData(isListOfEntities, cleanSchemaAttributes(componentAttributes)),
},
};
break;
}
case 'relation': {
// TODO: Sanitize relation attributes and list them in the schema
const isListOfEntities = attribute.relation.includes('ToMany');
attributesCopy[prop] = {
type: 'object',
properties: {
data: getSchemaData(isListOfEntities, {}),
},
};
break;
}
}
}
return attributesCopy;
};
module.exports = cleanSchemaAttributes;

View File

@ -0,0 +1,17 @@
{
"Error": {
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
}
}
}
}

View File

@ -0,0 +1,32 @@
'use strict';
/**
* @description Determines the format of the data response
*
* @param {boolean} isListOfEntities - Checks for a multiple entities
* @param {object} attributes - The attributes found on a contentType
* @returns object | array of attributes
*/
module.exports = (isListOfEntities, attributes) => {
if (isListOfEntities) {
return {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'string' },
attributes: { type: 'object', properties: attributes },
},
},
};
}
return {
type: 'object',
properties: {
id: { type: 'string' },
attributes: { type: 'object', properties: attributes },
},
};
};

View File

@ -0,0 +1,82 @@
[
{
"name": "sort",
"in": "query",
"description": "Sort by attributes ascending (asc) or descending (desc)",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "pagination[withCount]",
"in": "query",
"description": "Retun page/pageSize (default: true)",
"deprecated": false,
"required": false,
"schema": {
"type": "boolean"
}
},
{
"name": "pagination[page]",
"in": "query",
"description": "Page number (default: 0)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "pagination[pageSize]",
"in": "query",
"description": "Page size (default: 25)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "pagination[start]",
"in": "query",
"description": "Offset value (default: 0)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "pagination[limit]",
"in": "query",
"description": "Number of entities to return (default: 25)",
"deprecated": false,
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "fields",
"in": "query",
"description": "Fields to return (ex: title,author)",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "populate",
"in": "query",
"description": "Relations to return",
"deprecated": false,
"required": false,
"schema": {
"type": "string"
}
}
]

View File

@ -1,7 +1,6 @@
'use strict';
const bootstrap = require('./server/bootstrap');
const policies = require('./server/policies');
const services = require('./server/services');
const routes = require('./server/routes');
const controllers = require('./server/controllers');
@ -15,7 +14,6 @@ module.exports = () => {
routes,
controllers,
middlewares,
policies,
services,
};
};

View File

@ -18506,6 +18506,11 @@ path-to-regexp@0.1.7:
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
path-to-regexp@6.2.0, path-to-regexp@^6.1.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.0.tgz#f7b3803336104c346889adece614669230645f38"
integrity sha512-f66KywYG6+43afgE/8j/GoiNyygk/bnoCbps++3ErRKsIYkGGupyv07R2Ok5m9i67Iqc+T2g1eAUGUPzWhYTyg==
path-to-regexp@^1.7.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
@ -18513,16 +18518,6 @@ path-to-regexp@^1.7.0:
dependencies:
isarray "0.0.1"
path-to-regexp@^3.1.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.2.0.tgz#fa7877ecbc495c601907562222453c43cc204a5f"
integrity sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA==
path-to-regexp@^6.1.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.0.tgz#f7b3803336104c346889adece614669230645f38"
integrity sha512-f66KywYG6+43afgE/8j/GoiNyygk/bnoCbps++3ErRKsIYkGGupyv07R2Ok5m9i67Iqc+T2g1eAUGUPzWhYTyg==
path-type@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"