add extension system for content-types

This commit is contained in:
Pierre Noël 2021-08-13 15:35:19 +02:00
parent 18da6e2188
commit 2d7454553a
32 changed files with 281 additions and 63 deletions

View File

@ -0,0 +1,11 @@
'use strict';
module.exports = ({ env }) => ({
'users-permissions': {
contentTypes: {
users: {
schema: 'users-permissions/models/user.json',
},
},
},
});

View File

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

View File

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

View File

@ -1,3 +1,3 @@
module.exports = {
jwtSecret: process.env.JWT_SECRET || 'c4dc6f71-db45-49c6-82d0-9ca91cb93fa2',
jwtSecret: process.env.JWT_SECRET || 'efec74b5-274a-4b6b-8077-d3ac6bfdfd89',
};

View File

@ -0,0 +1,44 @@
{
"collectionName": "up_roles",
"info": {
"name": "role",
"description": "",
"singularName": "role",
"pluralName": "roles",
"displayName": "Role"
},
"options": {
"draftAndPublish": false
},
"attributes": {
"name": {
"type": "string",
"minLength": 3,
"required": true,
"configurable": false
},
"description": {
"type": "string",
"configurable": false
},
"type": {
"type": "string",
"unique": true,
"configurable": false
},
"permissions": {
"type": "relation",
"relation": "oneToMany",
"target": "plugin::users-permissions.permission",
"mappedBy": "role",
"configurable": false
},
"users": {
"type": "relation",
"relation": "oneToMany",
"target": "plugin::users-permissions.user",
"mappedBy": "role",
"configurable": false
}
}
}

View File

@ -0,0 +1,84 @@
{
"kind": "collectionType",
"collectionName": "up_users",
"info": {
"name": "User",
"description": "",
"singularName": "user",
"pluralName": "users",
"displayName": "User"
},
"options": {
"draftAndPublish": false,
"timestamps": true
},
"attributes": {
"username": {
"type": "string",
"minLength": 3,
"unique": true,
"configurable": false,
"required": true
},
"email": {
"type": "email",
"minLength": 6,
"configurable": false,
"required": true
},
"provider": {
"type": "string",
"configurable": false
},
"password": {
"type": "password",
"minLength": 6,
"configurable": false,
"private": true
},
"resetPasswordToken": {
"type": "string",
"configurable": false,
"private": true
},
"confirmationToken": {
"type": "string",
"configurable": false,
"private": true
},
"confirmed": {
"type": "boolean",
"default": false,
"configurable": false
},
"blocked": {
"type": "boolean",
"default": false,
"configurable": false
},
"role": {
"type": "relation",
"relation": "manyToOne",
"target": "plugin::users-permissions.role",
"inversedBy": "users",
"configurable": false
},
"picture": {
"type": "media",
"multiple": false,
"required": false
},
"aaa": {
"type": "string"
},
"bbbb": {
"type": "string"
},
"ccc": {
"type": "string"
},
"dddd": {
"type": "string"
}
}
}

View File

@ -0,0 +1,16 @@
// 1 - load original plugin
// 2 - load content types overwrites
// 3 - execute plugin extensions
module.exports = plugin => {
// extend article content type
plugin.contentTypes.article.collectionName === 'foo';
plugin.routes.push({
method: 'GET',
handler: 'myCtrl.actionA',
});
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');
@ -33,7 +32,7 @@ module.exports = function createBuilder() {
let dir;
if (contentType.plugin) {
dir = `./extensions/${contentType.plugin}/models`;
dir = `./extensions/${contentType.plugin}/content-types/${contentType.info.singularName}`;
} else {
dir = `./api/${contentType.apiName}/models`;
}
@ -42,7 +41,7 @@ module.exports = function createBuilder() {
modelName: contentType.modelName,
plugin: contentType.plugin,
uid: contentType.uid,
filename: capitalize(`${contentType.info.singularName}.settings.json`),
filename: 'schema.json',
dir: path.join(strapi.dir, dir),
schema: contentType.__schema__,
};

View File

@ -321,7 +321,7 @@ class Strapi {
return;
}
if (this.config.autoReload) {
if (this.config.get('autoReload')) {
this.server.destroy();
process.send('reload');
}

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);
},
/**
* Removes 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,25 @@ const defaultPlugin = {
services: {},
policies: {},
middlewares: {},
contentTypes: [],
contentTypes: {},
};
const applyUserExtension = async plugins => {
const extensionsDir = resolve(strapi.dir, 'extensions');
if (!existsSync(extensionsDir)) {
return;
}
const files = await loadFiles(extensionsDir, '**/content-types/**/schema.json');
for (const pluginName in plugins) {
const plugin = plugins[pluginName];
for (const ctName in plugin.contentTypes) {
const extendedSchema = get([pluginName, 'content-types', ctName, 'schema'], files);
if (extendedSchema) {
plugin.contentTypes[ctName].schema = extendedSchema;
}
}
}
};
const formatContentTypes = plugins => {
@ -34,7 +53,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 +62,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 +83,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

@ -46,19 +46,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;

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

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

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) => {

View File

@ -37,11 +37,9 @@ module.exports = async () => {
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

@ -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

@ -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,

View File

@ -18,7 +18,7 @@ module.exports = {
author: {
type: 'relation',
relation: 'manyToOne',
target: 'plugins::users-permissions.user',
target: 'plugin::users-permissions.user',
targetAttribute: 'articles',
},
},