diff --git a/examples/getstarted/exports/graphql/schema.graphql b/examples/getstarted/exports/graphql/schema.graphql new file mode 100644 index 0000000000..517a1892b9 --- /dev/null +++ b/examples/getstarted/exports/graphql/schema.graphql @@ -0,0 +1,336 @@ +type Articles { + id: ID! + created_at: DateTime! + updated_at: DateTime! + title: String +} + +input ArticlesInput { + title: String +} + +input createArticlesInput { + data: ArticlesInput +} + +type createArticlesPayload { + article: Articles +} + +input createRoleInput { + data: RoleInput +} + +type createRolePayload { + role: UsersPermissionsRole +} + +input createTagsInput { + data: TagsInput +} + +type createTagsPayload { + tag: Tags +} + +input createUserInput { + data: UserInput +} + +type createUserPayload { + user: UsersPermissionsUser +} + +""" +The `DateTime` scalar represents a date and time following the ISO 8601 standard +""" +scalar DateTime + +input deleteArticlesInput { + where: InputID +} + +type deleteArticlesPayload { + article: Articles +} + +input deleteRoleInput { + where: InputID +} + +type deleteRolePayload { + role: UsersPermissionsRole +} + +input deleteTagsInput { + where: InputID +} + +type deleteTagsPayload { + tag: Tags +} + +input deleteUserInput { + where: InputID +} + +type deleteUserPayload { + user: UsersPermissionsUser +} + +input editArticlesInput { + title: String +} + +input editFileInput { + name: String + hash: String + sha256: String + ext: String + mime: String + size: String + url: String + provider: String + public_id: String + related: [ID] +} + +input editRoleInput { + name: String + description: String + type: String + permissions: [ID] + users: [ID] +} + +input editTagsInput { + name: String +} + +input editTestInput { + type: String +} + +input editUserInput { + type: String + username: String + email: String + provider: String + password: String + resetPasswordToken: String + confirmed: Boolean + blocked: Boolean + role: ID +} + +input FileInput { + name: String! + hash: String! + sha256: String + ext: String + mime: String! + size: String! + url: String! + provider: String! + public_id: String + related: [ID] +} + +input InputID { + id: ID! +} + +""" +The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). +""" +scalar JSON + +union Morph = UsersPermissionsMe | UsersPermissionsMeRole | Articles | createArticlesPayload | updateArticlesPayload | deleteArticlesPayload | Tags | createTagsPayload | updateTagsPayload | deleteTagsPayload | UploadFile | UsersPermissionsPermission | UsersPermissionsRole | createRolePayload | updateRolePayload | deleteRolePayload | UsersPermissionsUser | createUserPayload | updateUserPayload | deleteUserPayload | MypluginTest + +type Mutation { + createArticles(input: createArticlesInput): createArticlesPayload + updateArticles(input: updateArticlesInput): updateArticlesPayload + deleteArticles(input: deleteArticlesInput): deleteArticlesPayload + createTags(input: createTagsInput): createTagsPayload + updateTags(input: updateTagsInput): updateTagsPayload + deleteTags(input: deleteTagsInput): deleteTagsPayload + + """Create a new role""" + createRole(input: createRoleInput): createRolePayload + + """Update an existing role""" + updateRole(input: updateRoleInput): updateRolePayload + + """Delete an existing role""" + deleteRole(input: deleteRoleInput): deleteRolePayload + + """Create a new user""" + createUser(input: createUserInput): createUserPayload + + """Update an existing user""" + updateUser(input: updateUserInput): updateUserPayload + + """Delete an existing user""" + deleteUser(input: deleteUserInput): deleteUserPayload + upload(refId: ID, ref: String, source: String, file: Upload!): UploadFile! +} + +type MypluginTest { + id: ID! + type: String! +} + +type Query { + article(id: ID!): Articles + articles(sort: String, limit: Int, start: Int, where: JSON): [Articles] + tag(id: ID!): Tags + tags(sort: String, limit: Int, start: Int, where: JSON): [Tags] + files(sort: String, limit: Int, start: Int, where: JSON): [UploadFile] + role(id: ID!): UsersPermissionsRole + + """ + Retrieve all the existing roles. You can't apply filters on this query. + """ + roles(sort: String, limit: Int, start: Int, where: JSON): [UsersPermissionsRole] + user(id: ID!): UsersPermissionsUser + users(sort: String, limit: Int, start: Int, where: JSON): [UsersPermissionsUser] + test(id: ID!): MypluginTest + tests(sort: String, limit: Int, start: Int, where: JSON): [MypluginTest] + me: UsersPermissionsMe +} + +input RoleInput { + name: String! + description: String + type: String + permissions: [ID] + users: [ID] +} + +type Tags { + id: ID! + created_at: DateTime! + updated_at: DateTime! + name: String +} + +input TagsInput { + name: String +} + +input TestInput { + type: String! +} + +input updateArticlesInput { + where: InputID + data: editArticlesInput +} + +type updateArticlesPayload { + article: Articles +} + +input updateRoleInput { + where: InputID + data: editRoleInput +} + +type updateRolePayload { + role: UsersPermissionsRole +} + +input updateTagsInput { + where: InputID + data: editTagsInput +} + +type updateTagsPayload { + tag: Tags +} + +input updateUserInput { + where: InputID + data: editUserInput +} + +type updateUserPayload { + user: UsersPermissionsUser +} + +"""The `Upload` scalar type represents a file upload.""" +scalar Upload + +type UploadFile { + id: ID! + created_at: DateTime! + updated_at: DateTime! + name: String! + hash: String! + sha256: String + ext: String + mime: String! + size: String! + url: String! + provider: String! + public_id: String + related(sort: String, limit: Int, start: Int, where: JSON): [Morph] +} + +input UserInput { + type: String + username: String! + email: String! + provider: String + password: String + resetPasswordToken: String + confirmed: Boolean + blocked: Boolean + role: ID +} + +type UsersPermissionsMe { + _id: ID! + username: String! + email: String! + confirmed: Boolean + blocked: Boolean + role: UsersPermissionsMeRole +} + +type UsersPermissionsMeRole { + _id: ID! + name: String! + description: String + type: String +} + +type UsersPermissionsPermission { + id: ID! + type: String! + controller: String! + action: String! + enabled: Boolean! + policy: String + role: UsersPermissionsRole +} + +type UsersPermissionsRole { + id: ID! + name: String! + description: String + type: String + permissions(sort: String, limit: Int, start: Int, where: JSON): [UsersPermissionsPermission] + users(sort: String, limit: Int, start: Int, where: JSON): [UsersPermissionsUser] +} + +type UsersPermissionsUser { + id: ID! + type: String + username: String! + email: String! + provider: String + confirmed: Boolean + blocked: Boolean + role: UsersPermissionsRole +} diff --git a/examples/getstarted/extensions/users-permissions/config/actions.json b/examples/getstarted/extensions/users-permissions/config/actions.json new file mode 100644 index 0000000000..a6ee344982 --- /dev/null +++ b/examples/getstarted/extensions/users-permissions/config/actions.json @@ -0,0 +1 @@ +{"actions":["application.articles.find","application.articles.findone","application.articles.count","application.articles.create","application.articles.update","application.articles.destroy","application.tags.find","application.tags.findone","application.tags.count","application.tags.create","application.tags.update","application.tags.destroy","content-manager.contentmanager.models","content-manager.contentmanager.find","content-manager.contentmanager.count","content-manager.contentmanager.findone","content-manager.contentmanager.create","content-manager.contentmanager.update","content-manager.contentmanager.updatesettings","content-manager.contentmanager.delete","content-manager.contentmanager.deleteall","content-type-builder.contenttypebuilder.getmodels","content-type-builder.contenttypebuilder.getmodel","content-type-builder.contenttypebuilder.getconnections","content-type-builder.contenttypebuilder.createmodel","content-type-builder.contenttypebuilder.updatemodel","content-type-builder.contenttypebuilder.deletemodel","content-type-builder.contenttypebuilder.autoreload","content-type-builder.contenttypebuilder.checktableexists","email.email.send","email.email.getenvironments","email.email.getsettings","email.email.updatesettings","settings-manager.settingsmanager.menu","settings-manager.settingsmanager.environments","settings-manager.settingsmanager.languages","settings-manager.settingsmanager.databases","settings-manager.settingsmanager.database","settings-manager.settingsmanager.databasemodel","settings-manager.settingsmanager.get","settings-manager.settingsmanager.update","settings-manager.settingsmanager.createlanguage","settings-manager.settingsmanager.deletelanguage","settings-manager.settingsmanager.createdatabase","settings-manager.settingsmanager.updatedatabase","settings-manager.settingsmanager.deletedatabase","settings-manager.settingsmanager.autoreload","upload.upload.upload","upload.upload.getenvironments","upload.upload.getsettings","upload.upload.updatesettings","upload.upload.find","upload.upload.findone","upload.upload.count","upload.upload.destroy","upload.upload.search","users-permissions.auth.callback","users-permissions.auth.changepassword","users-permissions.auth.connect","users-permissions.auth.forgotpassword","users-permissions.auth.register","users-permissions.auth.emailconfirmation","users-permissions.user.find","users-permissions.user.me","users-permissions.user.findone","users-permissions.user.create","users-permissions.user.update","users-permissions.user.destroy","users-permissions.user.destroyall","users-permissions.userspermissions.createrole","users-permissions.userspermissions.deleteprovider","users-permissions.userspermissions.deleterole","users-permissions.userspermissions.getpermissions","users-permissions.userspermissions.getpolicies","users-permissions.userspermissions.getrole","users-permissions.userspermissions.getroles","users-permissions.userspermissions.getroutes","users-permissions.userspermissions.index","users-permissions.userspermissions.init","users-permissions.userspermissions.searchusers","users-permissions.userspermissions.updaterole","users-permissions.userspermissions.getemailtemplate","users-permissions.userspermissions.updateemailtemplate","users-permissions.userspermissions.getadvancedsettings","users-permissions.userspermissions.updateadvancedsettings","users-permissions.userspermissions.getproviders","users-permissions.userspermissions.updateproviders","myplugin.test.findone","myplugin.test.find"]} \ No newline at end of file diff --git a/examples/getstarted/extensions/users-permissions/config/jwt.json b/examples/getstarted/extensions/users-permissions/config/jwt.json new file mode 100644 index 0000000000..0f0f4a8071 --- /dev/null +++ b/examples/getstarted/extensions/users-permissions/config/jwt.json @@ -0,0 +1,3 @@ +{ + "jwtSecret": "1481145e-8625-4032-a4f0-75de2a3f10c9" +} \ No newline at end of file diff --git a/examples/getstarted/extensions/users-permissions/models/User.settings.json b/examples/getstarted/extensions/users-permissions/models/User.settings.json new file mode 100644 index 0000000000..34bdfb47be --- /dev/null +++ b/examples/getstarted/extensions/users-permissions/models/User.settings.json @@ -0,0 +1,57 @@ +{ + "connection": "default", + "info": { + "name": "user", + "description": "" + }, + "attributes": { + "type": { + "type": "string" + }, + "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 + }, + "confirmed": { + "type": "boolean", + "default": false, + "configurable": false + }, + "blocked": { + "type": "boolean", + "default": false, + "configurable": false + }, + "role": { + "model": "role", + "via": "users", + "plugin": "users-permissions", + "configurable": false + } + }, + "collectionName": "users-permissions_user" +} diff --git a/packages/strapi-plugin-graphql/services/Schema.js b/packages/strapi-plugin-graphql/services/Schema.js index ebac664342..2c82565c10 100644 --- a/packages/strapi-plugin-graphql/services/Schema.js +++ b/packages/strapi-plugin-graphql/services/Schema.js @@ -260,15 +260,15 @@ const schemaBuilder = { `; // // Build schema. - // const schema = makeExecutableSchema({ - // typeDefs, - // resolvers, - // }); + if (!strapi.config.currentEnvironment.server.production) { + // Write schema. + const schema = makeExecutableSchema({ + typeDefs, + resolvers, + }); - // if (!strapi.config.currentEnvironment.server.production) { - // // Write schema. - // this.writeGenerateSchema(graphql.printSchema(schema)); - // } + this.writeGenerateSchema(graphql.printSchema(schema)); + } // Remove custom scaler (like Upload); typeDefs = Types.removeCustomScalar(typeDefs, resolvers); @@ -286,27 +286,8 @@ const schemaBuilder = { */ writeGenerateSchema: schema => { - const generatedFolder = path.resolve( - strapi.config.appPath, - 'extensions', - 'graphql', - 'config', - 'generated' - ); - - // Create folder if necessary. - try { - fs.accessSync(generatedFolder, fs.constants.R_OK | fs.constants.W_OK); - } catch (err) { - if (err && err.code === 'ENOENT') { - fs.mkdirSync(generatedFolder); - } else { - strapi.log.error(err); - } - } - - fs.writeFileSync(path.join(generatedFolder, 'schema.graphql'), schema); - }, + return strapi.fs.writeFile('exports/graphql/schema.graphql', schema); + } }; module.exports = schemaBuilder; diff --git a/packages/strapi-plugin-users-permissions/.gitignore b/packages/strapi-plugin-users-permissions/.gitignore index 4caa6ef132..304c5bddad 100644 --- a/packages/strapi-plugin-users-permissions/.gitignore +++ b/packages/strapi-plugin-users-permissions/.gitignore @@ -2,6 +2,8 @@ coverage build node_modules + +# writable files jwt.json config/layout.json actions.json diff --git a/packages/strapi-plugin-users-permissions/config/functions/bootstrap.js b/packages/strapi-plugin-users-permissions/config/functions/bootstrap.js index b04e2e2c39..4fc28d7b26 100644 --- a/packages/strapi-plugin-users-permissions/config/functions/bootstrap.js +++ b/packages/strapi-plugin-users-permissions/config/functions/bootstrap.js @@ -7,37 +7,27 @@ * This gives you an opportunity to set up your data model, * run jobs, or perform some special logic. */ - -const path = require('path'); -const fs = require('fs'); const _ = require('lodash'); const uuid = require('uuid/v4'); module.exports = async cb => { if (!_.get(strapi.plugins['users-permissions'], 'config.jwtSecret')) { - try { - const jwtSecret = uuid(); + const jwtSecret = uuid(); + _.set(strapi.plugins['users-permissions'], 'config.jwtSecret', jwtSecret); - fs.writeFileSync(path.join(strapi.config.appPath, 'plugins', 'users-permissions', 'config', 'jwt.json'), JSON.stringify({ - jwtSecret - }, null, 2), 'utf8'); - - _.set(strapi.plugins['users-permissions'], 'config.jwtSecret', jwtSecret); - } catch(err) { - strapi.log.error(err); - } + await strapi.fs.writePluginFile('users-permissions', 'config/jwt.json', JSON.stringify({ jwtSecret }, null, 2)); } const pluginStore = strapi.store({ environment: '', type: 'plugin', - name: 'users-permissions' + name: 'users-permissions', }); const grantConfig = { email: { enabled: true, - icon: 'envelope' + icon: 'envelope', }, discord: { enabled: false, @@ -45,10 +35,7 @@ module.exports = async cb => { key: '', secret: '', callback: '/auth/discord/callback', - scope: [ - 'identify', - 'email' - ] + scope: ['identify', 'email'], }, facebook: { enabled: false, @@ -56,7 +43,7 @@ module.exports = async cb => { key: '', secret: '', callback: '/auth/facebook/callback', - scope: ['email'] + scope: ['email'], }, google: { enabled: false, @@ -64,7 +51,7 @@ module.exports = async cb => { key: '', secret: '', callback: '/auth/google/callback', - scope: ['email'] + scope: ['email'], }, github: { enabled: false, @@ -72,10 +59,7 @@ module.exports = async cb => { key: '', secret: '', redirect_uri: '/auth/github/callback', - scope: [ - 'user', - 'user:email' - ] + scope: ['user', 'user:email'], }, microsoft: { enabled: false, @@ -83,39 +67,42 @@ module.exports = async cb => { key: '', secret: '', callback: '/auth/microsoft/callback', - scope: ['user.read'] + scope: ['user.read'], }, twitter: { enabled: false, icon: 'twitter', key: '', secret: '', - callback: '/auth/twitter/callback' - } + callback: '/auth/twitter/callback', + }, }; - const prevGrantConfig = await pluginStore.get({key: 'grant'}) || {}; + const prevGrantConfig = (await pluginStore.get({ key: 'grant' })) || {}; // store grant auth config to db // when plugin_users-permissions_grant is not existed in db // or we have added/deleted provider here. - if (!prevGrantConfig || !_.isEqual(_.keys(prevGrantConfig), _.keys(grantConfig))) { + if ( + !prevGrantConfig || + !_.isEqual(_.keys(prevGrantConfig), _.keys(grantConfig)) + ) { // merge with the previous provider config. - _.keys(grantConfig).forEach((key) => { + _.keys(grantConfig).forEach(key => { if (key in prevGrantConfig) { grantConfig[key] = _.merge(grantConfig[key], prevGrantConfig[key]); } }); - await pluginStore.set({key: 'grant', value: grantConfig}); + await pluginStore.set({ key: 'grant', value: grantConfig }); } - if (!await pluginStore.get({key: 'email'})) { + if (!(await pluginStore.get({ key: 'email' }))) { const value = { - 'reset_password': { + reset_password: { display: 'Email.template.reset_password', icon: 'refresh', options: { from: { name: 'Administration Panel', - email: 'no-reply@strapi.io' + email: 'no-reply@strapi.io', }, response_email: '', object: '­Reset password', @@ -125,16 +112,16 @@ module.exports = async cb => {

<%= URL %>?code=<%= TOKEN %>

-

Thanks.

` - } +

Thanks.

`, + }, }, - 'email_confirmation': { + email_confirmation: { display: 'Email.template.email_confirmation', icon: 'check-square-o', options: { from: { name: 'Administration Panel', - email: 'no-reply@strapi.io' + email: 'no-reply@strapi.io', }, response_email: '', object: 'Account confirmation', @@ -144,24 +131,26 @@ module.exports = async cb => {

<%= URL %>?confirmation=<%= CODE %>

-

Thanks.

` - } - } +

Thanks.

`, + }, + }, }; - await pluginStore.set({key: 'email', value}); + await pluginStore.set({ key: 'email', value }); } - if (!await pluginStore.get({key: 'advanced'})) { + if (!(await pluginStore.get({ key: 'advanced' }))) { const value = { unique_email: true, allow_register: true, email_confirmation: false, - email_confirmation_redirection: `http://${strapi.config.currentEnvironment.server.host}:${strapi.config.currentEnvironment.server.port}/admin`, - default_role: 'authenticated' + email_confirmation_redirection: `http://${ + strapi.config.currentEnvironment.server.host + }:${strapi.config.currentEnvironment.server.port}/admin`, + default_role: 'authenticated', }; - await pluginStore.set({key: 'advanced', value}); + await pluginStore.set({ key: 'advanced', value }); } strapi.plugins['users-permissions'].services.userspermissions.initialize(cb); diff --git a/packages/strapi-plugin-users-permissions/services/UsersPermissions.js b/packages/strapi-plugin-users-permissions/services/UsersPermissions.js index 731d44a911..d441dddf98 100644 --- a/packages/strapi-plugin-users-permissions/services/UsersPermissions.js +++ b/packages/strapi-plugin-users-permissions/services/UsersPermissions.js @@ -1,7 +1,5 @@ 'use strict'; -const fs = require('fs-extra'); -const path = require('path'); const _ = require('lodash'); const request = require('request'); @@ -279,7 +277,7 @@ module.exports = { return _.merge({ application: routes }, pluginsRoutes); }, - updatePermissions: async function(cb) { + async updatePermissions() { // fetch all the current permissions from the database, and format them into an array of actions. const databasePermissions = await strapi .query('permission', 'users-permissions') @@ -438,11 +436,7 @@ module.exports = { ), ]), ); - - return this.writeActions(currentActions, cb); } - - cb(); }, removeDuplicate: async function() { @@ -490,15 +484,18 @@ module.exports = { }); }, - initialize: async function(cb) { - const roles = await strapi.query('role', 'users-permissions').count(); + async initialize(cb) { + const roleCount = await strapi.query('role', 'users-permissions').count(); // It has already been initialized. - if (roles > 0) { - return await this.updatePermissions(async () => { + if (roleCount > 0) { + try { + await this.updatePermissions(); await this.removeDuplicate(); - cb(); - }); + return cb(); + } catch (err) { + return cb(err); + } } // Create two first default roles. @@ -515,7 +512,8 @@ module.exports = { }), ]); - await this.updatePermissions(cb); + + this.updatePermissions().then(() => cb(), err => cb(err)); }, updateRole: async function(roleID, body) { @@ -610,28 +608,6 @@ module.exports = { ); }, - writeActions: (data, cb) => { - const actionsPath = path.join(strapi.config.appPath, 'extensions', 'users-permissions', 'config', 'actions.json'); - - try { - // Disable auto-reload. - strapi.reload.isWatching = false; - if (!strapi.config.currentEnvironment.server.production) { - // Rewrite actions.json file. - fs.ensureFileSync(actionsPath); - fs.writeFileSync(actionsPath, JSON.stringify({ actions: data }), 'utf8'); - } - // Set value to AST to avoid restart. - _.set(strapi.plugins['users-permissions'], 'config.actions', data); - // Disable auto-reload. - strapi.reload.isWatching = true; - - cb(); - } catch (err) { - strapi.log.error(err); - } - }, - template: (layout, data) => { const compiledObject = _.template(layout); return compiledObject(data); diff --git a/packages/strapi/bin/strapi-start.js b/packages/strapi/bin/strapi-start.js index e359406513..155fba4c44 100644 --- a/packages/strapi/bin/strapi-start.js +++ b/packages/strapi/bin/strapi-start.js @@ -52,7 +52,7 @@ const watchFileChanges = ({ appPath, strapi }) => { '**/cypress', '**/cypress/**', '**/*.db*', - '**/generated/schema.graphql' + '**/exports/**' ], }); diff --git a/packages/strapi/lib/Strapi.js b/packages/strapi/lib/Strapi.js index 14a342e305..59fae8014e 100644 --- a/packages/strapi/lib/Strapi.js +++ b/packages/strapi/lib/Strapi.js @@ -18,10 +18,18 @@ const { bootstrap, plugins, admin, + loadExtensions, initCoreStore, } = require('./core'); const initializeMiddlewares = require('./middlewares'); const initializeHooks = require('./hooks'); +const createStrapiFs = require('./core/fs'); + +const getPrefixedDependencies = (prefix, pkgJSON) => { + return Object.keys(pkgJSON.dependencies) + .filter(d => d.startsWith(prefix)) + .map(pkgName => pkgName.substring(prefix.length + 1)); +}; /** * Construct an Strapi instance. @@ -88,6 +96,8 @@ class Strapi extends EventEmitter { functions: {}, routes: {}, }; + + this.fs = createStrapiFs(this); } async start(config = {}, cb) { @@ -211,6 +221,7 @@ class Strapi extends EventEmitter { process.exit(1); } + // TODO: Split code async load() { await this.enhancer(); @@ -223,24 +234,13 @@ class Strapi extends EventEmitter { } }); - this.config.info = require(path.resolve( - this.config.appPath, - 'package.json' - )); - - this.config.installedPlugins = Object.keys(this.config.info.dependencies) - .filter(d => d.startsWith('strapi-plugin')) - .map(pkgName => pkgName.substring('strapi-plugin'.length + 1)); - - this.config.installedMiddlewares = Object.keys( - this.config.info.dependencies - ) - .filter(d => d.startsWith('strapi-middleware')) - .map(pkgName => pkgName.substring('strapi-middleware'.length + 1)); - - this.config.installedHooks = Object.keys(this.config.info.dependencies) - .filter(d => d.startsWith('strapi-hook')) - .map(pkgName => pkgName.substring('strapi-hook'.length + 1)); + const pkgJSON = require(path.resolve(this.config.appPath, 'package.json')); + Object.assign(this.config, { + info: pkgJSON, + installedPlugins: getPrefixedDependencies('strapi-plugin', pkgJSON), + installedMiddlewares: getPrefixedDependencies('strapi-middleware', pkgJSON), + installedHooks: getPrefixedDependencies('strapi-hook', pkgJSON), + }); // load configs _.merge(this, await loadConfigs(this.config)); @@ -255,6 +255,17 @@ class Strapi extends EventEmitter { // load hooks this.hook = await loadHooks(this.config); + /** + * Handle plugin extensions + */ + const extensions = await loadExtensions(this.config); + // merge extensions config folders + _.merge(strapi.plugins, extensions.configs); + // overwrite plugins with extensions overwrites + extensions.overwrites.forEach(({ path, mod }) => + _.set(strapi.plugins, path, mod) + ); + // Populate AST with configurations. await bootstrap.call(this); // Usage. diff --git a/packages/strapi/lib/core/fs.js b/packages/strapi/lib/core/fs.js new file mode 100644 index 0000000000..64951f5d12 --- /dev/null +++ b/packages/strapi/lib/core/fs.js @@ -0,0 +1,38 @@ +const path = require('path'); +const fs = require('fs-extra'); + +/** + * create strapi fs layer + */ +module.exports = strapi => { + /** + * Writes a file in a strapi app + * @param {Array|string} optPath - file path + * @param {string} data - content + */ + const writeFile = (optPath, data) => { + const filePath = Array.isArray(optPath) ? optPath.join('/') : optPath; + + const normalizedPath = path.normalize(filePath).replace(/^(\/?\.\.?)+/, ''); + + const writePath = path.join(strapi.config.appPath, normalizedPath); + + return fs.ensureFile(writePath).then(() => fs.writeFile(writePath, data)); + }; + + /** + * Writes a file in a plugin extensions folder + * @param {string} plugin - plugin name + * @param {Array|string} optPath - path to file + * @param {string} data - content + */ + const writePluginFile = (plugin, optPath, data) => { + const newPath = ['extensions', plugin].concat(optPath).join('/'); + return writeFile(newPath, data); + }; + + return { + writeFile, + writePluginFile, + }; +}; diff --git a/packages/strapi/lib/core/index.js b/packages/strapi/lib/core/index.js index 3c82fe95df..d30ccaed1e 100644 --- a/packages/strapi/lib/core/index.js +++ b/packages/strapi/lib/core/index.js @@ -3,6 +3,7 @@ const loadConfigs = require('./load-configs'); const loadApis = require('./load-apis'); const loadMiddlewares = require('./load-middlewares'); +const loadExtensions = require('./load-extensions'); const loadHooks = require('./load-hooks'); const bootstrap = require('./bootstrap'); const plugins = require('./plugins'); @@ -14,6 +15,7 @@ module.exports = { loadMiddlewares, loadHooks, loadApis, + loadExtensions, bootstrap, plugins, admin, diff --git a/packages/strapi/lib/core/load-configs.js b/packages/strapi/lib/core/load-configs.js index 2ddd506c31..39be962cdc 100644 --- a/packages/strapi/lib/core/load-configs.js +++ b/packages/strapi/lib/core/load-configs.js @@ -5,8 +5,7 @@ const _ = require('lodash'); const fs = require('fs-extra'); const findPackagePath = require('../load/package-path'); -const loadFiles = require('../load/load-files'); -const requireFileAndParse = require('../load/require-file-parse'); +const loadConfig = require('../load/load-config-files'); module.exports = async ({ appPath, installedPlugins }) => { const [config, admin, api, plugins, localPlugins] = await Promise.all([ @@ -25,38 +24,6 @@ module.exports = async ({ appPath, installedPlugins }) => { }; }; -const loadConfig = dir => { - return loadFiles(dir, 'config/**/*.+(js|json)', { - requireFn: requireFileAndParse, - shouldUseFileNameAsKey, - }); -}; - -const prefixedPaths = [ - ...['staging', 'production', 'development'].reduce((acc, env) => { - return acc.concat( - `environments/${env}/database`, - `environments/${env}/security`, - `environments/${env}/request`, - `environments/${env}/response`, - `environments/${env}/server` - ); - }, []), - 'functions', - 'policies', - 'locales', - 'hook', - 'middleware', - 'language', - 'queries', - 'layout', -]; - -const shouldUseFileNameAsKey = file => { - return _.some(prefixedPaths, e => file.startsWith(`config/${e}`)) - ? true - : false; -}; // Loads an app config folder const loadAppConfig = async appPath => { diff --git a/packages/strapi/lib/core/load-extensions.js b/packages/strapi/lib/core/load-extensions.js new file mode 100644 index 0000000000..4a1795f363 --- /dev/null +++ b/packages/strapi/lib/core/load-extensions.js @@ -0,0 +1,37 @@ +'use strict'; + +const path = require('path'); +const loadConfig = require('../load/load-config-files'); +const glob = require('../load/glob'); +const filePathToPath = require('../load/filepath-to-prop-path'); + +const overwritableFoldersGlob = 'models'; + +module.exports = async function({ appPath }) { + const extensionsDir = path.resolve(appPath, 'extensions'); + + const overwrites = await loadOverwrites(extensionsDir); + const configs = await loadConfig(extensionsDir, '*/config/**/*.+(js|json)'); + + return { + overwrites, + configs, + }; +}; + +// returns a list of path and module to overwrite +const loadOverwrites = async extensionsDir => { + const files = await glob(`*/${overwritableFoldersGlob}/*.*(js|json)`, { + cwd: extensionsDir, + }); + + return files.map(file => { + const mod = require(path.resolve(extensionsDir, file)); + const propPath = filePathToPath(file); + + return { + path: propPath, + mod, + }; + }); +}; diff --git a/packages/strapi/lib/load/filepath-to-prop-path.js b/packages/strapi/lib/load/filepath-to-prop-path.js new file mode 100644 index 0000000000..0f32629ad3 --- /dev/null +++ b/packages/strapi/lib/load/filepath-to-prop-path.js @@ -0,0 +1,13 @@ +const path = require('path'); + +module.exports = (fileP, useFileNameAsKey = true) => { + const prop = path + .normalize(fileP) + .replace(/(.settings|.json|.js)/g, '') + .toLowerCase() + .split('/') + .join('.') + .split('.'); + + return useFileNameAsKey === true ? prop : prop.slice(0, -1); +}; diff --git a/packages/strapi/lib/load/load-config-files.js b/packages/strapi/lib/load/load-config-files.js new file mode 100644 index 0000000000..aa3cf7064e --- /dev/null +++ b/packages/strapi/lib/load/load-config-files.js @@ -0,0 +1,34 @@ +const _ = require('lodash'); +const loadFiles = require('./load-files'); +const requireFileAndParse = require('./require-file-parse'); + +module.exports = (dir, pattern = 'config/**/*.+(js|json)') => + loadFiles(dir, pattern, { + requireFn: requireFileAndParse, + shouldUseFileNameAsKey, + }); + +const shouldUseFileNameAsKey = file => { + return _.some(prefixedPaths, e => file.startsWith(`config/${e}`)) + ? true + : false; +}; +const prefixedPaths = [ + ...['staging', 'production', 'development'].reduce((acc, env) => { + return acc.concat( + `environments/${env}/database`, + `environments/${env}/security`, + `environments/${env}/request`, + `environments/${env}/response`, + `environments/${env}/server` + ); + }, []), + 'functions', + 'policies', + 'locales', + 'hook', + 'middleware', + 'language', + 'queries', + 'layout', +]; diff --git a/packages/strapi/lib/load/load-files.js b/packages/strapi/lib/load/load-files.js index 8efc22fa84..6cb2a68b78 100644 --- a/packages/strapi/lib/load/load-files.js +++ b/packages/strapi/lib/load/load-files.js @@ -1,18 +1,7 @@ const path = require('path'); const glob = require('./glob'); const _ = require('lodash'); - -const filePathToPath = (fileP, useFileNameAsKey = true) => { - const prop = path - .normalize(fileP) - .replace(/(.settings|.json|.js)/g, '') - .toLowerCase() - .split('/') - .join('.') - .split('.'); - - return useFileNameAsKey === true ? prop : prop.slice(0, -1); -}; +const filePathToPath = require('./filepath-to-prop-path'); module.exports = async ( dir,