diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5d9ab64699..81a301ac52 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contribute to Strapi -👍🎉 First off, thanks for taking the time to contribute! 🎉👍 +First off, thanks for taking the time to contribute! 🎉👍 The following is a set of guidelines for contributing to Strapi and its packages. @@ -32,40 +32,115 @@ If you send a pull request, please do it again the `master` branch. We are devel ## Setup Development Environment To facilitate the contribution, we drastically reduce the amount of commands necessary to install the entire development environment. First of all, you need to check if you're using the recommended versions of Node.js (v8) and npm (v5). -**Then, please follow the instructions below:** +Then, please follow the instructions below: -1. [Fork the repository](https://github.com/strapi/strapi) to your own GitHub account. -2. Clone it to your computer `git clone git@github.com:strapi/strapi.git`. -3. Run `npm run setup` at the root of the directory. +#### 1. ▪️ Fork the repository -> Note: If the installation failed, please remove the global packages related to Strapi. The command `npm ls strapi` will help you to find where your packages are installed globally. +[Go to the repository](https://github.com/strapi/strapi) and fork it to your own GitHub account. -> Note: You can run `npm run setup:build` to build the plugins' admin (the setup time will be longer). +#### 2. 💿 Clone the repository +```bash +git clone git@github.com:strapi/strapi.git +``` -The development environment has been installed. Now, you have to create a development project to live-test your updates. +#### 3. ⏳ Installation + +Go to the root of the repository. +```bash +cd strapi +``` -1. Go to a folder on your computer `cd /path/to/my/folder`. -2. Create a new project `strapi new myDevelopmentProject --dev`. -3. Start your app with `strapi start`. +**Two setup are available... with or without the front-end builds.** -Awesome! You are now able to make bug fixes or enhancements in the framework layer of Strapi. **To make updates in the administration panel, you need to go a little bit further.** +Without the front-end builds, you won't be able to access to the administration panel via http://localhost:1337/admin, you'll have to run the administration separately and access it through http://localhost:4000/admin. -4. Open a new tab or new terminal window. -5. Go to the `my-app/admin` folder of your currently running app. -6. Run `npm start` and go to the following url [http://localhost:4000/admin](http://localhost:4000/admin) +
+ +Without the front-end builds (recommended) +```bash +npm run setup +``` +or with the front-end builds +```bash +npm run setup:build +``` + +> ⚠️  If the installation failed, please remove the global packages related to Strapi. The command `npm ls strapi` will help you to find where your packages are installed globally. + +#### 4. 🏗 Create a new project + +You can open a new terminal window and go into any folder you want for the next steps. +```bash +cd /.../workspace/ +``` + +The command to generate a project is the same, except you have to add the `--dev` argument at the end of line. +```bash +strapi new my-project --dev +``` + +#### 5. 🚀 Start the project + +First, you have to start the server. +```bash +cd ./my-project +strapi start +``` + +The server (API) is available at http://localhost:1337 + +> ⚠️  If you've followed the recommended setup, you should not be able to reach the administration panel at http://localhost:1337/admin. + +Then, you have to start the Webpack server to build and run the administration. +```bash +cd ./my-project/admin +npm run start +``` + +The administration panel is available at http://localhost:4000/admin + +**Awesome! You are now able to contribute to Strapi.** + +--- ## Plugin Development Setup -To create a new plugin, you'll have to run the following commands +To create a new plugin, you'll have to run the following commands: -1. In your project folder `cd myDevelopmentProject && strapi generate:plugin my-plugin`. -2. Make sure that the `strapi-helper-plugin` is linked to your plugin - - In the folder where strapi is cloned `cd pathToStrapiRepo/strapi/packages/strapi-helper-plugin && npm link`. - - In your project folder `cd pathToMyProject/myDevelopmentProject/plugins/my-plugin && npm link strapi-helper-plugin`. -3. Start the server in the admin folder `cd pathToMyProject/myDevelopmentProject/admin && npm start` and go to the following url [http://localhost:4000/admin](http://localhost:4000/admin). +#### 1. 🏗 Generate a new plugin -*** +```bash +cd ./my-project +strapi generate:plugin my-plugin +``` + +#### 2. ✅ Verify the symlink + +Make you that the `strapi-helper-plugin` is linked to your project. + +Please run this command in the repository folder where Strapi is cloned: +```bash +cd /repository/strapi/packages/strapi-helper-plugin +npm link +``` + +Link the `strapi-helper-plugin` node_modules in the plugin folder: +```bash +cd ./my-project/plugins/my-plugin +npm link strapi-helper-plugin +``` + +#### 3. 🚀 Start the project + +```bash +cd ./my-project/admin +npm run start +``` + +The administration panel is available at http://localhost:4000/admin + +--- ## Reporting an issue diff --git a/README.md b/README.md index 8d8a1610c0..6d152b0053 100755 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@

API creation made simple, secure and fast.

-

The most advanced open-source Content Management Framework to build powerful API with no effort.

+

The most advanced open-source Content Management Framework (headless-CMS) to build powerful API with no effort.


@@ -32,35 +32,52 @@

- +


