Merge pull request #10752 from strapi/v4/plugin-api/extensions

V4/plugin api/extensions
This commit is contained in:
Pierre Noël 2021-08-18 12:10:16 +02:00 committed by GitHub
commit 65a89df6f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 186 additions and 216 deletions

View File

@ -2,9 +2,6 @@
"kind": "collectionType",
"collectionName": "countries",
"info": {
"displayName": "country",
"singularName": "country",
"pluralName": "countries",
"name": "country",
"description": ""
},

View File

@ -5,9 +5,12 @@ const path = require('path');
module.exports = ({ env }) => ({
graphql: {
enabled: true,
config: require('./plugins/graphql')({ env }),
},
i18n: {
config: require('./plugins/i18n')({ env }),
config: {
amountLimit: 50,
depthLimit: 10,
apolloServer: {
tracing: true,
},
},
},
});

View File

@ -1,7 +0,0 @@
module.exports = ({ env }) => ({
amountLimit: 50,
depthLimit: 10,
apolloServer: {
tracing: true,
},
});

View File

@ -1 +0,0 @@
module.exports = ({ env }) => ({});

View File

@ -1,8 +0,0 @@
module.exports = {
// provider: 'cloudinary',
// providerOptions: {
// cloud_name: 'cloud-name',
// api_key: 'api-key',
// api_secret: 'api-secret',
// },
};

View File

@ -1,3 +0,0 @@
module.exports = {
jwtSecret: process.env.JWT_SECRET || 'c4dc6f71-db45-49c6-82d0-9ca91cb93fa2',
};

View File

@ -1,3 +0,0 @@
module.exports = (ctx, next) => {
next();
};

View File

@ -1,20 +0,0 @@
{
"routes": [
{
"method": "GET",
"path": "/custom-route",
"handler": "users-permissions.customRoute",
"config": {
"policies": ["plugin::users-permissions.customPolicy"]
}
},
{
"method": "GET",
"path": "/",
"handler": "users-permissions.index",
"config": {
"policies": ["plugin::users-permissions.customPolicy"]
}
}
]
}

View File

@ -1,18 +0,0 @@
module.exports = {
query: `
userCustomRoute: String
`,
resolver: {
Mutation: {
updateUser: {
description: 'Updates a user',
policies: ['customPolicy'],
},
},
Query: {
userCustomRoute: {
resolver: 'plugin::users-permissions.users-permissions.customRoute',
},
},
},
};

View File

@ -2,7 +2,10 @@
"collectionName": "up_roles",
"info": {
"name": "role",
"description": ""
"description": "",
"singularName": "role",
"pluralName": "roles",
"displayName": "Role"
},
"options": {
"draftAndPublish": false

View File

@ -1,8 +1,11 @@
{
"collectionName": "up_users",
"info": {
"name": "user",
"description": ""
"name": "User",
"description": "",
"singularName": "user",
"pluralName": "users",
"displayName": "User"
},
"options": {
"draftAndPublish": false
@ -59,7 +62,9 @@
"configurable": false
},
"picture": {
"type": "media"
"type": "media",
"multiple": false,
"required": false
}
}
}

View File

@ -1,5 +0,0 @@
module.exports = {
customRoute(ctx) {
ctx.body = 'allRight';
},
};

View File

@ -1,38 +0,0 @@
module.exports = {
layouts: {
edit: [
[
{
name: 'email',
size: 6,
},
{
name: 'username',
size: 6,
},
],
[
{
name: 'password',
size: 6,
},
],
[
{
name: 'picture',
size: 6,
},
],
[
{
name: 'confirmed',
size: 4,
},
{
name: 'blocked',
size: 4,
},
],
],
},
};

View File

@ -0,0 +1,3 @@
module.exports = plugin => {
return plugin;
};

View File

@ -1,3 +0,0 @@
'use strict';
module.exports = [];

View File

