diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index b7a40dd5de..0b97e00b61 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -3,15 +3,17 @@ My PR is a: - - - - +- [ ] 💥 Breaking change +- [ ] 🐛 Bug fix #issueNumber +- [ ] 💅 Enhancement +- [ ] 🚀 New feature Main update on the: - - - - +- [ ] Admin +- [ ] Documentation +- [ ] Framework +- [ ] Plugin + + diff --git a/docs/3.x.x/guides/filters.md b/docs/3.x.x/guides/filters.md index 03f431d2dc..9f58b56029 100644 --- a/docs/3.x.x/guides/filters.md +++ b/docs/3.x.x/guides/filters.md @@ -37,6 +37,16 @@ Find products having a price equal or greater than `3`. `GET /products?price_gte=3` +#### Relations + You can also use filters into a relation attribute which will be applied to the first level of the request. + Find users having written a post named `Title`. + `GET /users?posts.name=Title` + Find posts written by a user having more than 12 years old. + `GET /posts?author.age_gt=12` + > Note: You can't use filter to have specific results inside relation, like "Find users and only their posts older than yesterday" as example. If you need it, you can modify or create your own service ou use [GraphQL](./graphql.md#query-api). + + > Warning: this filter isn't available for `upload` plugin + ### Sort Sort according to a specific field. diff --git a/packages/strapi-admin/admin/src/components/FullStory/index.js b/packages/strapi-admin/admin/src/components/FullStory/index.js new file mode 100644 index 0000000000..590b70b26c --- /dev/null +++ b/packages/strapi-admin/admin/src/components/FullStory/index.js @@ -0,0 +1,95 @@ +/* + * Copyright@React-FullStory (https://github.com/cereallarceny/react-fullstory) + */ + +import React from 'react'; +import PropTypes from 'prop-types'; + +const canUseDOM = !!( + typeof window !== 'undefined' && + window.document && + window.document.createElement +); + +export const getWindowFullStory = () => window[window['_fs_namespace']]; + +class FullStory extends React.Component { + constructor(props) { + super(props); + + window['_fs_debug'] = false; + window['_fs_host'] = 'fullstory.com'; + window['_fs_org'] = props.org; + window['_fs_namespace'] = 'FS'; + (function(m,n,e,t,l,o,g,y) { + if (e in m) { + if(m.console && m.console.log) { + m.console.log('FullStory namespace conflict. Please set window["_fs_namespace"].'); + } + + return; + } + + g = m[e]= function(a,b,s) { + g.q ? g.q.push([a,b,s]) : g._api(a,b,s); + }; + g.q=[]; + o = n.createElement(t); + o.async = 1; + o.src = `https://${window._fs_host}/s/fs.js`; + y = n.getElementsByTagName(t)[0]; + y.parentNode.insertBefore(o,y); + g.identify = function(i,v,s) { + g(l,{ uid:i },s); + + if (v) { + g(l,v,s); + } + }; + g.setUserVars = function(v,s) { + g(l,v,s); + }; + g.event = function(i,v,s) { + g('event',{ n:i,p:v },s); + }; + g.shutdown = function() { + g("rec",!1); + }; + g.restart = function() { + g("rec",!0); + }; + g.consent = function(a) { + g("consent",!arguments.length||a); + }; + g.identifyAccount = function(i,v) { + o = 'account'; + v = v||{}; + v.acctId = i; + g(o,v); + }; + g.clearUserCookie = function() {}; + })(window, document, window['_fs_namespace'], 'script', 'user'); + } + + shouldComponentUpdate() { + return false; + } + + componentWillUnmount() { + if (!canUseDOM || !getWindowFullStory()) return false; + + getWindowFullStory().shutdown(); + + delete getWindowFullStory(); + } + + render() { + return false; + } +} + +FullStory.propTypes = { + org: PropTypes.string.isRequired, +}; + +export default FullStory; diff --git a/packages/strapi-admin/admin/src/containers/AdminPage/index.js b/packages/strapi-admin/admin/src/containers/AdminPage/index.js index f556ed6cc2..72c6a31ce9 100644 --- a/packages/strapi-admin/admin/src/containers/AdminPage/index.js +++ b/packages/strapi-admin/admin/src/containers/AdminPage/index.js @@ -43,6 +43,7 @@ import Logout from 'components/Logout'; import NotFoundPage from 'containers/NotFoundPage/Loadable'; import OverlayBlocker from 'components/OverlayBlocker'; import PluginPage from 'containers/PluginPage'; +import FullStory from 'components/FullStory'; // Utils import auth from 'utils/auth'; import injectReducer from 'utils/injectReducer'; @@ -73,12 +74,12 @@ export class AdminPage extends React.Component { } componentDidUpdate(prevProps) { - const { adminPage: { allowGa }, location: { pathname }, plugins } = this.props; + const { adminPage: { uuid }, location: { pathname }, plugins } = this.props; if (prevProps.location.pathname !== pathname) { this.checkLogin(this.props); - if (allowGa) { + if (uuid) { ReactGA.pageview(pathname); } } @@ -198,6 +199,7 @@ export class AdminPage extends React.Component { return (
+ {this.props.adminPage.uuid ? : ''} {this.showLeftMenu() && ( action.data.allowGa) + .update('uuid', () => action.data.uuid) .update('currentEnvironment', () => action.data.currentEnvironment) .update('layout', () => Map(action.data.layout)) .update('strapiVersion', () => action.data.strapiVersion) diff --git a/packages/strapi-admin/admin/src/containers/AdminPage/saga.js b/packages/strapi-admin/admin/src/containers/AdminPage/saga.js index b922a1705c..f06d28125e 100644 --- a/packages/strapi-admin/admin/src/containers/AdminPage/saga.js +++ b/packages/strapi-admin/admin/src/containers/AdminPage/saga.js @@ -16,13 +16,13 @@ function* getData() { yield call(request, `${strapi.backendURL}/users/me`, { method: 'GET' }); } - const [{ allowGa }, { strapiVersion }, { currentEnvironment }, { layout }] = yield all([ + const [{ uuid }, { strapiVersion }, { currentEnvironment }, { layout }] = yield all([ call(request, '/admin/gaConfig', { method: 'GET' }), call(request, '/admin/strapiVersion', { method: 'GET' }), call(request, '/admin/currentEnvironment', { method: 'GET' }), call(request, '/admin/layout', { method: 'GET' }), ]); - yield put(getAdminDataSucceeded({ allowGa, strapiVersion, currentEnvironment, layout })); + yield put(getAdminDataSucceeded({ uuid, strapiVersion, currentEnvironment, layout })); } catch(err) { console.log(err); // eslint-disable-line no-console diff --git a/packages/strapi-admin/admin/src/containers/InstallPluginPage/styles.scss b/packages/strapi-admin/admin/src/containers/InstallPluginPage/styles.scss index 5d84dcb5c8..007c081812 100644 --- a/packages/strapi-admin/admin/src/containers/InstallPluginPage/styles.scss +++ b/packages/strapi-admin/admin/src/containers/InstallPluginPage/styles.scss @@ -1,8 +1,7 @@ .containerFluid { padding: 18px 30px !important; > div:first-child { - max-height: 33px; - margin-bottom: 48px; + margin-bottom: 11px; } } diff --git a/packages/strapi-admin/controllers/Admin.js b/packages/strapi-admin/controllers/Admin.js index a239825e62..3b4ff00f41 100644 --- a/packages/strapi-admin/controllers/Admin.js +++ b/packages/strapi-admin/controllers/Admin.js @@ -28,8 +28,7 @@ module.exports = { getGaConfig: async ctx => { try { - const allowGa = _.get(strapi.config, 'info.customs.allowGa', true); - ctx.send({ allowGa }); + ctx.send({ uuid: _.get(strapi.config, 'uuid', false) }); } catch(err) { ctx.badRequest(null, [{ messages: [{ id: 'An error occurred' }] }]); } diff --git a/packages/strapi-admin/doc/disable-tracking.md b/packages/strapi-admin/doc/disable-tracking.md index e16bc9f537..93c0b5132f 100644 --- a/packages/strapi-admin/doc/disable-tracking.md +++ b/packages/strapi-admin/doc/disable-tracking.md @@ -10,7 +10,7 @@ If you don't want to share your data with us, you can simply modify the `strapi` ```json { "strapi": { - "allowGa": false + "uuid": false } } ``` diff --git a/packages/strapi-generate-api/templates/bookshelf/service.template b/packages/strapi-generate-api/templates/bookshelf/service.template index 90657eb78c..b74ac459f1 100644 --- a/packages/strapi-generate-api/templates/bookshelf/service.template +++ b/packages/strapi-generate-api/templates/bookshelf/service.template @@ -1,4 +1,5 @@ 'use strict'; +/* global <%= globalID %> */ /** * <%= filename %> service @@ -9,9 +10,6 @@ // Public dependencies. const _ = require('lodash'); -// Strapi utilities. -const utils = require('strapi-hook-bookshelf/lib/utils/'); - module.exports = { /** @@ -21,6 +19,8 @@ module.exports = { */ fetchAll: (params) => { + // Get model hook + const hook = strapi.hook[<%= globalID %>.orm]; // Convert `params` object to filters compatible with Bookshelf. const filters = strapi.utils.models.convertParams('<%= globalID.toLowerCase() %>', params); // Select field to populate. @@ -29,22 +29,18 @@ module.exports = { .map(ast => ast.alias); return <%= globalID %>.query(function(qb) { - _.forEach(filters.where, (where, key) => { - if (_.isArray(where.value) && where.symbol !== 'IN') { - for (const value in where.value) { - qb[value ? 'where' : 'orWhere'](key, where.symbol, where.value[value]) - } + // Generate match stage. + hook.load().generateMatchStage(qb)(<%= globalID %>, filters); + + if (_.has(filters, 'start')) qb.offset(filters.start); + if (_.has(filters, 'limit')) qb.limit(filters.limit); + if (!_.isEmpty(filters.sort)) { + if (filters.sort.key) { + qb.orderBy(filters.sort.key, filters.sort.order); } else { - qb.where(key, where.symbol, where.value); + qb.orderBy(filters.sort); } - }); - - if (filters.sort) { - qb.orderBy(filters.sort.key, filters.sort.order); } - - qb.offset(filters.start); - qb.limit(filters.limit); }).fetchAll({ withRelated: populate }); @@ -81,7 +77,7 @@ module.exports = { _.forEach(filters.where, (where, key) => { if (_.isArray(where.value)) { for (const value in where.value) { - qb[value ? 'where' : 'orWhere'](key, where.symbol, where.value[value]) + qb[value ? 'where' : 'orWhere'](key, where.symbol, where.value[value]); } } else { qb.where(key, where.symbol, where.value); diff --git a/packages/strapi-generate-api/templates/mongoose/service.template b/packages/strapi-generate-api/templates/mongoose/service.template index f5de552e9d..f2295f4c33 100644 --- a/packages/strapi-generate-api/templates/mongoose/service.template +++ b/packages/strapi-generate-api/templates/mongoose/service.template @@ -1,4 +1,5 @@ 'use strict'; +/* global <%= globalID %> */ /** * <%= filename %> service @@ -9,6 +10,8 @@ // Public dependencies. const _ = require('lodash'); +const { models: { mergeStages } } = require('strapi-utils'); + module.exports = { /** @@ -17,22 +20,24 @@ module.exports = { * @return {Promise} */ - fetchAll: (params) => { + fetchAll: (params, next, { populate } = {}) => { // Convert `params` object to filters compatible with Mongo. const filters = strapi.utils.models.convertParams('<%= globalID.toLowerCase() %>', params); - // Select field to populate. - const populate = <%= globalID %>.associations - .filter(ast => ast.autoPopulate !== false) - .map(ast => ast.alias) - .join(' '); + const hook = strapi.hook[<%= globalID %>.orm]; + // Generate stages. + const populateStage = hook.load().generateLookupStage(<%= globalID %>, { whitelistedPopulate: populate }); // Nested-Population + const matchStage = hook.load().generateMatchStage(<%= globalID %>, filters); // Nested relation filter + const aggregateStages = mergeStages(populateStage, matchStage); - return <%= globalID %> - .find() - .where(filters.where) - .sort(filters.sort) + const result = <%= globalID %>.aggregate(aggregateStages) .skip(filters.start) - .limit(filters.limit) - .populate(populate); + .limit(filters.limit); + + if (_.has(filters, 'start')) result.skip(filters.start); + if (_.has(filters, 'limit')) result.limit(filters.limit); + if (!_.isEmpty(filters.sort)) result.sort(filters.sort); + + return result; }, /** diff --git a/packages/strapi-generate-new/json/package.json.js b/packages/strapi-generate-new/json/package.json.js index 1b1f2fad2f..bf0db4aa63 100644 --- a/packages/strapi-generate-new/json/package.json.js +++ b/packages/strapi-generate-new/json/package.json.js @@ -56,6 +56,7 @@ module.exports = scope => { 'dependencies': Object.assign({}, { 'lodash': '^4.17.5', 'strapi': getDependencyVersion(cliPkg, 'strapi'), + 'strapi-utils': getDependencyVersion(cliPkg, 'strapi'), [scope.client.connector]: getDependencyVersion(cliPkg, 'strapi'), }, additionalsDependencies, { [scope.client.module]: scope.client.version diff --git a/packages/strapi-hook-bookshelf/lib/index.js b/packages/strapi-hook-bookshelf/lib/index.js index efd8b33b75..8688d498a0 100644 --- a/packages/strapi-hook-bookshelf/lib/index.js +++ b/packages/strapi-hook-bookshelf/lib/index.js @@ -502,7 +502,7 @@ module.exports = function(strapi) { console.log(e); } - strapi.log.warn(`The SQL database indexes haven't been generated successfully. Please enable the debug mode for more details.`); + strapi.log.warn('The SQL database indexes haven\'t been generated successfully. Please enable the debug mode for more details.'); } } }; @@ -677,24 +677,11 @@ module.exports = function(strapi) { } }; - const table = _.get(manyRelations, 'collectionName') || - _.map( - _.sortBy( - [ - collection.attributes[ - manyRelations.via - ], - manyRelations - ], - 'collection' - ), - table => { - return _.snakeCase( - // eslint-disable-next-line prefer-template - pluralize.plural(table.collection) + ' ' + pluralize.plural(table.via) - ); - } - ).join('__'); + const table = _.get(manyRelations, 'collectionName') + || utilsModels.getCollectionName( + collection.attributes[manyRelations.via], + manyRelations + ); await handler(table, attributes); } @@ -813,24 +800,11 @@ module.exports = function(strapi) { strapi.plugins[details.plugin].models[details.collection]: strapi.models[details.collection]; - const collectionName = _.get(details, 'collectionName') || - _.map( - _.sortBy( - [ - collection.attributes[ - details.via - ], - details - ], - 'collection' - ), - table => { - return _.snakeCase( - // eslint-disable-next-line prefer-template - pluralize.plural(table.collection) + ' ' + pluralize.plural(table.via) - ); - } - ).join('__'); + const collectionName = _.get(details, 'collectionName') + || utilsModels.getCollectionName( + collection.attributes[details.via], + details, + ); const relationship = _.clone( collection.attributes[details.via] diff --git a/packages/strapi-hook-bookshelf/lib/relations.js b/packages/strapi-hook-bookshelf/lib/relations.js index 550c928b23..0b8695510c 100644 --- a/packages/strapi-hook-bookshelf/lib/relations.js +++ b/packages/strapi-hook-bookshelf/lib/relations.js @@ -35,6 +35,77 @@ module.exports = { return _.get(strapi.plugins, [plugin, 'models', model]) || _.get(strapi, ['models', model]) || undefined; }, + generateMatchStage: function (qb) { + return (strapiModel, filters) => { + if (!filters) { + return undefined; + } + + // 1st level deep filter + if (filters.where) { + this.generateMatchStage(qb)(strapiModel, { relations: filters.where }); + } + + // 2nd+ level deep filter + _.forEach(filters.relations, (value, key) => { + if (key !== 'relations') { + const association = strapiModel.associations.find(a => a.alias === key); + if (!association) { + const fieldKey = `${strapiModel.collectionName}.${key}`; + if (_.isArray(value.value) && value.symbol !== 'IN') { + for (const value in value.value) { + qb[value ? 'where' : 'orWhere'](fieldKey, value.symbol, value.value[value]); + } + } else { + qb.where(fieldKey, value.symbol, value.value); + } + } else { + const model = association.plugin ? + strapi.plugins[association.plugin].models[association.model || association.collection] : + strapi.models[association.model || association.collection]; + const relationTable = model.collectionName; + + qb.distinct(); + + if (association.nature === 'manyToMany') { + // Join on both ends + qb.innerJoin( + association.tableCollectionName, + `${association.tableCollectionName}.${strapiModel.info.name}_${strapiModel.primaryKey}`, + `${strapiModel.collectionName}.${strapiModel.primaryKey}`, + ); + + qb.innerJoin( + relationTable, + `${association.tableCollectionName}.${strapiModel.attributes[key].attribute}_${strapiModel.attributes[key].column}`, + `${relationTable}.${model.primaryKey}`, + ); + } else { + const externalKey = association.type === 'collection' + ? `${relationTable}.${association.via}` + : `${relationTable}.${model.primaryKey}`; + + const internalKey = association.type === 'collection' + ? `${strapiModel.collectionName}.${strapiModel.primaryKey}` + : `${strapiModel.collectionName}.${association.alias}`; + + qb.innerJoin(relationTable, externalKey, internalKey); + } + + if (_.isPlainObject(value)) { + this.generateMatchStage(qb)( + model, + { relations: value.value } + ); + } + } + } else { + this.generateMatchStage(qb)(strapiModel, { relations: value }); + } + }); + }; + }, + findOne: async function (params, populate) { const record = await this .forge({ diff --git a/packages/strapi-hook-mongoose/lib/index.js b/packages/strapi-hook-mongoose/lib/index.js index b2cc899a6a..cdfdc16569 100644 --- a/packages/strapi-hook-mongoose/lib/index.js +++ b/packages/strapi-hook-mongoose/lib/index.js @@ -17,6 +17,8 @@ const { models: utilsModels } = require('strapi-utils'); // Local helpers. const utils = require('./utils/'); +const _utils = utils(); + const relations = require('./relations'); /** @@ -488,16 +490,16 @@ module.exports = function (strapi) { result.value = value; break; case '_sort': - result.key = `sort`; + result.key = 'sort'; result.value = (_.toLower(value) === 'desc') ? '-' : ''; result.value += key; break; case '_start': - result.key = `start`; + result.key = 'start'; result.value = parseFloat(value); break; case '_limit': - result.key = `limit`; + result.key = 'limit'; result.value = parseFloat(value); break; case '_contains': @@ -520,6 +522,13 @@ module.exports = function (strapi) { } return result; + }, + + postProcessValue: (value) => { + if (_.isArray(value)) { + return value.map(_utils.valueToId); + } + return _utils.valueToId(value); } }, relations); diff --git a/packages/strapi-hook-mongoose/lib/relations.js b/packages/strapi-hook-mongoose/lib/relations.js index 3acd9c728d..9f37eb035d 100644 --- a/packages/strapi-hook-mongoose/lib/relations.js +++ b/packages/strapi-hook-mongoose/lib/relations.js @@ -10,11 +10,156 @@ const _ = require('lodash'); // Utils const { models: { getValuePrimaryKey } } = require('strapi-utils'); + +const buildTempFieldPath = field => { + return `__${field}`; +}; + +const restoreRealFieldPath = (field, prefix) => { + return `${prefix}${field}`; +}; + module.exports = { getModel: function (model, plugin) { return _.get(strapi.plugins, [plugin, 'models', model]) || _.get(strapi, ['models', model]) || undefined; }, + generateLookupStage: function (strapiModel, { whitelistedPopulate = null, prefixPath = '' } = {}) { + return strapiModel.associations + .filter(ast => { + if (whitelistedPopulate) { + return _.includes(whitelistedPopulate, ast.alias); + } + return ast.autoPopulate; + }) + .reduce((acc, ast) => { + const model = ast.plugin + ? strapi.plugins[ast.plugin].models[ast.collection || ast.model] + : strapi.models[ast.collection || ast.model]; + + const from = model.collectionName; + const isDominantAssociation = + (ast.dominant && ast.nature === 'manyToMany') || !!ast.model; + + const _localField = + !isDominantAssociation || ast.via === 'related' ? '_id' : ast.alias; + + const localField = `${prefixPath}${_localField}`; + + const foreignField = ast.filter + ? `${ast.via}.ref` + : isDominantAssociation + ? '_id' + : ast.via; + + // Add the juncture like the `.populate()` function + const asTempPath = buildTempFieldPath(ast.alias, prefixPath); + const asRealPath = restoreRealFieldPath(ast.alias, prefixPath); + acc.push({ + $lookup: { + from, + localField, + foreignField, + as: asTempPath, + }, + }); + + // Unwind the relation's result if only one is expected + if (ast.type === 'model') { + acc.push({ + $unwind: { + path: `$${asTempPath}`, + preserveNullAndEmptyArrays: true, + }, + }); + } + + // Preserve relation field if it is empty + acc.push({ + $addFields: { + [asRealPath]: { + $ifNull: [`$${asTempPath}`, null], + }, + }, + }); + + // Remove temp field + acc.push({ + $project: { + [asTempPath]: 0, + }, + }); + + return acc; + }, []); + }, + + generateMatchStage: function (strapiModel, filters, { prefixPath = '' } = {}) { + if (!filters) { + return undefined; + } + + let acc = []; + + // 1st level deep filter + if (filters.where) { + acc.push( + ...this.generateMatchStage( + strapiModel, + { relations: filters.where }, + { prefixPath } + ) + ); + } + + // 2nd+ level deep filter + _.forEach(filters.relations, (value, key) => { + if (key !== 'relations') { + const nextPrefixedPath = `${prefixPath}${key}.`; + const association = strapiModel.associations.find(a => a.alias === key); + + if (!association) { + acc.push({ + $match: { [`${prefixPath}${key}`]: value }, + }); + } else { + const model = association.plugin + ? strapi.plugins[association.plugin].models[ + association.collection || association.model + ] + : strapi.models[association.collection || association.model]; + + // Generate lookup for this relation + acc.push( + ...this.generateLookupStage(strapiModel, { + whitelistedPopulate: [key], + prefixPath, + }) + ); + + // If it's an object re-run the same function with this new value until having either a primitive value or an array. + if (_.isPlainObject(value)) { + acc.push( + ...this.generateMatchStage( + model, + { relations: value }, + { + prefixPath: nextPrefixedPath, + } + ) + ); + } + } + } else { + acc.push( + ...this.generateMatchStage(strapiModel, { relations: value }, { prefixPath }) + ); + } + }); + + return acc; + }, + update: async function (params) { const virtualFields = []; const response = await this diff --git a/packages/strapi-hook-mongoose/lib/utils/index.js b/packages/strapi-hook-mongoose/lib/utils/index.js index 8906ed5613..2e0ff1412e 100644 --- a/packages/strapi-hook-mongoose/lib/utils/index.js +++ b/packages/strapi-hook-mongoose/lib/utils/index.js @@ -4,9 +4,23 @@ * Module dependencies */ -module.exports = mongoose => { - var Decimal = require('mongoose-float').loadType(mongoose, 2); - var Float = require('mongoose-float').loadType(mongoose, 20); +// Public node modules. +const mongoose = require('mongoose'); +const Mongoose = mongoose.Mongoose; + +/** + * Convert MongoDB ID to the stringify version as GraphQL throws an error if not. + * + * Refer to: https://github.com/graphql/graphql-js/commit/3521e1429eec7eabeee4da65c93306b51308727b#diff-87c5e74dd1f7d923143e0eee611f598eR183 + */ +mongoose.Types.ObjectId.prototype.valueOf = function () { + return this.toString(); +}; + +module.exports = (mongoose = new Mongoose()) => { + + const Decimal = require('mongoose-float').loadType(mongoose, 2); + const Float = require('mongoose-float').loadType(mongoose, 20); return { convertType: mongooseType => { @@ -42,5 +56,16 @@ module.exports = mongoose => { default: } }, + valueToId: function (value) { + return this.isMongoId(value) + ? mongoose.Types.ObjectId(value) + : value; + }, + isMongoId: function (value) { + // Here we don't use mongoose.Types.ObjectId.isValid method because it's a weird check, + // it returns for instance true for any integer value ¯\_(ツ)_/¯ + const hexadecimal = /^[0-9A-F]+$/i; + return hexadecimal.test(value) && value.length === 24; + } }; }; diff --git a/packages/strapi-plugin-content-manager/admin/src/containers/SettingPage/styles.scss b/packages/strapi-plugin-content-manager/admin/src/containers/SettingPage/styles.scss index fc55593091..3de9ee6fe8 100644 --- a/packages/strapi-plugin-content-manager/admin/src/containers/SettingPage/styles.scss +++ b/packages/strapi-plugin-content-manager/admin/src/containers/SettingPage/styles.scss @@ -1,7 +1,7 @@ .containerFluid { padding: 18px 30px; > div:first-child { - max-height: 33px; + margin-bottom: 11px; } } @@ -192,4 +192,4 @@ .padded { padding-bottom: 1px; -} \ No newline at end of file +} diff --git a/packages/strapi-plugin-content-manager/admin/src/containers/SettingsPage/styles.scss b/packages/strapi-plugin-content-manager/admin/src/containers/SettingsPage/styles.scss index cc3db06f18..5f859e82d4 100644 --- a/packages/strapi-plugin-content-manager/admin/src/containers/SettingsPage/styles.scss +++ b/packages/strapi-plugin-content-manager/admin/src/containers/SettingsPage/styles.scss @@ -1,7 +1,7 @@ .containerFluid { padding: 18px 30px; > div:first-child { - max-height: 33px; + margin-bottom: 11px; } } @@ -35,4 +35,4 @@ border-bottom: none; } } -} \ No newline at end of file +} diff --git a/packages/strapi-plugin-email/package.json b/packages/strapi-plugin-email/package.json index 47c9f0f0ff..fdcd00416b 100644 --- a/packages/strapi-plugin-email/package.json +++ b/packages/strapi-plugin-email/package.json @@ -49,4 +49,4 @@ "npm": ">= 5.0.0" }, "license": "MIT" -} +} \ No newline at end of file diff --git a/packages/strapi-plugin-graphql/services/Query.js b/packages/strapi-plugin-graphql/services/Query.js index 32a5c16e34..989e78e89d 100644 --- a/packages/strapi-plugin-graphql/services/Query.js +++ b/packages/strapi-plugin-graphql/services/Query.js @@ -25,6 +25,23 @@ module.exports = { }, {}); }, + convertToQuery: function(params) { + const result = {}; + + _.forEach(params, (value, key) => { + if (_.isPlainObject(value)) { + const flatObject = this.convertToQuery(value); + _.forEach (flatObject, (_value, _key) => { + result[`${key}.${_key}`] = _value; + }); + } else { + result[key] = value; + } + }); + + return result; + }, + /** * Security to avoid infinite limit. * @@ -175,13 +192,15 @@ module.exports = { // Plural. return async (ctx, next) => { - ctx.params = this.amountLimiting(ctx.params); - ctx.query = Object.assign( - this.convertToParams(_.omit(ctx.params, 'where')), - ctx.params.where, + const queryOpts = {}; + queryOpts.params = this.amountLimiting(ctx.params); + queryOpts.query = Object.assign( + {}, + this.convertToParams(_.omit(queryOpts.params, 'where')), + this.convertToQuery(queryOpts.params.where) ); - return controller(ctx, next); + return controller(Object.assign({}, ctx, queryOpts, { send: ctx.send }), next, { populate: [] }); }; })(); @@ -256,8 +275,12 @@ module.exports = { // Resolver can be a function. Be also a native resolver or a controller's action. if (_.isFunction(resolver)) { - context.query = this.convertToParams(options); context.params = this.amountLimiting(options); + context.query = Object.assign( + {}, + this.convertToParams(_.omit(options, 'where')), + this.convertToQuery(options.where) + ); if (isController) { const values = await resolver.call(null, context); diff --git a/packages/strapi-plugin-graphql/services/Resolvers.js b/packages/strapi-plugin-graphql/services/Resolvers.js index 379ba28722..81827a912c 100644 --- a/packages/strapi-plugin-graphql/services/Resolvers.js +++ b/packages/strapi-plugin-graphql/services/Resolvers.js @@ -398,11 +398,11 @@ module.exports = { queryOpts.skip = convertedParams.start; switch (association.nature) { - case 'manyToMany': { + case "manyToMany": { const arrayOfIds = (obj[association.alias] || []).map( related => { return related[ref.primaryKey] || related; - }, + } ); // Where. @@ -413,7 +413,6 @@ module.exports = { ...where.where, }).where; break; - // falls through } default: // Where. diff --git a/packages/strapi-plugin-upload/package.json b/packages/strapi-plugin-upload/package.json index 76c576a467..849229487d 100644 --- a/packages/strapi-plugin-upload/package.json +++ b/packages/strapi-plugin-upload/package.json @@ -47,4 +47,4 @@ "npm": ">= 3.0.0" }, "license": "MIT" -} +} \ No newline at end of file diff --git a/packages/strapi-plugin-users-permissions/admin/src/containers/EditPage/styles.scss b/packages/strapi-plugin-users-permissions/admin/src/containers/EditPage/styles.scss index ade50dafc9..6232326bdb 100644 --- a/packages/strapi-plugin-users-permissions/admin/src/containers/EditPage/styles.scss +++ b/packages/strapi-plugin-users-permissions/admin/src/containers/EditPage/styles.scss @@ -5,7 +5,7 @@ .containerFluid { padding: 18px 30px; > div:first-child { - max-height: 33px; + margin-bottom: 11px; } } @@ -37,4 +37,4 @@ justify-content: center; min-height: 260px; margin: auto; -} \ No newline at end of file +} diff --git a/packages/strapi-plugin-users-permissions/config/queries/bookshelf.js b/packages/strapi-plugin-users-permissions/config/queries/bookshelf.js index e2382a70dd..860ac31e09 100644 --- a/packages/strapi-plugin-users-permissions/config/queries/bookshelf.js +++ b/packages/strapi-plugin-users-permissions/config/queries/bookshelf.js @@ -1,27 +1,16 @@ const _ = require('lodash'); + module.exports = { find: async function (params = {}, populate) { - const records = await this.query(function(qb) { - _.forEach(params.where, (where, key) => { - if (_.isArray(where.value)) { - for (const value in where.value) { - qb[value ? 'where' : 'orWhere'](key, where.symbol, where.value[value]); - } - } else { - qb.where(key, where.symbol, where.value); - } - }); + const hook = strapi.hook[this.orm]; + const records = await this.query((qb) => { + // Generate match stage. + hook.load().generateMatchStage(qb)(this, params); - if (params.start) { - qb.offset(params.start); - } - - if (params.limit) { - qb.limit(params.limit); - } - - if (params.sort) { + if (_.has(params, 'start')) qb.offset(params.start); + if (_.has(params, 'limit')) qb.limit(params.limit); + if (!_.isEmpty(params.sort)) { if (params.sort.key) { qb.orderBy(params.sort.key, params.sort.order); } else { @@ -33,7 +22,6 @@ module.exports = { withRelated: populate || _.keys(_.groupBy(_.reject(this.associations, { autoPopulate: false }), 'alias')) }); - return records ? records.toJSON() : records; }, diff --git a/packages/strapi-plugin-users-permissions/config/queries/mongoose.js b/packages/strapi-plugin-users-permissions/config/queries/mongoose.js index 53ce2cd17a..88e988ed95 100644 --- a/packages/strapi-plugin-users-permissions/config/queries/mongoose.js +++ b/packages/strapi-plugin-users-permissions/config/queries/mongoose.js @@ -1,14 +1,22 @@ const _ = require('lodash'); +const { models: { mergeStages } } = require('strapi-utils'); + module.exports = { - find: async function (params = {}, populate) { - return this - .find(params.where) - .limit(Number(params.limit)) - .sort(params.sort) - .skip(Number(params.skip)) - .populate(populate || this.associations.map(x => x.alias).join(' ')) - .lean(); + find: async function (filters = {}, populate) { + const hook = strapi.hook[this.orm]; + // Generate stages. + const populateStage = hook.load().generateLookupStage(this, { whitelistedPopulate: populate }); // Nested-Population + const matchStage = hook.load().generateMatchStage(this, filters); // Nested relation filter + const aggregateStages = mergeStages(populateStage, matchStage); + + const result = this.aggregate(aggregateStages); + + if (_.has(filters, 'start')) result.skip(filters.start); + if (_.has(filters, 'limit')) result.limit(filters.limit); + if (_.has(filters, 'sort')) result.sort(filters.sort); + + return result; }, count: async function (params = {}) { diff --git a/packages/strapi-plugin-users-permissions/package.json b/packages/strapi-plugin-users-permissions/package.json index 513d5d0b1c..840a5f2363 100644 --- a/packages/strapi-plugin-users-permissions/package.json +++ b/packages/strapi-plugin-users-permissions/package.json @@ -23,12 +23,13 @@ }, "dependencies": { "bcryptjs": "^2.4.3", - "grant-koa": "^3.8.1", + "grant-koa": "^4.2.0", "jsonwebtoken": "^8.1.0", "koa": "^2.1.0", "koa2-ratelimit": "^0.6.1", "purest": "^2.0.1", "request": "^2.83.0", + "strapi-utils": "3.0.0-alpha.14.5", "uuid": "^3.1.0" }, "devDependencies": { @@ -55,4 +56,4 @@ "npm": ">= 5.0.0" }, "license": "MIT" -} \ No newline at end of file +} diff --git a/packages/strapi-provider-email-amazon-ses/package.json b/packages/strapi-provider-email-amazon-ses/package.json index a28935b21b..bd0b308c13 100644 --- a/packages/strapi-provider-email-amazon-ses/package.json +++ b/packages/strapi-provider-email-amazon-ses/package.json @@ -39,4 +39,4 @@ "npm": ">= 5.3.0" }, "license": "MIT" -} +} \ No newline at end of file diff --git a/packages/strapi-provider-email-mailgun/package.json b/packages/strapi-provider-email-mailgun/package.json index 0df48e93bc..e3a5c23561 100644 --- a/packages/strapi-provider-email-mailgun/package.json +++ b/packages/strapi-provider-email-mailgun/package.json @@ -42,4 +42,4 @@ "npm": ">= 5.3.0" }, "license": "MIT" -} +} \ No newline at end of file diff --git a/packages/strapi-provider-email-sendgrid/package.json b/packages/strapi-provider-email-sendgrid/package.json index 37014367fa..bda2fe3145 100644 --- a/packages/strapi-provider-email-sendgrid/package.json +++ b/packages/strapi-provider-email-sendgrid/package.json @@ -42,4 +42,4 @@ "npm": ">= 5.3.0" }, "license": "MIT" -} +} \ No newline at end of file diff --git a/packages/strapi-provider-email-sendmail/package.json b/packages/strapi-provider-email-sendmail/package.json index 25fa7fb62f..f4fa0cdd19 100644 --- a/packages/strapi-provider-email-sendmail/package.json +++ b/packages/strapi-provider-email-sendmail/package.json @@ -41,4 +41,4 @@ "npm": ">= 5.3.0" }, "license": "MIT" -} +} \ No newline at end of file diff --git a/packages/strapi-provider-upload-aws-s3/package.json b/packages/strapi-provider-upload-aws-s3/package.json index c47840adb0..0b6b9a6a07 100644 --- a/packages/strapi-provider-upload-aws-s3/package.json +++ b/packages/strapi-provider-upload-aws-s3/package.json @@ -43,4 +43,4 @@ "npm": ">= 5.3.0" }, "license": "MIT" -} +} \ No newline at end of file diff --git a/packages/strapi-provider-upload-cloudinary/package.json b/packages/strapi-provider-upload-cloudinary/package.json index cdd2fb5aa7..8b448dd016 100644 --- a/packages/strapi-provider-upload-cloudinary/package.json +++ b/packages/strapi-provider-upload-cloudinary/package.json @@ -43,4 +43,4 @@ "npm": ">= 5.3.0" }, "license": "MIT" -} +} \ No newline at end of file diff --git a/packages/strapi-provider-upload-local/package.json b/packages/strapi-provider-upload-local/package.json index 873627d833..f20fbd8f83 100644 --- a/packages/strapi-provider-upload-local/package.json +++ b/packages/strapi-provider-upload-local/package.json @@ -39,4 +39,4 @@ "npm": ">= 5.3.0" }, "license": "MIT" -} +} \ No newline at end of file diff --git a/packages/strapi-provider-upload-rackspace/package.json b/packages/strapi-provider-upload-rackspace/package.json index ca74f7fa13..923773db82 100644 --- a/packages/strapi-provider-upload-rackspace/package.json +++ b/packages/strapi-provider-upload-rackspace/package.json @@ -13,4 +13,4 @@ "pkgcloud": "^1.5.0", "streamifier": "^0.1.1" } -} +} \ No newline at end of file diff --git a/packages/strapi-utils/lib/models.js b/packages/strapi-utils/lib/models.js index f690a032b1..de1545ad24 100644 --- a/packages/strapi-utils/lib/models.js +++ b/packages/strapi-utils/lib/models.js @@ -9,17 +9,15 @@ const path = require('path'); // Public node modules. const _ = require('lodash'); +const pluralize = require('pluralize'); -// Following this discussion https://stackoverflow.com/questions/18082/validate-decimal-numbers-in-javascript-isnumeric this function is the best implem to determine if a value is a valid number candidate -const isNumeric = (value) => { - return !_.isObject(value) && !isNaN(parseFloat(value)) && isFinite(value); -}; +// Constants +const ORDERS = ['ASC', 'DESC']; /* eslint-disable prefer-template */ /* * Set of utils for models */ - module.exports = { /** @@ -37,7 +35,6 @@ module.exports = { getPK: function (collectionIdentity, collection, models) { if (_.isString(collectionIdentity)) { const ORM = this.getORM(collectionIdentity); - try { const GraphQLFunctions = require(path.resolve(strapi.config.appPath, 'node_modules', 'strapi-' + ORM, 'lib', 'utils')); @@ -311,6 +308,16 @@ module.exports = { return _.get(strapi.models, collectionIdentity.toLowerCase() + '.orm'); }, + /** + * Return table name for a collection many-to-many + */ + getCollectionName: (associationA, associationB) => { + return [associationA, associationB] + .sort((a, b) => a.collection < b.collection ? -1 : 1) + .map(table => _.snakeCase(`${pluralize.plural(table.collection)} ${pluralize.plural(table.via)}`)) + .join('__'); + }, + /** * Define associations key to models */ @@ -340,7 +347,7 @@ module.exports = { // Build associations object if (association.hasOwnProperty('collection') && association.collection !== '*') { - definition.associations.push({ + const ast = { alias: key, type: 'collection', collection: association.collection, @@ -350,7 +357,13 @@ module.exports = { dominant: details.dominant !== true, plugin: association.plugin || undefined, filter: details.filter, - }); + }; + + if (infos.nature === 'manyToMany' && definition.orm === 'bookshelf') { + ast.tableCollectionName = this.getCollectionName(association, details); + } + + definition.associations.push(ast); } else if (association.hasOwnProperty('model') && association.model !== '*') { definition.associations.push({ alias: key, @@ -418,9 +431,32 @@ module.exports = { return _.findKey(strapi.models[association.model || association.collection].attributes, {via: attribute}); }, - convertParams: (entity, params) => { + mergeStages: (...stages) => { + return _.unionWith(...stages, _.isEqual); + }, + + convertParams: function (entity, params) { + const { model, models, convertor, postProcessValue } = this.prepareStage( + entity, + params + ); + + const _filter = this.splitPrimitiveAndRelationValues(params); + + // Execute Steps in the given order + return _.flow([ + this.processValues({ model, models, convertor, postProcessValue }), + this.processPredicates({ model, models, convertor }), + this.processGeneratedResults(), + this.mergeWhereAndRelationPayloads() + ])(_filter); + }, + + prepareStage: function (entity, params) { if (!entity) { - throw new Error('You can\'t call the convert params method without passing the model\'s name as a first argument.'); + throw new Error( + 'You can\'t call the convert params method without passing the model\'s name as a first argument.' + ); } // Remove the source params (that can be sent from the ctm plugin) since it is not a filter @@ -428,86 +464,229 @@ module.exports = { delete params.source; } - const model = entity.toLowerCase(); + const modelName = entity.toLowerCase(); + const models = this.getStrapiModels(); + const model = models[modelName]; - const models = _.assign(_.clone(strapi.models), Object.keys(strapi.plugins).reduce((acc, current) => { - _.assign(acc, _.get(strapi.plugins[current], ['models'], {})); - return acc; - }, {})); - - if (!models.hasOwnProperty(model)) { - return this.log.error(`The model ${model} can't be found.`); + if (!model) { + throw new Error(`The model ${modelName} can't be found.`); } - const client = models[model].client; - const connector = models[model].orm; - - if (!connector) { - throw new Error(`Impossible to determine the ORM used for the model ${model}.`); + if (!model.orm) { + throw new Error( + `Impossible to determine the ORM used for the model ${modelName}.` + ); } - const convertor = strapi.hook[connector].load().getQueryParams; - const convertParams = { - where: {}, - sort: '', - start: 0, - limit: 100 + const hook = strapi.hook[model.orm]; + const convertor = hook.load().getQueryParams; + const postProcessValue = hook.load().postProcessValue || _.identity; + + return { + models, + model, + hook, + convertor, + postProcessValue, }; + }, - _.forEach(params, (value, key) => { - let result; - let formattedValue; - let modelAttributes = models[model]['attributes']; - let fieldType; - // Get the field type to later check if it's a string before number conversion - if (modelAttributes[key]) { - fieldType = modelAttributes[key]['type']; - } else { - // Remove the filter keyword at the end - let splitKey = key.split('_').slice(0,-1); - splitKey = splitKey.join('_'); + getStrapiModels: function() { + return { + ...strapi.models, + ...Object.keys(strapi.plugins).reduce( + (acc, pluginName) => ({ + ...acc, + ..._.get(strapi.plugins[pluginName], 'models', {}), + }), + {} + ), + }; + }, - if (modelAttributes[splitKey]) { - fieldType = modelAttributes[splitKey]['type']; - } - } - // Check if the value is a valid candidate to be converted to a number value - if (fieldType !== 'string') { - formattedValue = isNumeric(value) - ? _.toNumber(value) - : value; - } else { - formattedValue = value; - } - - if (_.includes(['_start', '_limit'], key)) { - result = convertor(formattedValue, key); - } else if (key === '_sort') { - const [attr, order = 'ASC'] = formattedValue.split(':'); - result = convertor(order, key, attr); - } else { - const suffix = key.split('_'); - - // Mysql stores boolean as 1 or 0 - if (client === 'mysql' && _.get(models, [model, 'attributes', suffix, 'type']) === 'boolean') { - formattedValue = value === 'true' ? '1' : '0'; - } - - let type; - - if (_.includes(['ne', 'lt', 'gt', 'lte', 'gte', 'contains', 'containss', 'in'], _.last(suffix))) { - type = `_${_.last(suffix)}`; - key = _.dropRight(suffix).join('_'); + splitPrimitiveAndRelationValues: function(_query) { + const result = _.reduce( + _query, + (acc, value, key) => { + if (_.startsWith(key, '_')) { + acc[key] = value; + } else if (!_.includes(key, '.')) { + acc.where[key] = value; } else { - type = '='; + _.set(acc.relations, this.injectRelationInKey(key), value); } + return acc; + }, + { + where: {}, + relations: {}, + sort: '', + start: 0, + limit: 100, + } + ); + return result; + }, - result = convertor(formattedValue, type, key); + injectRelationInKey: function (key) { + const numberOfRelations = key.match(/\./gi).length - 1; + const relationStrings = _.times(numberOfRelations, _.constant('relations')); + return _.chain(key) + .split('.') + .zip(relationStrings) + .flatten() + .compact() + .join('.') + .value(); + }, + + transformFilter: function (filter, iteratee) { + if (!_.isArray(filter) && !_.isPlainObject(filter)) { + return filter; + } + + return _.transform(filter, (updatedFilter, value, key) => { + const updatedValue = iteratee(value, key); + updatedFilter[key] = this.transformFilter(updatedValue, iteratee); + return updatedFilter; + }); + }, + + processValues: function ({ model, models, convertor, postProcessValue }) { + return filter => { + let parentModel = model; + return this.transformFilter(filter, (value, key) => { + const field = this.getFieldFromKey(key, parentModel); + if (!field) { + return this.processMeta(value, key, { + field, + client: model.client, + model, + convertor, + }); + } + if (field.collection || field.model) { + parentModel = models[field.collection || field.model]; + } + return postProcessValue( + this.processValue(value, key, { field, client: model.client, model }) + ); + }); + }; + }, + + getFieldFromKey: function (key, model) { + let field; + // Primary key is a unique case because it doesn't belong to the model's attributes + if (key === model.primaryKey) { + field = { + type: 'ID', // Just in case + }; + } else if (model.attributes[key]) { + field = model.attributes[key]; + } else { + // Remove the filter keyword at the end + let splitKey = key.split('_').slice(0, -1); + splitKey = splitKey.join('_'); + + if (model.attributes[splitKey]) { + field = model.attributes[splitKey]; + } + } + + return field; + }, + + processValue: function (value, key, { field, client }) { + if (field.type === 'boolean' && client === 'mysql') { + return value === 'true' ? '1' : '0'; + } + + return value; + }, + + processMeta: function (value, key, { convertor, model }) { + if (_.includes(['_start', '_limit'], key)) { + return convertor(value, key); + } else if (key === '_sort') { + return this.processSortMeta(value, key, { convertor, model }); + } + + return value; + }, + + processSortMeta: function (value, key, { convertor, model }) { + const [attr, order = 'ASC'] = value.split(':'); + if (!_.includes(ORDERS, order)) { + throw new Error( + `Unkown order value: "${order}", available values are: ${ORDERS.join( + ', ' + )}` + ); + } + + const field = this.getFieldFromKey(attr, model); + if (!field) { + throw new Error(`Unkown field: "${attr}"`); + } + + return convertor(order, key, attr); + }, + + processPredicates: function ({ model, models, convertor }) { + return filter => { + let parentModel = model; + return this.transformFilter(filter, (value, key) => { + const field = this.getFieldFromKey(key, parentModel); + if (!field) { + return value; + } + if (field.collection || field.model) { + parentModel = models[field.collection || field.model]; + } + return this.processCriteriaMeta(value, key, { convertor }); + }); + }; + }, + + processCriteriaMeta: function (value, key, { convertor }) { + let type = '='; + if (key.match(/_{1}(?:ne|lte?|gte?|containss?|in)/)) { + type = key.match(/_{1}(?:ne|lte?|gte?|containss?|in)/)[0]; + key = key.replace(type, ''); + } + return convertor(value, type, key); + }, + + processGeneratedResults: function() { + return filter => { + if (!_.isArray(filter) && !_.isPlainObject(filter)) { + return filter; } - _.set(convertParams, result.key, result.value); - }); + return _.transform(filter, (updatedFilter, value, key) => { + // Only set results for object of shape { value, key } + if (_.has(value, 'value') && _.has(value, 'key')) { + const cleanKey = _.replace(value.key, 'where.', ''); + _.set(updatedFilter, cleanKey, this.processGeneratedResults()(value.value)); + } else { + updatedFilter[key] = this.processGeneratedResults()(value); + } - return convertParams; + return updatedFilter; + }); + }; + }, + + mergeWhereAndRelationPayloads: function() { + return filter => { + return { + ...filter, // Normally here we need to omit where key + relations: { + ...filter.where, + relations: filter.relations + } + }; + }; } }; diff --git a/packages/strapi-utils/package.json b/packages/strapi-utils/package.json index 30b0a1a980..cab3d191fa 100644 --- a/packages/strapi-utils/package.json +++ b/packages/strapi-utils/package.json @@ -23,6 +23,7 @@ "knex": "^0.13.0", "lodash": "^4.17.5", "pino": "^4.7.1", + "pluralize": "^7.0.0", "shelljs": "^0.7.7" }, "author": { diff --git a/scripts/setup.js b/scripts/setup.js index 2b04adc32e..7ad61bbb0f 100644 --- a/scripts/setup.js +++ b/scripts/setup.js @@ -34,7 +34,6 @@ const watcher = (label, cmd, withSuccess = true) => { shell.echo('✅ Success'); shell.echo(''); } - }; const asyncWatcher = (label, cmd, withSuccess = true, resolve) => { @@ -88,7 +87,6 @@ if (shell.test('-e', 'admin/src/config/plugins.json') === false) { shell.cd('../../../'); } - watcher('📦 Linking strapi-admin', 'npm link --no-optional', false); shell.cd('../strapi-generate-admin'); @@ -112,18 +110,33 @@ watcher('', 'npm install ../strapi-hook-knex'); watcher('📦 Linking strapi-hook-bookshelf...', 'npm link'); shell.cd('../strapi'); -watcher('', 'npm install ../strapi-generate ../strapi-generate-admin ../strapi-generate-api ../strapi-generate-new ../strapi-generate-plugin ../strapi-generate-policy ../strapi-generate-service ../strapi-utils'); +watcher( + '', + 'npm install ../strapi-generate ../strapi-generate-admin ../strapi-generate-api ../strapi-generate-new ../strapi-generate-plugin ../strapi-generate-policy ../strapi-generate-service ../strapi-utils' +); watcher('📦 Linking strapi...', 'npm link'); shell.cd('../strapi-plugin-graphql'); -watcher('📦 Linking strapi-plugin-graphql...', 'npm link --no-optional', false); +watcher( + '📦 Linking strapi-plugin-graphql...', + 'npm link --no-optional', + false +); // Plugin services shell.cd('../strapi-provider-upload-local'); -watcher('📦 Linking strapi-provider-upload-local...', 'npm link --no-optional', false); +watcher( + '📦 Linking strapi-provider-upload-local...', + 'npm link --no-optional', + false +); shell.cd('../strapi-provider-email-sendmail'); -watcher('📦 Linking strapi-provider-email-sendmail...', 'npm link --no-optional', false); +watcher( + '📦 Linking strapi-provider-email-sendmail...', + 'npm link --no-optional', + false +); // Plugins with admin shell.cd('../strapi-plugin-email'); @@ -134,19 +147,31 @@ watcher('📦 Linking strapi-plugin-email...', 'npm link --no-optional', false) shell.cd('../strapi-plugin-users-permissions'); watcher('', 'npm install ../strapi-helper-plugin --no-optional'); +watcher('', 'npm install ../strapi-utils --no-optional'); shell.rm('-f', 'package-lock.json'); -watcher('📦 Linking strapi-plugin-users-permissions...', 'npm link --no-optional', false); +watcher( + '📦 Linking strapi-plugin-users-permissions...', + 'npm link --no-optional', + false +); shell.cd('../strapi-plugin-content-manager'); watcher('', 'npm install ../strapi-helper-plugin --no-optional'); shell.rm('-f', 'package-lock.json'); -watcher('📦 Linking strapi-plugin-content-manager...', 'npm link --no-optional', false); +watcher( + '📦 Linking strapi-plugin-content-manager...', + 'npm link --no-optional', + false +); shell.cd('../strapi-plugin-settings-manager'); watcher('', 'npm install ../strapi-helper-plugin --no-optional'); shell.rm('-f', 'package-lock.json'); -watcher('📦 Linking strapi-plugin-settings-manager...', 'npm link --no-optional', false); - +watcher( + '📦 Linking strapi-plugin-settings-manager...', + 'npm link --no-optional', + false +); // Plugins with admin and other plugin's dependencies shell.cd('../strapi-plugin-upload'); @@ -160,16 +185,32 @@ watcher('', 'npm install ../strapi-helper-plugin --no-optional'); watcher('', 'npm install ../strapi-generate --no-optional'); watcher('', 'npm install ../strapi-generate-api --no-optional'); shell.rm('-f', 'package-lock.json'); -watcher('📦 Linking strapi-plugin-content-type-builder...', 'npm link --no-optional', false); +watcher( + '📦 Linking strapi-plugin-content-type-builder...', + 'npm link --no-optional', + false +); - -const pluginsToBuild = ['admin', 'content-manager', 'content-type-builder', 'upload', 'email', 'users-permissions', 'settings-manager']; +const pluginsToBuild = [ + 'admin', + 'content-manager', + 'content-type-builder', + 'upload', + 'email', + 'users-permissions', + 'settings-manager' +]; const buildPlugins = async () => { - const build = (pckgName) => { + const build = pckgName => { return new Promise(resolve => { - const name = pckgName === 'admin' ? pckgName: `plugin-${pckgName}`; - asyncWatcher(`🏗 Building ${name}...`, `cd ../strapi-${name} && IS_MONOREPO=true npm run build`, false, resolve); + const name = pckgName === 'admin' ? pckgName : `plugin-${pckgName}`; + asyncWatcher( + `🏗 Building ${name}...`, + `cd ../strapi-${name} && IS_MONOREPO=true npm run build`, + false, + resolve + ); }); }; @@ -178,23 +219,34 @@ const buildPlugins = async () => { const setup = async () => { if (process.env.npm_config_build) { - if (process.platform === 'darwin') { // Allow async build for darwin platform + if (process.platform === 'darwin') { + // Allow async build for darwin platform await buildPlugins(); } else { pluginsToBuild.map(name => { const pluginName = name === 'admin' ? name : `plugin-${name}`; shell.cd(`../strapi-${pluginName}`); - return watcher(`🏗 Building ${pluginName}...`, 'IS_MONOREPO=true npm run build'); + return watcher( + `🏗 Building ${pluginName}...`, + 'IS_MONOREPO=true npm run build' + ); }); } } // Log installation duration. const installationEndDate = new Date(); - const duration = (installationEndDate.getTime() - installationStartDate.getTime()) / 1000; + const duration = + (installationEndDate.getTime() - installationStartDate.getTime()) / 1000; shell.echo('✅ Strapi has been succesfully installed.'); - shell.echo(`⏳ The installation took ${Math.floor(duration / 60) > 0 ? `${Math.floor(duration / 60)} minutes and ` : ''}${Math.floor(duration % 60)} seconds.`); + shell.echo( + `⏳ The installation took ${ + Math.floor(duration / 60) > 0 + ? `${Math.floor(duration / 60)} minutes and ` + : '' + }${Math.floor(duration % 60)} seconds.` + ); }; setup();