-## Quick start +## Getting Started -We've been working on a major update to Strapi for several months now, rewriting the core framework and the administration panel. Performances has been increased, Developer eXperience has been improved and a brand new plugins -ecosystem has been introduced. **Both versions are available, we still recommend you to use v1 for production usage.**. +Read the Getting Started tutorial or follow the steps below: + +#### ⏳ Installation + +```bash +npm install strapi@alpha -g +```` + +**We recommend to use the latest version of Strapi to start your new project**. +Some breaking changes might happen, new releases are shipped every two weeks to fix/enhance the product. + +#### 🏗 Create a new project + +```bash +strapi new my-project +``` + +It will generate a brand new project with the default features (authentication, permissions, content management, content type builder & file upload). + +#### 🚀 Start your project + +```bash +cd my-project +strapi start +``` + +Congratulations, you made it! Enjoy 🎉 + +
+ +You can also give it a try using Heroku! Be aware that one of the content type builder won't work due to the writing files restriction on the Heroku servers. Deploy -#### Alpha +
-The alpha has support for the latest version of Node.js (v9) and npm (v5). -```bash -npm install strapi@alpha -g -``` - -#### Stable -This is the production-ready version of Strapi (v1). You should also consider that the migration to v3 will not be easy due to many breaking changes. -```bash -npm install strapi -g -``` - -Read the [Getting started](https://strapi.io/getting-started) page to create your first project using Strapi. ## Features @@ -73,19 +90,17 @@ Read the [Getting started](https://strapi.io/getting-started) page to create you - **Powerful CLI:** Scaffold projects and APIs on the fly. - **SQL & NoSQL databases:** Work with Mongo as a main database, also supports Postgres, MySQL, etc. -## Philosophy ? +**[See more on our website](https://strapi.io/overview)** -> At [Strapi](https://strapi.io), everything we do we believe in changing the status quo of web development. Our products are simple to use, user friendly and production-ready. +## Contributing -Web and mobile applications needed a powerful, simple to use and production-ready API-driven solution. That's why we created Strapi, an open-source Content Management Framework (CMF) for exposing your content (data, media) accross multi-devices. - -Halfway between a CMS and a framework, Strapi takes advantages of both worlds. A powerful dashboard to easily manage your content with a flexible framework layer to develop and integrate specific features. +Please read our [Contributing Guide](./CONTRIBUTING.md) before submitting a Pull Request to the project. ## Support For more information on the upcoming version, please take a look to our [ROADMAP](https://github.com/strapi/strapi/projects). -### Community support +#### Community support For general help using Strapi, please refer to [the official Strapi documentation](https://strapi.io/documentation/). For additional help, you can use one of this channel to ask question: @@ -95,13 +110,15 @@ For general help using Strapi, please refer to [the official Strapi documentatio - [Twitter](https://twitter.com/strapijs) - [Facebook](https://www.facebook.com/Strapi-616063331867161). -### Professional support +#### Professional support -[Strapi Solutions](https://strapi.io), the company behind Strapi, provides a full range of solutions to get better results, faster. We're always looking for the next challenge: coaching, consulting, training, customization, etc. [Drop us an email](mailto:support@strapi.io) to see how we can help you. +[Strapi Solutions](https://strapi.io), the company behind Strapi, provides a full range of solutions to get better results, faster. We're always looking for the next challenge: coaching, consulting, training, customization, etc. -### Migration +[Drop us an email](mailto:support@strapi.io) to see how we can help you. -Follow our [migration guides](https://github.com/strapi/strapi/wiki) on the wiki to keep your Strapi projects updated. +## Migration + +Follow our [migration guides](https://github.com/strapi/strapi/wiki) on the wiki to keep your projects up-to-date. ## License diff --git a/docs/3.x.x/en/guides/graphql.md b/docs/3.x.x/en/guides/graphql.md index 6449538a25..3f67c612f1 100644 --- a/docs/3.x.x/en/guides/graphql.md +++ b/docs/3.x.x/en/guides/graphql.md @@ -146,6 +146,149 @@ type Query { The query will use the generated controller's actions as resolvers. It means that the `posts` query will execute the `Post.find` action and the `post` query will use the `Post.findOne` action. +## Aggregation & Grouping +> This feature is only available on Mongoose ORM. + +Strapi now supports Aggregation & Grouping. +Let's consider again the model mentioned above: +``` +type Post { + _id: ID + createdAt: String + updatedAt: String + title: String + content: String + nb_likes: Int, + published: Boolean +} + +``` +Strapi will generate automatically for you the following queries & types: + +### Aggregation +``` +type PostConnection { + values: [Post] + groupBy: PostGroupBy + aggregate: PostAggregator +} + +type PostGroupBy { + _id: [PostConnection_id] + createdAt: [PostConnectionCreatedAt] + updatedAt: [PostConnectionUpdatedAt] + title: [PostConnectionTitle] + content: [PostConnectionContent] + nb_likes: [PostConnectionNbLikes], + published: [PostConnectionPublished] +} + +type PostConnectionPublished { + key: Boolean + connection: PostConnection +} + +type PostAggregator { + count: Int + sum: PostAggregatorSum + avg: PostAggregatorAvg + min: PostAggregatorMin + max: PostAggregatorMax +} + +type PostAggregatorAvg { + nb_likes: Float +} + +type PostAggregatorMin { // Same for max and sum + nb_likes: Int +} + +type Query { + postsConnection(sort: String, limit: Int, start: Int, where: JSON): PostConnection +} +``` + +Getting the total count and the average likes of posts: + +``` +postsConnection { + aggregate { + count + avg { + nb_likes + } + } + +} +``` + +Let's say we want to do the same query but for only published posts +``` +postsConnection(where: { published: true }) { + aggregate { + count + avg { + nb_likes + } + } + +} +``` + +Gettings the average likes of published and unpublished posts + +``` +postsConnection { + groupBy { + published: { + key + connection { + aggregate { + avg { + nb_likes + } + } + } + } + } +} +``` + +Result +```JSON +{ + data: { + postsConnection: { + groupBy: { + published: [ + { + key: true, + connection: { + aggregate: { + avg { + nb_likes: 10 + } + } + } + }, + { + key: false, + connection: { + aggregate: { + avg { + nb_likes: 0 + } + } + } + } + ] + } + } + } +} +``` + ## Customise the GraphQL schema If you want to define a new scalar, input or enum types, this section is for you. To do so, you will have to create a `schema.graphql` file. This file has to be placed into the config folder of each API `./api/*/config/schema.graphql` or plugin `./plugins/*/config/schema.graphql`. diff --git a/docs/3.x.x/en/guides/models.md b/docs/3.x.x/en/guides/models.md index 91f0a517bc..de58ff2c6d 100644 --- a/docs/3.x.x/en/guides/models.md +++ b/docs/3.x.x/en/guides/models.md @@ -19,6 +19,11 @@ The info key on the model-json states information about the model. This informat - `description`: The description of the model. - `mainField`: Determines which model-attribute is shown when displaying the model. +## Model options +The options key on the model-json states. + - `idAttribute`: This tells the model which attribute to expect as the unique identifier for each database row (typically an auto-incrementing primary key named 'id'). + - `idAttributeType`: Data type of `idAttribute`, accepted list of value bellow: + ## Define the attributes The following types are currently available: diff --git a/packages/strapi-admin/admin/src/app.js b/packages/strapi-admin/admin/src/app.js index a7f4cff8d0..83a6e8debd 100755 --- a/packages/strapi-admin/admin/src/app.js +++ b/packages/strapi-admin/admin/src/app.js @@ -129,7 +129,7 @@ if (window.location.port !== '4000') { }); }) .catch(err => { - console.log(err); + console.log(err); // eslint-disable-line no-console }); } else if (findIndex(plugins, ['id', 'users-permissions']) === -1) { store.dispatch(unsetHasUserPlugin()); diff --git a/packages/strapi-admin/admin/src/components/LeftMenuLink/index.js b/packages/strapi-admin/admin/src/components/LeftMenuLink/index.js index 5a36b8e12b..d37f84e0bc 100755 --- a/packages/strapi-admin/admin/src/components/LeftMenuLink/index.js +++ b/packages/strapi-admin/admin/src/components/LeftMenuLink/index.js @@ -20,9 +20,10 @@ class LeftMenuLink extends React.Component { // We need to create our own active url checker, // because of the two levels router. const isLinkActive = startsWith( - window.location.pathname.replace('/admin', ''), - this.props.destination, + window.location.pathname.replace('/admin', '').concat('/'), + this.props.destination.concat('/'), ); + const plugin = this.props.source !== 'content-manager' && this.props.source !== '' ? (
diff --git a/packages/strapi-admin/admin/src/containers/HomePage/index.js b/packages/strapi-admin/admin/src/containers/HomePage/index.js index 39a12cc6aa..a307417f03 100644 --- a/packages/strapi-admin/admin/src/containers/HomePage/index.js +++ b/packages/strapi-admin/admin/src/containers/HomePage/index.js @@ -137,7 +137,7 @@ export class HomePage extends React.PureComponent { const data = this.showFirstBlock() ? { className: styles.homePageTutorialButton, - href: 'https://strapi.io/documentation/getting-started/quick-start.html', + href: 'https://strapi.io/documentation/getting-started/quick-start.html#create-your-first-api', id: 'app.components.HomePage.button.quickStart', primary: true, } diff --git a/packages/strapi-admin/admin/src/containers/HomePage/styles.scss b/packages/strapi-admin/admin/src/containers/HomePage/styles.scss index ffab3626ff..6c8c72d2fd 100644 --- a/packages/strapi-admin/admin/src/containers/HomePage/styles.scss +++ b/packages/strapi-admin/admin/src/containers/HomePage/styles.scss @@ -1,9 +1,9 @@ .blockLink { position: relative; width: calc(50% - 6px); - height: 99px; + height: auto; margin-top: 41px; - padding: 22px 25px 0 96px; + padding: 22px 25px 19px 96px; background: #F7F8F8; border-radius: 3px; line-height: 18px; diff --git a/packages/strapi-admin/admin/src/containers/InstallPluginPage/actions.js b/packages/strapi-admin/admin/src/containers/InstallPluginPage/actions.js index 4de39e11f8..6f58869926 100644 --- a/packages/strapi-admin/admin/src/containers/InstallPluginPage/actions.js +++ b/packages/strapi-admin/admin/src/containers/InstallPluginPage/actions.js @@ -8,8 +8,10 @@ import { DOWNLOAD_PLUGIN, DOWNLOAD_PLUGIN_ERROR, DOWNLOAD_PLUGIN_SUCCEEDED, - GET_PLUGINS, - GET_PLUGINS_SUCCEEDED, + GET_AVAILABLE_PLUGINS, + GET_AVAILABLE_PLUGINS_SUCCEEDED, + GET_INSTALLED_PLUGINS, + GET_INSTALLED_PLUGINS_SUCCEEDED, ON_CHANGE, } from './constants'; @@ -32,19 +34,32 @@ export function downloadPluginSucceeded() { }; } -export function getPlugins() { +export function getAvailablePlugins() { return { - type: GET_PLUGINS, + type: GET_AVAILABLE_PLUGINS, }; } -export function getPluginsSucceeded(availablePlugins) { +export function getAvailablePluginsSucceeded(availablePlugins) { return { - type: GET_PLUGINS_SUCCEEDED, + type: GET_AVAILABLE_PLUGINS_SUCCEEDED, availablePlugins, }; } +export function getInstalledPlugins() { + return { + type: GET_INSTALLED_PLUGINS, + }; +} + +export function getInstalledPluginsSucceeded(installedPlugins) { + return { + type: GET_INSTALLED_PLUGINS_SUCCEEDED, + installedPlugins, + }; +} + export function onChange({ target }) { return { type: ON_CHANGE, diff --git a/packages/strapi-admin/admin/src/containers/InstallPluginPage/constants.js b/packages/strapi-admin/admin/src/containers/InstallPluginPage/constants.js index 2141383d83..06650ea0e6 100644 --- a/packages/strapi-admin/admin/src/containers/InstallPluginPage/constants.js +++ b/packages/strapi-admin/admin/src/containers/InstallPluginPage/constants.js @@ -7,6 +7,8 @@ export const DOWNLOAD_PLUGIN = 'StrapiAdmin/InstallPluginPage/DOWNLOAD_PLUGIN'; export const DOWNLOAD_PLUGIN_ERROR = 'StrapiAdmin/InstallPluginPage/DOWNLOAD_PLUGIN_ERROR'; export const DOWNLOAD_PLUGIN_SUCCEEDED = 'StrapiAdmin/InstallPluginPage/DOWNLOAD_PLUGIN_SUCCEEDED'; -export const GET_PLUGINS = 'StrapiAdmin/InstallPluginPage/GET_PLUGINS'; -export const GET_PLUGINS_SUCCEEDED = 'StrapiAdmin/InstallPluginPage/GET_PLUGINS_SUCCEEDED'; +export const GET_AVAILABLE_PLUGINS = 'StrapiAdmin/InstallPluginPage/GET_AVAILABLE_PLUGINS'; +export const GET_AVAILABLE_PLUGINS_SUCCEEDED = 'StrapiAdmin/InstallPluginPage/GET_AVAILABLE_PLUGINS_SUCCEEDED'; +export const GET_INSTALLED_PLUGINS = 'StrapiAdmin/InstallPluginPage/GET_INSTALLED_PLUGINS'; +export const GET_INSTALLED_PLUGINS_SUCCEEDED = 'StrapiAdmin/InstallPluginPage/GET_INSTALLED_PLUGINS_SUCCEEDED'; export const ON_CHANGE = 'StrapiAdmin/InstallPluginPage/ON_CHANGE'; diff --git a/packages/strapi-admin/admin/src/containers/InstallPluginPage/index.js b/packages/strapi-admin/admin/src/containers/InstallPluginPage/index.js index 49634bb882..c1c4016891 100644 --- a/packages/strapi-admin/admin/src/containers/InstallPluginPage/index.js +++ b/packages/strapi-admin/admin/src/containers/InstallPluginPage/index.js @@ -11,7 +11,7 @@ import { Helmet } from 'react-helmet'; import { FormattedMessage } from 'react-intl'; import { bindActionCreators, compose } from 'redux'; import cn from 'classnames'; -import { get, isUndefined, map } from 'lodash'; +import { map } from 'lodash'; import { disableGlobalOverlayBlocker, @@ -32,7 +32,8 @@ import injectReducer from 'utils/injectReducer'; import { downloadPlugin, - getPlugins, + getAvailablePlugins, + getInstalledPlugins, onChange, } from './actions'; @@ -55,8 +56,11 @@ export class InstallPluginPage extends React.Component { // eslint-disable-line // Don't fetch the available plugins if it has already been done if (!this.props.didFetchPlugins) { - this.props.getPlugins(); + this.props.getAvailablePlugins(); } + + // Get installed plugins + this.props.getInstalledPlugins(); } componentWillUnmount() { @@ -65,10 +69,10 @@ export class InstallPluginPage extends React.Component { // eslint-disable-line } render() { - if (!this.props.didFetchPlugins) { + if (!this.props.didFetchPlugins || !this.props.didFetchInstalledPlugins) { return ; } - + return (
@@ -112,7 +116,7 @@ export class InstallPluginPage extends React.Component { // eslint-disable-line key={plugin.id} plugin={plugin} showSupportUsButton={plugin.id === 'support-us'} - isAlreadyInstalled={!isUndefined(get(this.context.plugins.toJS(), plugin.id))} + isAlreadyInstalled={this.props.installedPlugins.includes(plugin.id)} downloadPlugin={(e) => { e.preventDefault(); e.stopPropagation(); @@ -134,19 +138,18 @@ InstallPluginPage.childContextTypes = { downloadPlugin: PropTypes.func.isRequired, }; -InstallPluginPage.contextTypes = { - plugins: PropTypes.object.isRequired, -}; - InstallPluginPage.propTypes = { availablePlugins: PropTypes.array.isRequired, blockApp: PropTypes.bool.isRequired, + didFetchInstalledPlugins: PropTypes.bool.isRequired, didFetchPlugins: PropTypes.bool.isRequired, disableGlobalOverlayBlocker: PropTypes.func.isRequired, downloadPlugin: PropTypes.func.isRequired, enableGlobalOverlayBlocker: PropTypes.func.isRequired, - getPlugins: PropTypes.func.isRequired, + getAvailablePlugins: PropTypes.func.isRequired, + getInstalledPlugins: PropTypes.func.isRequired, history: PropTypes.object.isRequired, + installedPlugins: PropTypes.array.isRequired, // onChange: PropTypes.func.isRequired, // search: PropTypes.string.isRequired, }; @@ -159,7 +162,8 @@ function mapDispatchToProps(dispatch) { disableGlobalOverlayBlocker, downloadPlugin, enableGlobalOverlayBlocker, - getPlugins, + getAvailablePlugins, + getInstalledPlugins, onChange, }, dispatch, diff --git a/packages/strapi-admin/admin/src/containers/InstallPluginPage/reducer.js b/packages/strapi-admin/admin/src/containers/InstallPluginPage/reducer.js index cc70f6f36c..2f7f007b46 100644 --- a/packages/strapi-admin/admin/src/containers/InstallPluginPage/reducer.js +++ b/packages/strapi-admin/admin/src/containers/InstallPluginPage/reducer.js @@ -9,14 +9,17 @@ import { DOWNLOAD_PLUGIN, DOWNLOAD_PLUGIN_ERROR, DOWNLOAD_PLUGIN_SUCCEEDED, - GET_PLUGINS_SUCCEEDED, + GET_AVAILABLE_PLUGINS_SUCCEEDED, + GET_INSTALLED_PLUGINS_SUCCEEDED, ON_CHANGE, } from './constants'; const initialState = fromJS({ availablePlugins: List([]), + installedPlugins: List([]), blockApp: false, didFetchPlugins: false, + didFetchInstalledPlugins: false, pluginToDownload: '', search: '', }); @@ -35,10 +38,14 @@ function installPluginPageReducer(state = initialState, action) { return state .set('blockApp', false) .set('pluginToDownload', ''); - case GET_PLUGINS_SUCCEEDED: + case GET_AVAILABLE_PLUGINS_SUCCEEDED: return state .set('didFetchPlugins', true) .set('availablePlugins', List(action.availablePlugins)); + case GET_INSTALLED_PLUGINS_SUCCEEDED: + return state + .set('didFetchInstalledPlugins', true) + .set('installedPlugins', List(action.installedPlugins)); case ON_CHANGE: return state.updateIn(action.keys, () => action.value); default: diff --git a/packages/strapi-admin/admin/src/containers/InstallPluginPage/saga.js b/packages/strapi-admin/admin/src/containers/InstallPluginPage/saga.js index ee8c18c416..0a2384a6b0 100644 --- a/packages/strapi-admin/admin/src/containers/InstallPluginPage/saga.js +++ b/packages/strapi-admin/admin/src/containers/InstallPluginPage/saga.js @@ -15,9 +15,10 @@ import { selectLocale } from '../LanguageProvider/selectors'; import { downloadPluginError, downloadPluginSucceeded, - getPluginsSucceeded, + getAvailablePluginsSucceeded, + getInstalledPluginsSucceeded, } from './actions'; -import { DOWNLOAD_PLUGIN, GET_PLUGINS } from './constants'; +import { DOWNLOAD_PLUGIN, GET_AVAILABLE_PLUGINS, GET_INSTALLED_PLUGINS } from './constants'; import { makeSelectPluginToDownload } from './selectors'; @@ -49,7 +50,7 @@ export function* pluginDownload() { } } -export function* pluginsGet() { +export function* getAvailablePlugins() { try { // Get current locale. const locale = yield select(selectLocale()); @@ -73,20 +74,44 @@ export function* pluginsGet() { availablePlugins = []; } - yield put(getPluginsSucceeded(availablePlugins)); + yield put(getAvailablePluginsSucceeded(availablePlugins)); } catch(err) { strapi.notification.error('notification.error'); } } +export function* getInstalledPlugins() { + try { + const opts = { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }; + let installedPlugins; + + try { + // Retrieve plugins list. + installedPlugins = yield call(request, '/admin/plugins', opts); + } catch (e) { + installedPlugins = []; + } + + yield put(getInstalledPluginsSucceeded(Object.keys(installedPlugins.plugins))); + } catch(err) { + strapi.notification.error('notification.error'); + } +} // Individual exports for testing export default function* defaultSaga() { - const loadPluginsWatcher = yield fork(takeLatest, GET_PLUGINS, pluginsGet); + const loadAvailablePluginsWatcher = yield fork(takeLatest, GET_AVAILABLE_PLUGINS, getAvailablePlugins); + const loadInstalledPluginsWatcher = yield fork(takeLatest, GET_INSTALLED_PLUGINS, getInstalledPlugins); yield fork(takeLatest, DOWNLOAD_PLUGIN, pluginDownload); yield take(LOCATION_CHANGE); - yield cancel(loadPluginsWatcher); + yield cancel(loadAvailablePluginsWatcher); + yield cancel(loadInstalledPluginsWatcher); } diff --git a/packages/strapi-generate-api/lib/before.js b/packages/strapi-generate-api/lib/before.js index 6f0900b41b..dd1d8892bb 100755 --- a/packages/strapi-generate-api/lib/before.js +++ b/packages/strapi-generate-api/lib/before.js @@ -39,7 +39,7 @@ module.exports = (scope, cb) => { idPluralized: pluralize.plural(_.trim(_.camelCase(scope.id))), parentId: _.isEmpty(parent) ? undefined : _.trim(_.deburr(parent)), parentIdPluralized: _.isEmpty(scope.parentId) ? undefined : pluralize.plural(_.trim(_.camelCase(scope.parentId))), - environment: process.NODE_ENV || 'development' + environment: process.env.NODE_ENV || 'development' }); // Determine default values based on the available scope. diff --git a/packages/strapi-generate-model/lib/before.js b/packages/strapi-generate-model/lib/before.js index 4715422d01..13e4525ba5 100755 --- a/packages/strapi-generate-model/lib/before.js +++ b/packages/strapi-generate-model/lib/before.js @@ -33,7 +33,7 @@ module.exports = (scope, cb) => { _.defaults(scope, { id: _.trim(_.deburr(scope.id)), idPluralized: pluralize.plural(_.trim(_.deburr(scope.id))), - environment: process.NODE_ENV || 'development' + environment: process.env.NODE_ENV || 'development' }); // Determine default values based on the available scope. diff --git a/packages/strapi-generate-new/lib/after.js b/packages/strapi-generate-new/lib/after.js index f704dc6d5e..17a8e38092 100755 --- a/packages/strapi-generate-new/lib/after.js +++ b/packages/strapi-generate-new/lib/after.js @@ -187,10 +187,10 @@ module.exports = (scope, cb) => { console.log(); console.log(`👌 Your new application ${green(scope.name)} is ready at ${cyan(scope.rootPath)}.`); console.log(); - console.log('⚡️ change directory:'); + console.log('⚡️ Change directory:'); console.log(`$ ${green(`cd ${scope.name}`)}`); console.log(); - console.log('⚡️ start application:'); + console.log('⚡️ Start application:'); console.log(`$ ${green('strapi start')}`); cb(); diff --git a/packages/strapi-generate-new/lib/before.js b/packages/strapi-generate-new/lib/before.js index 4182643982..411063f82e 100755 --- a/packages/strapi-generate-new/lib/before.js +++ b/packages/strapi-generate-new/lib/before.js @@ -132,7 +132,7 @@ module.exports = (scope, cb) => { type: 'input', name: 'database', message: 'Database name:', - default: _.get(scope.database, 'database', 'strapi') + default: _.get(scope.database, 'database', scope.name) }, { when: !hasDatabaseConfig, diff --git a/packages/strapi-helper-plugin/lib/src/components/Button/styles.scss b/packages/strapi-helper-plugin/lib/src/components/Button/styles.scss index 495b33d2d6..1d52efe957 100755 --- a/packages/strapi-helper-plugin/lib/src/components/Button/styles.scss +++ b/packages/strapi-helper-plugin/lib/src/components/Button/styles.scss @@ -26,6 +26,7 @@ height: 3rem; position: relative; border-radius: 0.3rem; + white-space: nowrap; margin-right: 1.8rem; cursor: pointer; font-family: Lato; diff --git a/packages/strapi-hook-bookshelf/lib/index.js b/packages/strapi-hook-bookshelf/lib/index.js index f91837dcc3..ff0d8fc8d0 100755 --- a/packages/strapi-hook-bookshelf/lib/index.js +++ b/packages/strapi-hook-bookshelf/lib/index.js @@ -26,9 +26,6 @@ const GLOBALS = {}; * Bookshelf hook */ -/* eslint-disable no-unused-vars */ -/* eslint-disable prefer-template */ -/* eslint-disable no-case-declarations */ module.exports = function(strapi) { const hook = _.merge({ /** @@ -79,17 +76,16 @@ module.exports = function(strapi) { _.forEach(models, (definition, model) => { definition.globalName = _.upperFirst(_.camelCase(definition.globalId)); - _.defaults(definition, { - primaryKey: 'id' - }); - // Define local GLOBALS to expose every models in this file. GLOBALS[definition.globalId] = {}; // Add some informations about ORM & client connection & tableName definition.orm = 'bookshelf'; definition.client = _.get(connection.settings, 'client'); - + _.defaults(definition, { + primaryKey: 'id', + primaryKeyType: _.get(definition, 'options.idAttributeType', 'integer') + }); // Register the final model for Bookshelf. const loadedModel = _.assign({ tableName: definition.collectionName, @@ -287,7 +283,7 @@ module.exports = function(strapi) { : Promise.resolve(); }); - this.on('saving', (instance, attrs) => { + this.on('saving', (instance, attrs, options) => { //eslint-disable-line instance.attributes = mapper(instance.attributes); attrs = mapper(attrs); @@ -394,13 +390,16 @@ module.exports = function(strapi) { case 'oneToOne': case 'manyToOne': case 'oneWay': - type = definition.client === 'pg' ? 'integer' : 'int'; + type = definition.primaryKeyType; break; default: return null; } } else { switch (attribute.type) { + case 'uuid': + type = definition.client === 'pg' ? 'uuid' : 'varchar(36)'; + break; case 'text': type = definition.client === 'pg' ? type = 'text' : 'longtext'; break; @@ -521,7 +520,13 @@ module.exports = function(strapi) { if (!tableExist) { - const columns = generateColumns(attributes, [`id ${definition.client === 'pg' ? 'SERIAL' : 'INT AUTO_INCREMENT'} NOT NULL PRIMARY KEY`]).join(',\n\r'); + let idAttributeBuilder = [`id ${definition.client === 'pg' ? 'SERIAL' : 'INT AUTO_INCREMENT'} NOT NULL PRIMARY KEY`]; + if (definition.primaryKeyType === 'uuid' && definition.client === 'pg') { + idAttributeBuilder = ['id uuid NOT NULL DEFAULT uuid_generate_v4() NOT NULL PRIMARY KEY']; + } else if (definition.primaryKeyType !== 'integer') { + idAttributeBuilder = [`id ${getType({type: definition.primaryKeyType})} NOT NULL PRIMARY KEY`]; + } + const columns = generateColumns(attributes, idAttributeBuilder).join(',\n\r'); // Create table await ORM.knex.raw(` @@ -593,7 +598,6 @@ module.exports = function(strapi) { const changeRequired = definition.client === 'pg' ? `ALTER COLUMN ${quote}${attribute}${quote} ${attributes[attribute].required ? 'SET' : 'DROP'} NOT NULL` : `CHANGE ${quote}${attribute}${quote} ${quote}${attribute}${quote} ${type} ${attributes[attribute].required ? 'NOT' : ''} NULL`; - await ORM.knex.raw(`ALTER TABLE ${quote}${table}${quote} ${changeType}`); await ORM.knex.raw(`ALTER TABLE ${quote}${table}${quote} ${changeRequired}`); } @@ -631,10 +635,10 @@ module.exports = function(strapi) { if (morphRelations) { const attributes = { [`${loadedModel.tableName}_id`]: { - type: 'integer' + type: definition.primaryKeyType }, [`${morphRelations.alias}_id`]: { - type: 'integer' + type: definition.primaryKeyType }, [`${morphRelations.alias}_type`]: { type: 'text' @@ -661,10 +665,10 @@ module.exports = function(strapi) { const attributes = { [`${pluralize.singular(manyRelations.collection)}_id`]: { - type: 'integer' + type: definition.primaryKeyType }, [`${pluralize.singular(definition.globalId.toLowerCase())}_id`]: { - type: 'integer' + type: definition.primaryKeyType } }; @@ -999,7 +1003,7 @@ module.exports = function(strapi) { cb(); }, - getQueryParams: (value, type, key) => { + getQueryParams: (value, type, key) =>{ const result = {}; switch (type) { @@ -1046,18 +1050,18 @@ module.exports = function(strapi) { }; break; case '_sort': - result.key = `sort`; + result.key = 'sort'; result.value = { key, order: value.toUpperCase() }; 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': diff --git a/packages/strapi-plugin-content-type-builder/admin/src/containers/Form/forms.json b/packages/strapi-plugin-content-type-builder/admin/src/containers/Form/forms.json index 13c8467ec6..af00853d4e 100644 --- a/packages/strapi-plugin-content-type-builder/admin/src/containers/Form/forms.json +++ b/packages/strapi-plugin-content-type-builder/admin/src/containers/Form/forms.json @@ -2,18 +2,6 @@ "contentType": { "baseSettings": { "items": [ - { - "label": { - "id": "content-type-builder.form.contentType.item.connections" - }, - "name": "connection", - "type": "select", - "value": "default", - "items": [{}], - "validations": { - "required": true - } - }, { "label": { "id": "content-type-builder.form.contentType.item.name" @@ -45,6 +33,18 @@ } } }, + { + "label": { + "id": "content-type-builder.form.contentType.item.connections" + }, + "name": "connection", + "type": "select", + "value": "default", + "items": [{}], + "validations": { + "required": true + } + }, { "label": { "id": "content-type-builder.form.contentType.item.description" diff --git a/packages/strapi-plugin-content-type-builder/admin/src/containers/Form/index.js b/packages/strapi-plugin-content-type-builder/admin/src/containers/Form/index.js index c0513dfa95..390b17bf66 100644 --- a/packages/strapi-plugin-content-type-builder/admin/src/containers/Form/index.js +++ b/packages/strapi-plugin-content-type-builder/admin/src/containers/Form/index.js @@ -514,12 +514,9 @@ export class Form extends React.Component { // eslint-disable-line react/prefer- } renderModalBodyChooseAttributes = () => { - const attributesDisplay = forms.attributesDisplay.items; - - // Don't display the media field if the upload plugin isn't installed - if (!has(this.context.plugins.toJS(), 'upload')) { - attributesDisplay.splice(8, 1); - } + const attributesDisplay = has(this.context.plugins.toJS(), 'upload') + ? forms.attributesDisplay.items + : forms.attributesDisplay.items.filter(obj => obj.type !== 'media'); // Don't display the media field if the upload plugin isn't installed return ( map(attributesDisplay, (attribute, key) => ( diff --git a/packages/strapi-plugin-graphql/package.json b/packages/strapi-plugin-graphql/package.json index 67b9741144..33960c0271 100644 --- a/packages/strapi-plugin-graphql/package.json +++ b/packages/strapi-plugin-graphql/package.json @@ -49,4 +49,4 @@ "npm": ">= 5.3.0" }, "license": "MIT" -} \ No newline at end of file +} diff --git a/packages/strapi-plugin-graphql/services/GraphQL.js b/packages/strapi-plugin-graphql/services/GraphQL.js index eb911fae2b..0abdb64c28 100644 --- a/packages/strapi-plugin-graphql/services/GraphQL.js +++ b/packages/strapi-plugin-graphql/services/GraphQL.js @@ -18,6 +18,445 @@ const GraphQLDateTime = require('graphql-type-datetime'); const policyUtils = require('strapi-utils').policy; module.exports = { + /** + * Returns all fields of type primitive + * + * @returns {Boolean} + */ + isPrimitiveType: (_type) => { + const type = _type.replace('!', ''); + return ( + type === 'Int' || + type === 'Float' || + type === 'String' || + type === 'Boolean' || + type === 'DateTime' || + type === 'JSON' + ); + }, + + /** + * Checks if the field is of type enum + * + * @returns {Boolean} + */ + isEnumType: (type) => { + return type === 'enumeration'; + }, + + /** + * Returns all fields that are not of type array + * + * @returns {Boolean} + * + * @example + * + * isNotOfTypeArray([String]) + * // => false + * isNotOfTypeArray(String!) + * // => true + */ + isNotOfTypeArray: (type) => { + return !/(\[\w+!?\])/.test(type); + }, + + /** + * Returns all fields of type Integer or float + */ + isNumberType: (type) => { + return type === 'Int' || type === 'Float'; + }, + + /** + * Convert non-primitive type to string (non-primitive types corresponds to a reference to an other model) + * + * @returns {String} + * + * @example + * + * extractType(String!) + * // => String + * + * extractType(user) + * // => ID + * + * extractType(ENUM_TEST_FIELD, enumeration) + * // => String + * + */ + extractType: function (_type, attributeType) { + return this.isPrimitiveType(_type) + ? _type.replace('!', '') + : this.isEnumType(attributeType) + ? 'String' + : 'ID'; + }, + + /** + * Returns a list of fields that have type included in fieldTypes. + */ + getFieldsByTypes: (fields, typeCheck, returnType) => { + return _.reduce(fields, (acc, fieldType, fieldName) => { + if (typeCheck(fieldType)) { + acc[fieldName] = returnType(fieldType, fieldName); + } + return acc; + }, {}); + }, + + /** + * Use the field resolver otherwise fall through the field value + * + * @returns {function} + */ + fieldResolver: (field, key) => { + return (object) => { + const resolver = field.resolve || function resolver(obj, options, context) { // eslint-disable-line no-unused-vars + return obj[key]; + }; + return resolver(object); + }; + }, + + /** + * Create fields resolvers + * + * @return {Object} + */ + createFieldsResolver: function(fields, resolver, typeCheck) { + return Object.keys(fields).reduce((acc, fieldKey) => { + const field = fields[fieldKey]; + // Check if the field is of the correct type + if (typeCheck(field)) { + return _.set(acc, fieldKey, (obj, options, context) => { + return resolver(obj, options, context, this.fieldResolver(field, fieldKey), fieldKey, obj, field); + }); + } + return acc; + }, {}); + }, + + /** + * Build the mongoose aggregator by applying the filters + */ + getModelAggregator: function (model, filters = {}) { + const aggregation = model.aggregate(); + if (!_.isEmpty(filters.where)) { + aggregation.match(filters.where); + } + if (filters.limit) { + aggregation.limit(filters.limit); + } + return aggregation; + }, + + /** + * Create the resolvers for each aggregation field + * + * @return {Object} + * + * @example + * + * const model = // Strapi model + * + * const fields = { + * username: String, + * age: Int, + * } + * + * const typeCheck = (type) => type === 'Int' || type === 'Float', + * + * const fieldsResoler = createAggregationFieldsResolver(model, fields, 'sum', typeCheck); + * + * // => { + * age: function ageResolver() { .... } + * } + */ + createAggregationFieldsResolver: function (model, fields, operation, typeCheck) { + return this.createFieldsResolver(fields, async (obj, options, context, fieldResolver, fieldKey) => { // eslint-disable-line no-unused-vars + const result = await this.getModelAggregator(model, obj).group({ + _id: null, + [fieldKey]: { [`$${operation}`]: `$${fieldKey}` } + }); + return _.get(result, `0.${fieldKey}`); + }, typeCheck); + }, + + /** + * Correctly format the data returned by the group by + */ + preProcessGroupByData: function ({ result, fieldKey, filters, modelName }) { + const _result = _.toArray(result); + return _.map(_result, (value) => { + const params = Object.assign( + {}, + this.convertToParams(_.omit(filters, 'where')), + filters.where, + { + [fieldKey]: value._id, + } + ); + + return { + key: value._id, + connection: strapi.utils.models.convertParams(modelName, params), + }; + }); + }, + + /** + * Create the resolvers for each group by field + * + * @return {Object} + * + * @example + * + * const model = // Strapi model + * const fields = { + * username: [UserConnectionUsername], + * email: [UserConnectionEmail], + * } + * const fieldsResoler = createGroupByFieldsResolver(model, fields); + * + * // => { + * username: function usernameResolver() { .... } + * email: function emailResolver() { .... } + * } + */ + createGroupByFieldsResolver: function (model, fields, name) { + return this.createFieldsResolver(fields, async (obj, options, context, fieldResolver, fieldKey) => { + const result = await this.getModelAggregator(model, obj).group({ + _id: `$${fieldKey}`, + }); + + return this.preProcessGroupByData({ + result, + fieldKey, + filters: obj, + modelName: name, + }); + }, () => true); + }, + + /** + * This method is the entry point to the GraphQL's Aggregation. + * It takes as param the model and its fields and it'll create the aggregation types and resolver to it + * Example: + * type User { + * username: String, + * age: Int, + * } + * + * It'll create + * type UserConnection { + * values: [User], + * groupBy: UserGroupBy, + * aggreate: UserAggregate + * } + * + * type UserAggregate { + * count: Int + * sum: UserAggregateSum + * avg: UserAggregateAvg + * } + * + * type UserAggregateSum { + * age: Float + * } + * + * type UserAggregateAvg { + * age: Float + * } + * + * type UserGroupBy { + * username: [UserConnectionUsername] + * age: [UserConnectionAge] + * } + * + * type UserConnectionUsername { + * key: String + * connection: UserConnection + * } + * + * type UserConnectionAge { + * key: Int + * connection: UserConnection + * } + * + */ + formatModelConnectionsGQL: function(fields, model, name, modelResolver) { + const { globalId } = model; + + const connectionGlobalId = `${globalId}Connection`; + const aggregatorFormat = this.formatConnectionAggregator(fields, model, name); + const groupByFormat = this.formatConnectionGroupBy(fields, model, name); + const connectionFields = { + values: `[${globalId}]`, + groupBy: `${globalId}GroupBy`, + aggregate: `${globalId}Aggregator`, + }; + + let modelConnectionTypes = `type ${connectionGlobalId} {${this.formatGQL(connectionFields)}}\n\n`; + if (aggregatorFormat) { + modelConnectionTypes += aggregatorFormat.type; + } + modelConnectionTypes += groupByFormat.type; + + return { + globalId: connectionGlobalId, + type: modelConnectionTypes, + query: { + [`${pluralize.plural(name)}Connection(sort: String, limit: Int, start: Int, where: JSON)`]: connectionGlobalId, + }, + resolver: { + Query: { + [`${pluralize.plural(name)}Connection`]: (obj, options, context) => { // eslint-disable-line no-unused-vars + const params = Object.assign( + {}, + this.convertToParams(_.omit(options, 'where')), + options.where + ); + return strapi.utils.models.convertParams(name, params); + } + }, + [connectionGlobalId]: { + values: (obj, option, context) => { + // Object here contains the key/value of the field that has been grouped-by + // for instance obj = { where: { country: 'USA' } } so the values here needs to be filtered according to the parent value + return modelResolver(obj, obj, context); + }, + groupBy: (obj, option, context) => { // eslint-disable-line no-unused-vars + // There is noting to resolve here, it's the aggregation resolver that will take care of it + return obj; + }, + aggregate: (obj, option, context) => { // eslint-disable-line no-unused-vars + // There is noting to resolve here, it's the aggregation resolver that will take care of it + return obj; + }, + }, + ...aggregatorFormat.resolver, + ...groupByFormat.resolver, + }, + }; + }, + + /** + * Generate the connection type of each non-array field of the model + * + * @return {String} + */ + generateConnectionFieldsTypes: function (fields, model) { + const { globalId, attributes } = model; + const primitiveFields = this.getFieldsByTypes( + fields, + this.isNotOfTypeArray, + (type, name) => this.extractType(type, (attributes[name] || {}).type), + ); + + const connectionFields = _.mapValues(primitiveFields, (fieldType) => ({ + key: fieldType, + connection: `${globalId}Connection`, + })); + + return Object.keys(primitiveFields).map((fieldKey) => + `type ${globalId}Connection${_.upperFirst(fieldKey)} {${this.formatGQL(connectionFields[fieldKey])}}` + ).join('\n\n'); + }, + + formatConnectionGroupBy: function(fields, model, name) { + const { globalId } = model; + const groupByGlobalId = `${globalId}GroupBy`; + + // Extract all primitive fields and change their types + const groupByFields = this.getFieldsByTypes( + fields, + this.isNotOfTypeArray, + (fieldType, fieldName) => `[${globalId}Connection${_.upperFirst(fieldName)}]`, + ); + + // Get the generated field types + let groupByTypes = `type ${groupByGlobalId} {${this.formatGQL(groupByFields)}}\n\n`; + groupByTypes += this.generateConnectionFieldsTypes(fields, model); + + return { + globalId: groupByGlobalId, + type: groupByTypes, + resolver: { + [groupByGlobalId]: this.createGroupByFieldsResolver(model, groupByFields, name), + } + }; + }, + + formatConnectionAggregator: function(fields, model) { + const { globalId } = model; + + // Extract all fields of type Integer and Float and change their type to Float + const numericFields = this.getFieldsByTypes(fields, this.isNumberType, () => 'Float'); + + // Don't create an aggregator field if the model has not number fields + const aggregatorGlobalId = `${globalId}Aggregator`; + const initialFields = { + count: 'Int', + }; + + // Only add the aggregator's operations if there are some numeric fields + if (!_.isEmpty(numericFields)) { + ['sum', 'avg', 'min', 'max'].forEach((agg) => { + initialFields[agg] = `${aggregatorGlobalId}${_.startCase(agg)}`; + }); + } + + const gqlNumberFormat = this.formatGQL(numericFields); + let aggregatorTypes = `type ${aggregatorGlobalId} {${this.formatGQL(initialFields)}}\n\n`; + + let resolvers = { + [aggregatorGlobalId]: { + count: async (obj, options, context) => { // eslint-disable-line no-unused-vars + // Object here corresponds to the filter that needs to be applied to the aggregation + const result = await this.getModelAggregator(model, obj).group({ + _id: null, + count: { $sum: 1 } + }); + + return _.get(result, '0.count'); + }, + } + }; + + // Only add the aggregator's operations types and resolver if there are some numeric fields + if (!_.isEmpty(numericFields)) { + // Returns the actual object and handle aggregation in the query resolvers + const defaultAggregatorFunc = (obj, options, context) => { // eslint-disable-line no-unused-vars + return obj; + }; + + aggregatorTypes += `type ${aggregatorGlobalId}Sum {${gqlNumberFormat}}\n\n`; + aggregatorTypes += `type ${aggregatorGlobalId}Avg {${gqlNumberFormat}}\n\n`; + aggregatorTypes += `type ${aggregatorGlobalId}Min {${gqlNumberFormat}}\n\n`; + aggregatorTypes += `type ${aggregatorGlobalId}Max {${gqlNumberFormat}}\n\n`; + + _.merge(resolvers[aggregatorGlobalId], { + sum: defaultAggregatorFunc, + avg: defaultAggregatorFunc, + min: defaultAggregatorFunc, + max: defaultAggregatorFunc, + }); + + resolvers = { + ...resolvers, + [`${aggregatorGlobalId}Sum`]: this.createAggregationFieldsResolver(model, fields, 'sum', this.isNumberType), + [`${aggregatorGlobalId}Avg`]: this.createAggregationFieldsResolver(model, fields, 'avg', this.isNumberType), + [`${aggregatorGlobalId}Min`]: this.createAggregationFieldsResolver(model, fields, 'min', this.isNumberType), + [`${aggregatorGlobalId}Max`]: this.createAggregationFieldsResolver(model, fields, 'max', this.isNumberType) + }; + } + + return { + globalId: aggregatorGlobalId, + type: aggregatorTypes, + resolver: resolvers, + }; + }, /** * Receive an Object and return a string which is following the GraphQL specs. @@ -338,13 +777,16 @@ module.exports = { // Plural. return async (ctx, next) => { - ctx.params = this.amountLimiting(ctx.params); - ctx.query = Object.assign( + const queryOpts = {}; + queryOpts.params = this.amountLimiting(ctx.params); + // Avoid using ctx.query = ... because it converts the object values to string + queryOpts.query = Object.assign( + {}, this.convertToParams(_.omit(ctx.params, 'where')), ctx.params.where ); - return controller(ctx, next); + return controller(Object.assign({}, ctx, queryOpts, { send: ctx.send }), next); // send method doesn't get copied when using object.assign }; })(); @@ -561,6 +1003,22 @@ module.exports = { } }); + // TODO: + // - Add support for Graphql Aggregation in Bookshelf ORM + if (model.orm === 'mongoose') { + // Generation the aggregation for the given model + const modelAggregator = this.formatModelConnectionsGQL(attributes, model, name, queries.plural); + if (modelAggregator) { + acc.definition += modelAggregator.type; + if (!acc.resolver[modelAggregator.globalId]) { + acc.resolver[modelAggregator.globalId] = {}; + } + + _.merge(acc.resolver, modelAggregator.resolver); + _.merge(acc.query, modelAggregator.query); + } + } + // Build associations queries. (model.associations || []).forEach(association => { switch (association.nature) { @@ -630,7 +1088,8 @@ module.exports = { }; if (association.type === 'model') { - params.id = obj[association.alias]; + const rel = obj[association.alias]; + params.id = typeof rel === 'object' && 'id' in rel ? rel.id : rel; } else { // Get refering model. const ref = association.plugin ? diff --git a/packages/strapi-plugin-users-permissions/admin/src/containers/AuthPage/actions.js b/packages/strapi-plugin-users-permissions/admin/src/containers/AuthPage/actions.js index 53d0a91bce..b9e70f52be 100644 --- a/packages/strapi-plugin-users-permissions/admin/src/containers/AuthPage/actions.js +++ b/packages/strapi-plugin-users-permissions/admin/src/containers/AuthPage/actions.js @@ -50,7 +50,7 @@ export function setForm(formType, email) { data = { identifier: '', password: '', - rememberMe: false, + rememberMe: true, }; break; @@ -60,7 +60,7 @@ export function setForm(formType, email) { password: '', confirmPassword: '', email: '', - news: true, + news: false, }; break; case 'register-success': diff --git a/packages/strapi-plugin-users-permissions/admin/src/containers/AuthPage/form.json b/packages/strapi-plugin-users-permissions/admin/src/containers/AuthPage/form.json index 075de9f189..08a23dbdc2 100644 --- a/packages/strapi-plugin-users-permissions/admin/src/containers/AuthPage/form.json +++ b/packages/strapi-plugin-users-permissions/admin/src/containers/AuthPage/form.json @@ -80,7 +80,7 @@ }, "name": "news", "type": "checkbox", - "value": true + "value": false } ], "register-success": [ diff --git a/packages/strapi-plugin-users-permissions/controllers/UsersPermissions.js b/packages/strapi-plugin-users-permissions/controllers/UsersPermissions.js index bc8bec8133..d3a13ebfa0 100644 --- a/packages/strapi-plugin-users-permissions/controllers/UsersPermissions.js +++ b/packages/strapi-plugin-users-permissions/controllers/UsersPermissions.js @@ -186,7 +186,7 @@ module.exports = { type: 'plugin', name: 'users-permissions', key: 'email' - }).set({value: ctx.request.body}); + }).set({value: ctx.request.body['email-templates']}); ctx.send({ ok: true }); },