@ -1,7 +1,6 @@
'use strict';
const bootstrap = require('./server/bootstrap');
const contentTypes = require('./server/content-types');
const policies = require('./server/policies');
const services = require('./server/services');
const routes = require('./server/routes');
@ -9,11 +8,9 @@ const controllers = require('./server/controllers');
module.exports = () => {
return {
register: () => {},
bootstrap,
routes,
controllers,
contentTypes,
policies,
services,
};

View File

@ -2,7 +2,6 @@
const path = require('path');
const _ = require('lodash');
const { capitalize } = require('lodash/fp');
const createSchemaHandler = require('./schema-handler');
const createComponentBuilder = require('./component-builder');
@ -32,17 +31,20 @@ module.exports = function createBuilder() {
const contentType = strapi.contentTypes[key];
let dir;
let filename;
if (contentType.plugin) {
dir = `./extensions/${contentType.plugin}/models`;
dir = `./extensions/${contentType.plugin}/content-types/${contentType.info.singularName}`;
filename = 'schema.json';
} else {
dir = `./api/${contentType.apiName}/models`;
filename = contentType.__filename__;
}
return {
modelName: contentType.modelName,
plugin: contentType.plugin,
uid: contentType.uid,
filename: capitalize(`${contentType.info.singularName}.settings.json`),
filename,
dir: path.join(strapi.dir, dir),
schema: contentType.__schema__,
};

View File

@ -103,7 +103,7 @@ class Strapi {
return this;
} catch (error) {
return this.stopWithError(error.message);
return this.stopWithError(error);
}
}
@ -327,7 +327,7 @@ class Strapi {
return;
}
if (this.config.autoReload) {
if (this.config.get('autoReload')) {
this.server.destroy();
process.send('reload');
}

View File

@ -82,12 +82,11 @@ module.exports = async function({ build, watchAdmin, polling, browser }) {
polling,
});
process.on('message', message => {
process.on('message', async message => {
switch (message) {
case 'isKilled':
strapiInstance.server.destroy(() => {
process.send('kill');
});
await strapiInstance.server.destroy();
process.send('kill');
break;
default:
// Do nothing.

View File

@ -1,6 +1,6 @@
'use strict';
const { cloneDeep } = require('lodash/fp');
const { cloneDeep, kebabCase } = require('lodash/fp');
const _ = require('lodash');
const { hasDraftAndPublish } = require('@strapi/utils').contentTypes;
const {
@ -26,7 +26,7 @@ const createContentType = (uid, definition) => {
kind: createdContentType.schema.kind || 'collectionType',
__schema__: pickSchema(definition.schema),
modelType: 'contentType',
modelName: definition.schema.info.singularName,
modelName: kebabCase(definition.schema.info.singularName),
connection: 'default',
});

View File

@ -23,7 +23,7 @@ const strapiServerSchema = yup
services: yup.object().required(),
policies: yup.object().required(),
middlewares: yup.object().required(), // may be removed later
contentTypes: yup.array().required(),
contentTypes: yup.object().required(),
})
.noUnknown();

View File

@ -1,6 +1,7 @@
'use strict';
const path = require('path');
const fs = require('fs');
const fse = require('fs-extra');
/**
@ -10,7 +11,7 @@ module.exports = strapi => {
function normalizePath(optPath) {
const filePath = Array.isArray(optPath) ? optPath.join('/') : optPath;
const normalizedPath = path.normalize(filePath).replace(/^(\/?\.\.?)+/, '');
const normalizedPath = path.normalize(filePath).replace(/^\/?(\.\/|\.\.\/)+/, '');
return path.join(strapi.dir, normalizedPath);
}
@ -44,6 +45,14 @@ module.exports = strapi => {
const removePath = normalizePath(optPath);
return fse.remove(removePath);
},
/**
* Appends a file in strapi app
*/
async appendFile(optPath, data) {
const writePath = normalizePath(optPath);
return fs.appendFileSync(writePath, data);
},
};
return strapiFS;

View File

@ -1,10 +1,11 @@
'use strict';
const { join } = require('path');
const { join, resolve } = require('path');
const { existsSync } = require('fs');
const { defaultsDeep, getOr } = require('lodash/fp');
const { defaultsDeep, getOr, get } = require('lodash/fp');
const { env } = require('@strapi/utils');
const loadConfigFile = require('../app-configuration/load-config-file');
const loadFiles = require('../../load/load-files');
const getEnabledPlugins = require('./get-enabled-plugins');
const defaultPlugin = {
@ -20,7 +21,33 @@ const defaultPlugin = {
services: {},
policies: {},
middlewares: {},
contentTypes: [],
contentTypes: {},
};
const applyUserExtension = async plugins => {
const extensionsDir = resolve(strapi.dir, 'extensions');
if (!existsSync(extensionsDir)) {
return;
}
const extendedSchemas = await loadFiles(extensionsDir, '**/content-types/**/schema.json');
const strapiServers = await loadFiles(extensionsDir, '**/strapi-server.js');
for (const pluginName in plugins) {
const plugin = plugins[pluginName];
// first: load json schema
for (const ctName in plugin.contentTypes) {
const extendedSchema = get([pluginName, 'content-types', ctName, 'schema'], extendedSchemas);
if (extendedSchema) {
plugin.contentTypes[ctName].schema = extendedSchema;
}
}
// second: execute strapi-server extension
const strapiServer = get([pluginName, 'strapi-server'], strapiServers);
if (strapiServer) {
plugins[pluginName] = await strapiServer(plugin);
}
}
};
const formatContentTypes = plugins => {
@ -34,7 +61,7 @@ const formatContentTypes = plugins => {
}
};
const formatConfig = plugins => {
const applyUserConfig = plugins => {
const userPluginConfigPath = join(strapi.dir, 'config', 'plugins.js');
const userPluginsConfig = existsSync(userPluginConfigPath)
? loadConfigFile(userPluginConfigPath)
@ -43,13 +70,18 @@ const formatConfig = plugins => {
for (const pluginName in plugins) {
const plugin = plugins[pluginName];
const userPluginConfig = getOr({}, `${pluginName}.config`, userPluginsConfig);
const formattedConfig = defaultsDeep(plugin.config.default, userPluginConfig);
const defaultConfig =
typeof plugin.config.default === 'function'
? plugin.config.default({ env })
: plugin.config.default;
const config = defaultsDeep(defaultConfig, userPluginConfig);
try {
plugin.config.validator(formattedConfig);
plugin.config.validator(config);
} catch (e) {
throw new Error(`Error regarding ${pluginName} config: ${e.message}`);
}
plugin.config = formattedConfig;
plugin.config = config;
}
};
@ -59,12 +91,12 @@ const loadPlugins = async strapi => {
for (const pluginName in enabledPlugins) {
const enabledPlugin = enabledPlugins[pluginName];
const loadPluginServer = require(join(enabledPlugin.pathToPlugin, 'strapi-server.js'));
const pluginServer = await loadPluginServer({ env });
const pluginServer = loadConfigFile(join(enabledPlugin.pathToPlugin, 'strapi-server.js'));
plugins[pluginName] = defaultsDeep(defaultPlugin, pluginServer);
}
// TODO: validate plugin format
formatConfig(plugins);
applyUserConfig(plugins);
await applyUserExtension(plugins);
formatContentTypes(plugins);
return plugins;

View File

@ -3,6 +3,7 @@
const _ = require('lodash');
const { toLower, kebabCase, camelCase } = require('lodash/fp');
const { getConfigUrls } = require('@strapi/utils');
const pluralize = require('pluralize');
const { createContentType } = require('../domain/content-type');
const { createCoreApi } = require('../../core-api');
@ -23,10 +24,10 @@ module.exports = function(strapi) {
actions: {},
lifecycles: {},
};
ct.schema.info = {};
ct.schema.info.displayName = camelCase(modelName);
ct.schema.info.displayName = model.info.name;
ct.schema.info.singularName = camelCase(modelName);
ct.schema.info.pluralName = `${camelCase(modelName)}s`;
ct.schema.info.pluralName = pluralize(camelCase(modelName));
const createdContentType = createContentType(
`api::${apiName}.${kebabCase(ct.schema.info.singularName)}`,
@ -46,19 +47,21 @@ module.exports = function(strapi) {
}, {});
// Set controllers.
strapi.controllers = Object.keys(strapi.api || []).reduce((acc, key) => {
for (let index in strapi.api[key].controllers) {
let controller = strapi.api[key].controllers[index];
acc[index] = controller;
strapi.controllers = Object.keys(strapi.api || []).reduce((acc, apiName) => {
strapi.container.get('controllers').add(`api::${apiName}`, strapi.api[apiName].controllers);
for (let controllerName in strapi.api[apiName].controllers) {
let controller = strapi.api[apiName].controllers[controllerName];
acc[controllerName] = controller;
}
return acc;
}, {});
// Set services.
strapi.services = Object.keys(strapi.api || []).reduce((acc, key) => {
for (let index in strapi.api[key].services) {
acc[index] = strapi.api[key].services[index];
strapi.services = Object.keys(strapi.api || []).reduce((acc, apiName) => {
strapi.container.get('services').add(`api::${apiName}`, strapi.api[apiName].services);
for (let serviceName in strapi.api[apiName].services) {
acc[serviceName] = strapi.api[apiName].services[serviceName];
}
return acc;
@ -120,6 +123,7 @@ module.exports = function(strapi) {
_.forEach(plugin.middlewares, (middleware, middlewareUID) => {
const middlewareName = toLower(middlewareUID.split('.')[1]);
strapi.plugins[pluginName].middlewares[middlewareName] = middleware;
strapi.middleware[middlewareName] = middleware;
});
_.forEach(plugin.controllers, (controller, controllerUID) => {

View File

@ -19,6 +19,17 @@ const { createContentType } = require('../domain/content-type');
// });
// };
const validateKeySameToSingularName = contentTypes => {
for (const ctName in contentTypes) {
const contentType = contentTypes[ctName];
if (ctName !== contentType.schema.info.singularName) {
throw new Error(
`The key of the content-type should be the same as its singularName. Found ${ctName} and ${contentType.schema.info.singularName}.`
);
}
}
};
const contentTypesRegistry = () => {
const contentTypes = {};
@ -30,13 +41,15 @@ const contentTypesRegistry = () => {
return pickBy((ct, ctUID) => ctUID.startsWith(prefix))(contentTypes);
},
add(namespace, rawContentTypes) {
rawContentTypes.forEach(rawContentType => {
validateKeySameToSingularName(rawContentTypes);
for (const rawCtName in rawContentTypes) {
const rawContentType = rawContentTypes[rawCtName];
const uid = `${namespace}.${rawContentType.schema.info.singularName}`;
if (has(uid, contentTypes)) {
throw new Error(`Content-Type ${uid} has already been registered.`);
throw new Error(`Content-type ${uid} has already been registered.`);
}
contentTypes[uid] = createContentType(uid, rawContentType);
});
}
},
};
};

View File

@ -63,6 +63,7 @@
"open": "8.2.1",
"ora": "^5.4.0",
"package-json": "6.5.0",
"pluralize": "8.0.0",
"qs": "^6.10.1",
"resolve-cwd": "^3.0.0",
"rimraf": "^3.0.2",

View File

@ -2,8 +2,6 @@
const fileModel = require('../../models/File');
module.exports = [
{
schema: fileModel,
},
];
module.exports = {
[fileModel.info.singularName]: { schema: fileModel },
};

View File

@ -150,7 +150,7 @@ module.exports = ({ strapi }) => ({
},
async uploadFileAndPersist(fileData, { user } = {}) {
const config = strapi.plugins.upload.config;
const config = strapi.config.get('plugin.upload');
const {
getDimensions,

View File

@ -1,7 +1,6 @@
'use strict';
const _ = require('lodash');
const pluralize = require('pluralize');
const SINGLE_TYPE = 'singleType';
const COLLECTION_TYPE = 'collectionType';
@ -120,8 +119,8 @@ const isTypedAttribute = (attribute, type) => {
*/
const getContentTypeRoutePrefix = contentType => {
return isSingleType(contentType)
? _.kebabCase(contentType.modelName)
: _.kebabCase(pluralize(contentType.modelName));
? _.kebabCase(contentType.info.singularName)
: _.kebabCase(contentType.info.pluralName);
};
module.exports = {

View File

@ -10,6 +10,6 @@ module.exports = (/* strapi, config */) => {
services: () => {},
policies: {},
middlewares: {},
contentTypes: [],
contentTypes: {},
};
};

View File

@ -2,4 +2,6 @@
const localeModel = require('./locale');
module.exports = [localeModel];
module.exports = {
[localeModel.schema.info.singularName]: localeModel,
};

View File

@ -199,12 +199,21 @@ const addCreateLocalizationAction = contentType => {
const localizationRoute = createLocalizationRoute(contentType);
const coreApiControllerPath = `api.${apiName}.controllers.${modelName}.createLocalization`;
const handler = createLocalizationHandler(contentType);
strapi.config.routes.push(localizationRoute);
_.set(strapi, coreApiControllerPath, handler);
// TODO: to replace with:
// strapi.controllers.extends(`api::${apiName}.${modelName}`, (contr) => ({
// ...controller,
// createLocalization = createLocalizationHandler(contentType),
// }));
// OR
// strapi.api(apiName).controllers.extends(modelName, (contr) => ({
// ...controller,
// createLocalization = createLocalizationHandler(contentType),
// }));
const controller = strapi.container.get('controllers').get(`api::${apiName}.${modelName}`);
controller.createLocalization = createLocalizationHandler(contentType);
};
const mergeCustomizer = (dest, src) => {
@ -217,6 +226,7 @@ const mergeCustomizer = (dest, src) => {
* Add a graphql schema to the plugin's global graphl schema to be processed
* @param {object} schema
*/
// TODO: to replace with V4 config getter
const addGraphqlSchema = schema => {
_.mergeWith(strapi.plugins.i18n.config.schema.graphql, schema, mergeCustomizer);
};

View File

@ -36,7 +36,7 @@ describe('USERS PERMISSIONS | COMPONENTS | UserPermissions | init', () => {
application: [{ method: 'GET', path: '/addresses' }],
};
const policies = ['isauthenticated', 'ratelimit', 'custompolicy'];
const policies = ['isAuthenticated', 'rateLimit', 'custompolicy'];
const expected = {
initialData: permissions,

View File

@ -2,12 +2,12 @@ import formatPolicies from '../formatPolicies';
describe('USERS PERMISSIONS | utils | formatPolicies', () => {
it('should format the policies correclty', () => {
const policies = ['custompolicies', 'ratelimit', 'isauthenticated'];
const policies = ['customPolicies', 'rateLimit', 'isAuthenticated'];
const expected = [
{ label: 'custompolicies', value: 'custompolicies' },
{ label: 'ratelimit', value: 'ratelimit' },
{ label: 'isauthenticated', value: 'isauthenticated' },
{ label: 'customPolicies', value: 'customPolicies' },
{ label: 'rateLimit', value: 'rateLimit' },
{ label: 'isAuthenticated', value: 'isAuthenticated' },
];
expect(formatPolicies(policies)).toEqual(expected);

View File

@ -31,17 +31,14 @@ module.exports = async () => {
await getService('users-permissions').initialize();
// TODO: adapt with new extension system
if (!_.get(strapi.plugins['users-permissions'], 'config.jwtSecret')) {
if (!strapi.config.get('plugin.users-permissions.jwtSecret')) {
const jwtSecret = uuid();
_.set(strapi.plugins['users-permissions'], 'config.jwtSecret', jwtSecret);
strapi.config.set('plugin.users-permissions.jwtSecret', jwtSecret),
(strapi.reload.isWatching = false);
strapi.reload.isWatching = false;
await strapi.fs.writePluginFile(
'users-permissions',
'config/jwt.js',
`module.exports = {\n jwtSecret: process.env.JWT_SECRET || '${jwtSecret}'\n};`
);
if (!process.env.JWT_SECRET) {
await strapi.fs.appendFile('.env', `JWT_SECRET=${jwtSecret}\n`);
}
strapi.reload.isWatching = true;
}

View File

@ -27,7 +27,7 @@ module.exports = async (ctx, next) => {
prefixKey: `${ctx.request.path}:${ctx.request.ip}`,
message,
},
strapi.plugins['users-permissions'].config.ratelimit
strapi.config.get('plugin.users-permissions.ratelimit')
)
)(ctx, next);
};

View File

@ -176,7 +176,7 @@
"path": "/connect/*",
"handler": "auth.connect",
"config": {
"policies": ["plugin::users-permissions.ratelimit"],
"policies": ["plugin::users-permissions.rateLimit"],
"prefix": "",
"description": "Connect a provider",
"tag": {
@ -190,7 +190,7 @@
"path": "/auth/local",
"handler": "auth.callback",
"config": {
"policies": ["plugin::users-permissions.ratelimit"],
"policies": ["plugin::users-permissions.rateLimit"],
"prefix": "",
"description": "Login a user using the identifiers email and password",
"tag": {
@ -204,7 +204,7 @@
"path": "/auth/local/register",
"handler": "auth.register",
"config": {
"policies": ["plugin::users-permissions.ratelimit"],
"policies": ["plugin::users-permissions.rateLimit"],
"prefix": "",
"description": "Register a new user with the default role",
"tag": {
@ -233,7 +233,7 @@
"path": "/auth/forgot-password",
"handler": "auth.forgotPassword",
"config": {
"policies": ["plugin::users-permissions.ratelimit"],
"policies": ["plugin::users-permissions.rateLimit"],
"prefix": "",
"description": "Send the reset password email link",
"tag": {
@ -247,7 +247,7 @@
"path": "/auth/reset-password",
"handler": "auth.resetPassword",
"config": {
"policies": ["plugin::users-permissions.ratelimit"],
"policies": ["plugin::users-permissions.rateLimit"],
"prefix": "",
"description": "Reset user password with a code (resetToken)",
"tag": {

View File

@ -1,6 +1,15 @@
'use strict';
module.exports = {
default: {},
default: ({ env }) => ({
jwtSecret: env('JWT_SECRET'),
jwt: {
expiresIn: '30d',
},
ratelimit: {
interval: 60000,
max: 10,
},
}),
validator: () => {},
};

View File

@ -4,14 +4,8 @@ const permissionModel = require('../../models/Permission.settings');
const roleModel = require('../../models/Role.settings');
const userModel = require('../../models/User.settings');
module.exports = [
{
schema: permissionModel,
},
{
schema: roleModel,
},
{
schema: userModel,
},
];
module.exports = {
[permissionModel.info.singularName]: { schema: permissionModel },
[roleModel.info.singularName]: { schema: roleModel },
[userModel.info.singularName]: { schema: userModel },
};

View File

@ -39,27 +39,25 @@ module.exports = ({ strapi }) => ({
},
issue(payload, jwtOptions = {}) {
_.defaults(jwtOptions, strapi.plugins['users-permissions'].config.jwt);
_.defaults(jwtOptions, strapi.config.get('plugin.users-permissions.jwt'));
return jwt.sign(
_.clone(payload.toJSON ? payload.toJSON() : payload),
_.get(strapi.plugins, ['users-permissions', 'config', 'jwtSecret']),
strapi.config.get('plugin.users-permissions.jwtSecret'),
jwtOptions
);
},
verify(token) {
return new Promise(function(resolve, reject) {
jwt.verify(
token,
_.get(strapi.plugins, ['users-permissions', 'config', 'jwtSecret']),
{},
function(err, tokenPayload = {}) {
if (err) {
return reject(new Error('Invalid token.'));
}
resolve(tokenPayload);
jwt.verify(token, strapi.config.get('plugin.users-permissions.jwtSecret'), {}, function(
err,
tokenPayload = {}
) {
if (err) {
return reject(new Error('Invalid token.'));
}
);
resolve(tokenPayload);
});
});
},
});

View File

@ -7,10 +7,11 @@ const services = require('./server/services');
const routes = require('./server/routes');
const controllers = require('./server/controllers');
const middlewares = require('./server/middlewares');
const config = require('./server/config');
module.exports = () => ({
register: () => {},
bootstrap,
config,
routes,
controllers,
middlewares,