Merge branch 'master' of github.com:strapi/strapi into content-manager-filters

This commit is contained in:
soupette 2018-06-05 12:06:17 +02:00
commit 8302d32d71
24 changed files with 1553 additions and 265 deletions

View File

@ -10,7 +10,8 @@
"commonjs": true,
"es6": true,
"node": true,
"mocha": true
"mocha": true,
"jest": true
},
"parserOptions": {
"ecmaFeatures": {

View File

@ -6,7 +6,7 @@ Installation is very easy and only takes a few seconds.
Please make sure your computer/server meets the following requirements:
- [Node.js](https://nodejs.org) >= 9: Node.js is a server platform which runs JavaScript. Installation guide [here](https://nodejs.org/en/download/).
- [MongoDB](https://www.mongodb.com/): MongoDB is a powerful document store. Installation guide [here](https://www.mongodb.com/download-center?j#community).
- [MongoDB](https://www.mongodb.com/) >= 2.6: MongoDB is a powerful document store. Installation guide [here](https://www.mongodb.com/download-center?j#community).
## Setup

View File

@ -3,6 +3,7 @@
"version": "3.0.0-alpha.12.2",
"devDependencies": {
"assert": "~1.3.0",
"axios": "^0.18.0",
"babel-eslint": "^6.1.2",
"chalk": "^2.4.1",
"eslint": "^3.12.2",
@ -11,11 +12,14 @@
"eslint-plugin-redux-saga": "^0.3.0",
"gitbook-cli": "^2.3.2",
"istanbul": "~0.4.2",
"jest": "^22.4.3",
"jest-cli": "^22.4.4",
"lerna": "^2.0.0",
"lodash": "^4.17.5",
"mocha": "~2.4.5",
"pre-commit": "~1.1.2",
"redux-saga": "^0.14.3",
"request": "^2.87.0",
"shelljs": "^0.7.7",
"strapi-lint": "file:packages/strapi-lint"
},
@ -29,7 +33,7 @@
"setup:build": "npm run setup --build",
"setup": "npm run clean:all && npm install ./packages/strapi-lint --save-dev && npm install && node ./scripts/setup.js && npm run clean",
"lint": "node ./scripts/lint.js",
"test": "echo \"Error: no test specified yet...\"",
"test": "node ./test/start.js",
"prettier": "node ./packages/strapi-lint/lib/internals/prettier/index.js"
},
"author": {

View File

@ -19,10 +19,7 @@ import { get, includes, isFunction, map, omit } from 'lodash';
import { compose } from 'redux';
// Actions required for disabling and enabling the OverlayBlocker
import {
disableGlobalOverlayBlocker,
enableGlobalOverlayBlocker,
} from 'actions/overlayBlocker';
import { disableGlobalOverlayBlocker, enableGlobalOverlayBlocker } from 'actions/overlayBlocker';
import { pluginLoaded, updatePlugin } from 'containers/App/actions';
import {
@ -63,17 +60,16 @@ import styles from './styles.scss';
const PLUGINS_TO_BLOCK_PRODUCTION = ['content-type-builder', 'settings-manager'];
export class AdminPage extends React.Component { // eslint-disable-line react/prefer-stateless-function
export class AdminPage extends React.Component {
// eslint-disable-line react/prefer-stateless-function
state = { hasAlreadyRegistereOtherPlugins: false };
getChildContext = () => (
{
disableGlobalOverlayBlocker: this.props.disableGlobalOverlayBlocker,
enableGlobalOverlayBlocker: this.props.enableGlobalOverlayBlocker,
plugins: this.props.plugins,
updatePlugin: this.props.updatePlugin,
}
);
getChildContext = () => ({
disableGlobalOverlayBlocker: this.props.disableGlobalOverlayBlocker,
enableGlobalOverlayBlocker: this.props.enableGlobalOverlayBlocker,
plugins: this.props.plugins,
updatePlugin: this.props.updatePlugin,
});
componentDidMount() {
this.checkLogin(this.props);
@ -91,7 +87,10 @@ export class AdminPage extends React.Component { // eslint-disable-line react/pr
}
}
if (get(nextProps.plugins.toJS(), ['users-permissions', 'hasAdminUser']) !== get(this.props.plugins.toJS(), ['users-permissions', 'hasAdminUser'])) {
if (
get(nextProps.plugins.toJS(), ['users-permissions', 'hasAdminUser']) !==
get(this.props.plugins.toJS(), ['users-permissions', 'hasAdminUser'])
) {
this.checkLogin(nextProps, true);
}
@ -106,23 +105,34 @@ export class AdminPage extends React.Component { // eslint-disable-line react/pr
return;
}
const endPoint = this.hasAdminUser(props) ? 'login': 'register';
const endPoint = this.hasAdminUser(props) ? 'login' : 'register';
this.props.history.push(`/plugins/users-permissions/auth/${endPoint}`);
}
if (!this.isUrlProtected(props) && includes(props.location.pathname, 'auth/register') && this.hasAdminUser(props) && !skipAction) {
if (
!this.isUrlProtected(props) &&
includes(props.location.pathname, 'auth/register') &&
this.hasAdminUser(props) &&
!skipAction
) {
this.props.history.push('/plugins/users-permissions/auth/login');
}
if (props.hasUserPlugin && !this.isUrlProtected(props) && !includes(props.location.pathname, 'auth/register') && !this.hasAdminUser(props)) {
if (
props.hasUserPlugin &&
!this.isUrlProtected(props) &&
!includes(props.location.pathname, 'auth/register') &&
!this.hasAdminUser(props)
) {
this.props.history.push('/plugins/users-permissions/auth/register');
}
if (!props.hasUserPlugin || auth.getToken() && !this.state.hasAlreadyRegistereOtherPlugins) {
if (!props.hasUserPlugin || (auth.getToken() && !this.state.hasAlreadyRegistereOtherPlugins)) {
map(omit(this.props.plugins.toJS(), ['users-permissions', 'email']), plugin => {
switch (true) {
case isFunction(plugin.bootstrap) && isFunction(plugin.pluginRequirements):
plugin.pluginRequirements(plugin)
plugin
.pluginRequirements(plugin)
.then(plugin => {
return plugin.bootstrap(plugin);
})
@ -140,31 +150,35 @@ export class AdminPage extends React.Component { // eslint-disable-line react/pr
this.setState({ hasAlreadyRegistereOtherPlugins: true });
}
}
};
hasUserPluginLoaded = (props) => typeof get(props.plugins.toJS(), ['users-permissions', 'hasAdminUser']) !== 'undefined';
hasUserPluginLoaded = props =>
typeof get(props.plugins.toJS(), ['users-permissions', 'hasAdminUser']) !== 'undefined';
hasAdminUser = (props) => get(props.plugins.toJS(), ['users-permissions', 'hasAdminUser']);
hasAdminUser = props => get(props.plugins.toJS(), ['users-permissions', 'hasAdminUser']);
isUrlProtected = (props) => !includes(props.location.pathname, get(props.plugins.toJS(), ['users-permissions', 'nonProtectedUrl']));
isUrlProtected = props =>
!includes(props.location.pathname, get(props.plugins.toJS(), ['users-permissions', 'nonProtectedUrl']));
shouldDisplayLogout = () => auth.getToken() && this.props.hasUserPlugin && this.isUrlProtected(this.props);
showLeftMenu = () => !includes(this.props.location.pathname, 'users-permissions/auth/');
retrievePlugins = () => {
const { adminPage: { currentEnvironment }, plugins } = this.props;
const {
adminPage: { currentEnvironment },
plugins,
} = this.props;
if (currentEnvironment === 'production') {
let pluginsToDisplay = plugins;
PLUGINS_TO_BLOCK_PRODUCTION.map(plugin =>
pluginsToDisplay = pluginsToDisplay.delete(plugin));
PLUGINS_TO_BLOCK_PRODUCTION.map(plugin => (pluginsToDisplay = pluginsToDisplay.delete(plugin)));
return pluginsToDisplay;
}
return plugins;
}
};
render() {
const { adminPage } = this.props;
@ -247,13 +261,27 @@ const mapStateToProps = createStructuredSelector({
function mapDispatchToProps(dispatch) {
return {
disableGlobalOverlayBlocker: () => { dispatch(disableGlobalOverlayBlocker()); },
enableGlobalOverlayBlocker: () => { dispatch(enableGlobalOverlayBlocker()); },
getGaStatus: () => { dispatch(getGaStatus()); },
getLayout: () => { dispatch(getLayout()); },
onHideNotification: (id) => { dispatch(hideNotification(id)); },
pluginLoaded: (plugin) => { dispatch(pluginLoaded(plugin)); },
updatePlugin: (pluginId, updatedKey, updatedValue) => { dispatch(updatePlugin(pluginId, updatedKey, updatedValue)); },
disableGlobalOverlayBlocker: () => {
dispatch(disableGlobalOverlayBlocker());
},
enableGlobalOverlayBlocker: () => {
dispatch(enableGlobalOverlayBlocker());
},
getGaStatus: () => {
dispatch(getGaStatus());
},
getLayout: () => {
dispatch(getLayout());
},
onHideNotification: id => {
dispatch(hideNotification(id));
},
pluginLoaded: plugin => {
dispatch(pluginLoaded(plugin));
},
updatePlugin: (pluginId, updatedKey, updatedValue) => {
dispatch(updatePlugin(pluginId, updatedKey, updatedValue));
},
dispatch,
};
}
@ -262,10 +290,6 @@ const withConnect = connect(mapStateToProps, mapDispatchToProps);
const withReducer = injectReducer({ key: 'adminPage', reducer });
const withSaga = injectSaga({ key: 'adminPage', saga });
export default compose(
withReducer,
withSaga,
withConnect,
)(AdminPage);
export default compose(withReducer, withSaga, withConnect)(AdminPage);
// export default connect(mapStateToProps, mapDispatchToProps)(AdminPage);

View File

@ -361,7 +361,7 @@ module.exports = function(strapi) {
type = definition.client === 'pg' ? 'double precision' : 'double';
break;
case 'decimal':
type = 'decimal';
type = 'decimal(10,2)';
break;
case 'date':
case 'time':
@ -431,9 +431,9 @@ module.exports = function(strapi) {
const queries = columns.reduce((acc, attribute) => {
acc.push(`ALTER TABLE ${quote}${table}${quote} ADD ${attribute};`);
return acc;
}, []).join('\n\r');
}, []);
await ORM.knex.raw(queries);
await Promise.all(queries.map(query => ORM.knex.raw(query)));
}
// Execute query to update column type

View File

@ -179,7 +179,11 @@ module.exports = {
);
});
} else if (_.get(this._attributes, `${current}.isVirtual`) !== true) {
acc[current] = params.values[current];
if (typeof params.values[current] === 'object') {
acc[current] = _.get(params.values[current], this.primaryKey);
} else {
acc[current] = params.values[current];
}
}
break;

View File

@ -22,7 +22,7 @@ const appPath = (() => {
return isAdmin ? path.resolve(process.env.PWD, '..') : path.resolve(process.env.PWD, '..', '..');
})();
// const isSetup = path.resolve(process.env.PWD, '..', '..') === path.resolve(process.env.INIT_CWD);
const isSetup = process.env.IS_MONOREPO;
const isSetup = process.env.IS_MONOREPO;
const adminPath = (() => {
if (isAdmin && isSetup) {
@ -42,7 +42,13 @@ const URLs = {
if (isAdmin && !isSetup) {
// Load server configuration.
const serverConfig = path.resolve(appPath, 'config', 'environments', _.lowerCase(process.env.NODE_ENV), 'server.json');
const serverConfig = path.resolve(
appPath,
'config',
'environments',
_.lowerCase(process.env.NODE_ENV),
'server.json',
);
try {
const server = require(serverConfig);
@ -88,21 +94,32 @@ if (process.env.npm_lifecycle_event === 'start') {
// Read `plugins` directory and check if the plugin comes with an UI (it has an App container).
// If we don't do this check webpack expects the plugin to have a containers/App/reducer.js to create
// the plugin's store (redux).
plugins.src = isAdmin && !plugins.exist ? fs.readdirSync(path.resolve(appPath, 'plugins')).filter(x => {
let hasAdminFolder;
plugins.src =
isAdmin && !plugins.exist
? fs.readdirSync(path.resolve(appPath, 'plugins')).filter(x => {
let hasAdminFolder;
try {
fs.accessSync(path.resolve(appPath, 'plugins', x, 'admin', 'src', 'containers', 'App'));
hasAdminFolder = true;
} catch(err) {
hasAdminFolder = false;
}
return x[0] !== '.' && hasAdminFolder;
}) : [];
try {
fs.accessSync(path.resolve(appPath, 'plugins', x, 'admin', 'src', 'containers', 'App'));
hasAdminFolder = true;
} catch (err) {
hasAdminFolder = false;
}
return x[0] !== '.' && hasAdminFolder;
})
: [];
// Construct object of plugin' paths.
plugins.folders = plugins.src.reduce((acc, current) => {
acc[current] = path.resolve(appPath, 'plugins', current, 'node_modules', 'strapi-helper-plugin', 'lib', 'src');
acc[current] = path.resolve(
appPath,
'plugins',
current,
'node_modules',
'strapi-helper-plugin',
'lib',
'src',
);
return acc;
}, {});
@ -110,14 +127,16 @@ if (process.env.npm_lifecycle_event === 'start') {
// Tell webpack to use a loader only for those files
const foldersToInclude = [path.join(adminPath, 'admin', 'src')]
.concat(plugins.src.reduce((acc, current) => {
acc.push(path.resolve(appPath, 'plugins', current, 'admin', 'src'), plugins.folders[current]);
.concat(
plugins.src.reduce((acc, current) => {
acc.push(path.resolve(appPath, 'plugins', current, 'admin', 'src'), plugins.folders[current]);
return acc;
}, []))
return acc;
}, []),
)
.concat([path.join(adminPath, 'node_modules', 'strapi-helper-plugin', 'lib', 'src')]);
module.exports = (options) => {
module.exports = options => {
// The disable option is only for production
// Config from https://github.com/facebook/create-react-app/blob/next/packages/react-scripts/config/webpack.config.prod.js
const extractSass = new ExtractTextPlugin({
@ -127,11 +146,16 @@ module.exports = (options) => {
return {
entry: options.entry,
output: Object.assign({ // Compile into js/build.js
path: path.join(adminPath, 'admin', 'build'),
}, options.output), // Merge with env dependent settings
output: Object.assign(
{
// Compile into js/build.js
path: path.join(adminPath, 'admin', 'build'),
},
options.output,
), // Merge with env dependent settings
module: {
rules: [ // TODO: add eslint formatter
rules: [
// TODO: add eslint formatter
{
// "oneOf" will traverse all following loaders until one will
// match the requirements. When no loader matches it will fall
@ -145,9 +169,7 @@ module.exports = (options) => {
presets: options.babelPresets,
env: {
production: {
only: [
'src',
],
only: ['src'],
plugins: [
require.resolve('babel-plugin-transform-react-remove-prop-types'),
require.resolve('babel-plugin-transform-react-constant-elements'),
@ -159,9 +181,7 @@ module.exports = (options) => {
],
},
test: {
plugins: [
'istanbul',
],
plugins: ['istanbul'],
},
},
},
@ -305,16 +325,8 @@ module.exports = (options) => {
],
alias: options.alias,
symlinks: false,
extensions: [
'.js',
'.jsx',
'.react.js',
],
mainFields: [
'browser',
'jsnext:main',
'main',
],
extensions: ['.js', '.jsx', '.react.js'],
mainFields: ['browser', 'jsnext:main', 'main'],
},
externals: options.externals,
resolveLoader: {

View File

@ -26,7 +26,9 @@ const appPath = (() => {
const rootAdminpath = (() => {
if (isSetup) {
return isAdmin ? path.resolve(appPath, 'strapi-admin') : path.resolve(appPath, 'packages', 'strapi-admin');
return isAdmin
? path.resolve(appPath, 'strapi-admin')
: path.resolve(appPath, 'packages', 'strapi-admin');
}
return path.resolve(appPath, 'admin');
})();
@ -46,42 +48,54 @@ if (process.env.npm_lifecycle_event === 'start') {
plugins.exist = true;
}
plugins.src = process.env.IS_ADMIN === 'true' && !plugins.exist ? fs.readdirSync(path.resolve(appPath, 'plugins')).filter(x => {
let hasAdminFolder;
plugins.src =
process.env.IS_ADMIN === 'true' && !plugins.exist
? fs.readdirSync(path.resolve(appPath, 'plugins')).filter(x => {
let hasAdminFolder;
// Don't inject the plugins that don't have an admin into the app
try {
fs.accessSync(path.resolve(appPath, 'plugins', x, 'admin', 'src', 'containers', 'App'));
hasAdminFolder = true;
} catch(err) {
hasAdminFolder = false;
}
// Don't inject the plugins that don't have an admin into the app
try {
fs.accessSync(path.resolve(appPath, 'plugins', x, 'admin', 'src', 'containers', 'App'));
hasAdminFolder = true;
} catch (err) {
hasAdminFolder = false;
}
return x[0] !== '.' && hasAdminFolder;
}) : [];
return x[0] !== '.' && hasAdminFolder;
})
: [];
plugins.folders = plugins.src.reduce((acc, current) => {
acc[current] = path.resolve(appPath, 'plugins', current, 'node_modules', 'strapi-helper-plugin', 'lib', 'src');
acc[current] = path.resolve(
appPath,
'plugins',
current,
'node_modules',
'strapi-helper-plugin',
'lib',
'src',
);
return acc;
}, {});
}
const port = argv.port || process.env.PORT || 3000;
module.exports = require('./webpack.base.babel')({
// Add hot reloading in development
entry: Object.assign({
main: [
`webpack-hot-middleware/client?path=http://localhost:${port}/__webpack_hmr`,
path.join(appPath, 'admin', 'admin', 'src', 'app.js'),
],
}, plugins.src.reduce((acc, current) => {
acc[current] = path.resolve(plugins.folders[current], 'app.js');
entry: Object.assign(
{
main: [
`webpack-hot-middleware/client?path=http://localhost:${port}/__webpack_hmr`,
path.join(appPath, 'admin', 'admin', 'src', 'app.js'),
],
},
plugins.src.reduce((acc, current) => {
acc[current] = path.resolve(plugins.folders[current], 'app.js');
return acc;
}, {})
return acc;
}, {}),
),
// Don't use hashes in dev mode for better performance
@ -112,10 +126,12 @@ module.exports = require('./webpack.base.babel')({
// Process the CSS with PostCSS
postcssPlugins: [
postcssFocus(), // Add a :focus to every :hover
cssnext({ // Allow future CSS features to be used, also auto-prefixes the CSS...
cssnext({
// Allow future CSS features to be used, also auto-prefixes the CSS...
browsers: ['last 2 versions', 'IE > 10'], // ...based on this browser list
}),
postcssReporter({ // Posts messages from plugins to the terminal
postcssReporter({
// Posts messages from plugins to the terminal
clearMessages: true,
}),
],
@ -136,30 +152,69 @@ module.exports = require('./webpack.base.babel')({
],
alias: {
moment: 'moment/moment.js',
'babel-polyfill': path.resolve(rootAdminpath, 'node_modules', 'strapi-helper-plugin', 'node_modules', 'babel-polyfill'),
'lodash': path.resolve(rootAdminpath, 'node_modules', 'strapi-helper-plugin', 'node_modules', 'lodash'),
'immutable': path.resolve(rootAdminpath, 'node_modules', 'strapi-helper-plugin', 'node_modules', 'immutable'),
'react-intl': path.resolve(rootAdminpath, 'node_modules', 'strapi-helper-plugin', 'node_modules', 'react-intl'),
'react': path.resolve(rootAdminpath, 'node_modules', 'strapi-helper-plugin', 'node_modules', 'react'),
'react-dom': path.resolve(rootAdminpath, 'node_modules', 'strapi-helper-plugin', 'node_modules', 'react-dom'),
'react-transition-group': path.resolve(rootAdminpath, 'node_modules', 'strapi-helper-plugin', 'node_modules', 'react-transition-group'),
'reactstrap': path.resolve(rootAdminpath, 'node_modules', 'strapi-helper-plugin', 'node_modules', 'reactstrap'),
'styled-components': path.resolve(rootAdminpath, 'node_modules', 'strapi-helper-plugin', 'node_modules', 'styled-components')
'babel-polyfill': path.resolve(
rootAdminpath,
'node_modules',
'strapi-helper-plugin',
'node_modules',
'babel-polyfill',
),
lodash: path.resolve(rootAdminpath, 'node_modules', 'strapi-helper-plugin', 'node_modules', 'lodash'),
immutable: path.resolve(
rootAdminpath,
'node_modules',
'strapi-helper-plugin',
'node_modules',
'immutable',
),
'react-intl': path.resolve(
rootAdminpath,
'node_modules',
'strapi-helper-plugin',
'node_modules',
'react-intl',
),
react: path.resolve(rootAdminpath, 'node_modules', 'strapi-helper-plugin', 'node_modules', 'react'),
'react-dom': path.resolve(
rootAdminpath,
'node_modules',
'strapi-helper-plugin',
'node_modules',
'react-dom',
),
'react-transition-group': path.resolve(
rootAdminpath,
'node_modules',
'strapi-helper-plugin',
'node_modules',
'react-transition-group',
),
reactstrap: path.resolve(
rootAdminpath,
'node_modules',
'strapi-helper-plugin',
'node_modules',
'reactstrap',
),
'styled-components': path.resolve(
rootAdminpath,
'node_modules',
'strapi-helper-plugin',
'node_modules',
'styled-components',
),
},
// Emit a source map for easier debugging
devtool: 'cheap-module-source-map',
});
/**
* We dynamically generate the HTML content in development so that the different
* DLL Javascript files are loaded in script tags and available to our application.
*/
function templateContent() {
const html = fs.readFileSync(
path.resolve(appPath, 'admin', 'admin', 'src', 'index.html')
).toString();
const html = fs.readFileSync(path.resolve(appPath, 'admin', 'admin', 'src', 'index.html')).toString();
return html;
}

View File

@ -33,7 +33,9 @@ const adminPath = (() => {
const rootAdminpath = (() => {
if (isSetup) {
return isAdmin ? path.resolve(appPath, 'strapi-admin') : path.resolve(appPath, 'packages', 'strapi-admin');
return isAdmin
? path.resolve(appPath, 'strapi-admin')
: path.resolve(appPath, 'packages', 'strapi-admin');
}
return path.resolve(appPath, 'admin');
})();
@ -64,7 +66,13 @@ let publicPath;
if (isAdmin && !isSetup) {
// Load server configuration.
const serverConfig = path.resolve(appPath, 'config', 'environments', _.lowerCase(process.env.NODE_ENV), 'server.json');
const serverConfig = path.resolve(
appPath,
'config',
'environments',
_.lowerCase(process.env.NODE_ENV),
'server.json',
);
try {
const server = require(serverConfig);
@ -83,32 +91,40 @@ if (isAdmin && !isSetup) {
// Build the `index.html file`
if (isAdmin) {
plugins.push(new HtmlWebpackPlugin({
template: 'admin/src/index.html',
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
chunksSortMode: 'manual',
chunks: ['main'],
inject: true,
}));
plugins.push(new AddAssetHtmlPlugin({
filepath: path.resolve(__dirname, 'dist/*.dll.js'),
}));
plugins.push(new CopyWebpackPlugin([{
from: 'config/plugins.json',
context: path.resolve(adminPath, 'admin', 'src'),
to: 'config/plugins.json',
}]));
plugins.push(
new HtmlWebpackPlugin({
template: 'admin/src/index.html',
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
chunksSortMode: 'manual',
chunks: ['main'],
inject: true,
}),
);
plugins.push(
new AddAssetHtmlPlugin({
filepath: path.resolve(__dirname, 'dist/*.dll.js'),
}),
);
plugins.push(
new CopyWebpackPlugin([
{
from: 'config/plugins.json',
context: path.resolve(adminPath, 'admin', 'src'),
to: 'config/plugins.json',
},
]),
);
}
const main = (() => {
@ -128,11 +144,14 @@ module.exports = base({
},
// Utilize long-term caching by adding content hashes (not compilation hashes) to compiled assets
output: _.omitBy({
filename: '[name].js',
chunkFilename: '[name].[chunkhash].chunk.js',
publicPath,
}, _.isUndefined),
output: _.omitBy(
{
filename: '[name].js',
chunkFilename: '[name].[chunkhash].chunk.js',
publicPath,
},
_.isUndefined,
),
// In production, we minify our CSS with cssnano
postcssPlugins: [
@ -164,15 +183,57 @@ module.exports = base({
alias: {
moment: 'moment/moment.js',
'babel-polyfill': path.resolve(rootAdminpath, 'node_modules', 'strapi-helper-plugin', 'node_modules', 'babel-polyfill'),
'lodash': path.resolve(rootAdminpath, 'node_modules', 'strapi-helper-plugin', 'node_modules', 'lodash'),
'immutable': path.resolve(rootAdminpath, 'node_modules', 'strapi-helper-plugin', 'node_modules', 'immutable'),
'react-intl': path.resolve(rootAdminpath, 'node_modules', 'strapi-helper-plugin', 'node_modules', 'react-intl'),
'react': path.resolve(rootAdminpath, 'node_modules', 'strapi-helper-plugin', 'node_modules', 'react'),
'react-dom': path.resolve(rootAdminpath, 'node_modules', 'strapi-helper-plugin', 'node_modules', 'react-dom'),
'react-transition-group': path.resolve(rootAdminpath, 'node_modules', 'strapi-helper-plugin', 'node_modules', 'react-transition-group'),
'reactstrap': path.resolve(rootAdminpath, 'node_modules', 'strapi-helper-plugin', 'node_modules', 'reactstrap'),
'styled-components': path.resolve(rootAdminpath, 'node_modules', 'strapi-helper-plugin', 'node_modules', 'styled-components'),
'babel-polyfill': path.resolve(
rootAdminpath,
'node_modules',
'strapi-helper-plugin',
'node_modules',
'babel-polyfill',
),
lodash: path.resolve(rootAdminpath, 'node_modules', 'strapi-helper-plugin', 'node_modules', 'lodash'),
immutable: path.resolve(
rootAdminpath,
'node_modules',
'strapi-helper-plugin',
'node_modules',
'immutable',
),
'react-intl': path.resolve(
rootAdminpath,
'node_modules',
'strapi-helper-plugin',
'node_modules',
'react-intl',
),
react: path.resolve(rootAdminpath, 'node_modules', 'strapi-helper-plugin', 'node_modules', 'react'),
'react-dom': path.resolve(
rootAdminpath,
'node_modules',
'strapi-helper-plugin',
'node_modules',
'react-dom',
),
'react-transition-group': path.resolve(
rootAdminpath,
'node_modules',
'strapi-helper-plugin',
'node_modules',
'react-transition-group',
),
reactstrap: path.resolve(
rootAdminpath,
'node_modules',
'strapi-helper-plugin',
'node_modules',
'reactstrap',
),
'styled-components': path.resolve(
rootAdminpath,
'node_modules',
'strapi-helper-plugin',
'node_modules',
'styled-components',
),
},
devtool: 'cheap-module-source-map',

View File

@ -78,8 +78,9 @@ module.exports = strapi => {
// Make sure the client is installed in the application
// `node_modules` directory.
let client;
try {
require(path.resolve(strapi.config.appPath, 'node_modules', connection.settings.client));
client = require(path.resolve(strapi.config.appPath, 'node_modules', connection.settings.client));
} catch (err) {
strapi.log.error('The client `' + connection.settings.client + '` is not installed.');
strapi.log.error('You can install it with `$ npm install ' + connection.settings.client + ' --save`.');
@ -105,18 +106,22 @@ module.exports = strapi => {
migrations: _.get(connection.options, 'migrations')
}, strapi.config.hook.settings.knex);
if (options.client === 'pg' && _.isString(_.get(options.connection, 'schema'))) {
options.pool = {
min: _.get(connection.options, 'pool.min') || 0,
max: _.get(connection.options, 'pool.max') || 10,
afterCreate: (conn, cb) => {
conn.query(`SET SESSION SCHEMA '${options.connection.schema}';`, (err) => {
cb(err, conn);
});
}
};
} else {
delete options.connection.schema;
if (options.client === 'pg') {
client.types.setTypeParser(1700, 'text', parseFloat);
if (_.isString(_.get(options.connection, 'schema'))) {
options.pool = {
min: _.get(connection.options, 'pool.min') || 0,
max: _.get(connection.options, 'pool.max') || 10,
afterCreate: (conn, cb) => {
conn.query(`SET SESSION SCHEMA '${options.connection.schema}';`, (err) => {
cb(err, conn);
});
}
};
} else {
delete options.connection.schema;
}
}
if (options.client === 'mysql') {

View File

@ -10,6 +10,19 @@ export default function setParallelAttribute(newAttribute) {
parallelAttribute.params.columnName = newAttribute.params.targetColumnName;
parallelAttribute.params.targetColumnName = newAttribute.params.columnName;
parallelAttribute.params.dominant = false;
if (newAttribute.params.nature) {
switch (newAttribute.params.nature) {
case 'manyToOne':
parallelAttribute.params.nature = 'oneToMany';
break;
case 'oneToMany':
parallelAttribute.params.nature = 'manyToOne';
break;
default:
//
}
}
return parallelAttribute;
}
return;

View File

@ -177,6 +177,18 @@ function setParallelAttribute(data) {
parallelAttribute.params.columnName = data.params.targetColumnName;
parallelAttribute.params.targetColumnName = data.params.columnName;
parallelAttribute.params.dominant = false;
switch (data.params.nature) {
case 'manyToOne':
parallelAttribute.params.nature = 'oneToMany';
break;
case 'oneToMany':
parallelAttribute.params.nature = 'manyToOne';
break;
default:
//
}
return parallelAttribute;
}

View File

@ -624,6 +624,7 @@ module.exports = {
break;
// falls through
}
break;
default:
// Where.
queryOpts.query = strapi.utils.models.convertParams(name, {

View File

@ -40,4 +40,4 @@
"configurable": false
}
}
}
}

View File

@ -24,16 +24,18 @@ const { cli, logger } = require('strapi-utils');
* (fire up the application in our working directory).
*/
module.exports = function() {
module.exports = function(appPath = '') {
// Check that we're in a valid Strapi project.
if (!cli.isStrapiApp()) {
return logger.error('This command can only be used inside a Strapi project.');
}
appPath = path.join(process.cwd(), appPath);
try {
const strapi = function () {
try {
return require(path.resolve(process.cwd(), 'node_modules', 'strapi'));
return require(path.resolve(appPath, 'node_modules', 'strapi'));
} catch (e) {
return require('strapi'); // eslint-disable-line import/no-unresolved
}
@ -46,7 +48,7 @@ module.exports = function() {
// Require server configurations
const server = require(path.resolve(
process.cwd(),
appPath,
'config',
'environments',
'development',
@ -81,7 +83,7 @@ module.exports = function() {
});
};
setFilesToWatch(process.cwd());
setFilesToWatch(appPath);
@ -124,7 +126,9 @@ module.exports = function() {
}
});
return strapi.start(afterwards);
return strapi.start({
appPath
}, afterwards);
} else {
return;
}
@ -133,7 +137,9 @@ module.exports = function() {
// Otherwise, if no workable local `strapi` module exists,
// run the application using the currently running version
// of `strapi`. This is probably always the global install.
strapi.start(afterwards);
strapi.start({
appPath
}, afterwards);
} catch (e) {
logger.error(e);
process.exit(0);

View File

@ -64,9 +64,11 @@ program
// `$ strapi start`
program
.command('start')
.command('start [appPath]')
.description('start your Strapi application')
.action(require('./strapi-start'));
.action((appPath) => {
require('./strapi-start')(appPath);
});
// `$ strapi generate:api`
program

View File

@ -10,7 +10,16 @@ const { includes, get, assign, toLower } = require('lodash');
const { logger, models } = require('strapi-utils');
const stackTrace = require('stack-trace');
const utils = require('./utils');
const { nestedConfigurations, appConfigurations, apis, middlewares, hooks, plugins, admin, store } = require('./core');
const {
nestedConfigurations,
appConfigurations,
apis,
middlewares,
hooks,
plugins,
admin,
store,
} = require('./core');
const initializeMiddlewares = require('./middlewares');
const initializeHooks = require('./hooks');
@ -41,7 +50,7 @@ class Strapi extends EventEmitter {
// Utils.
this.utils = {
models
models,
};
// Exclude EventEmitter, Koa and HTTP server to be freezed.
@ -73,12 +82,12 @@ class Strapi extends EventEmitter {
services: 'services',
static: 'public',
validators: 'validators',
views: 'views'
views: 'views',
},
middleware: {},
hook: {},
functions: {},
routes: {}
routes: {},
};
// Bind context functions.
@ -92,8 +101,6 @@ class Strapi extends EventEmitter {
// Emit starting event.
this.emit('server:starting');
// Enhance app.
await this.enhancer();
// Load the app.
await this.load();
// Run bootstrap function.
@ -169,7 +176,11 @@ class Strapi extends EventEmitter {
// Destroy server and available connections.
this.server.destroy();
if (cluster.isWorker && this.config.environment === 'development' && get(this.config, 'currentEnvironment.server.autoReload.enabled', true) === true) {
if (
cluster.isWorker &&
this.config.environment === 'development' &&
get(this.config, 'currentEnvironment.server.autoReload.enabled', true) === true
) {
process.send('stop');
}
@ -194,7 +205,7 @@ class Strapi extends EventEmitter {
nestedConfigurations.call(this),
apis.call(this),
middlewares.call(this),
hooks.call(this)
hooks.call(this),
]);
// Populate AST with configurations.
@ -207,10 +218,7 @@ class Strapi extends EventEmitter {
await store.call(this);
// Initialize hooks and middlewares.
await Promise.all([
initializeMiddlewares.call(this),
initializeHooks.call(this)
]);
await Promise.all([initializeMiddlewares.call(this), initializeHooks.call(this)]);
// Harmonize plugins configuration.
await plugins.call(this);
@ -218,15 +226,19 @@ class Strapi extends EventEmitter {
reload() {
const state = {
shouldReload: true
shouldReload: true,
};
const reload = function () {
const reload = function() {
if (state.shouldReload === false) {
return;
}
if (cluster.isWorker && this.config.environment === 'development' && get(this.config, 'currentEnvironment.server.autoReload.enabled', true) === true) {
if (
cluster.isWorker &&
this.config.environment === 'development' &&
get(this.config, 'currentEnvironment.server.autoReload.enabled', true) === true
) {
process.send('reload');
}
};
@ -234,14 +246,14 @@ class Strapi extends EventEmitter {
Object.defineProperty(reload, 'isWatching', {
configurable: true,
enumerable: true,
set: (value) => {
set: value => {
// Special state when the reloader is disabled temporarly (see GraphQL plugin example).
state.shouldReload = !(state.isWatching === false && value === true);
state.isWatching = value;
},
get: () => {
return state.isWatching;
}
},
});
reload.isReloading = false;
@ -251,45 +263,49 @@ class Strapi extends EventEmitter {
}
async bootstrap() {
const execBootstrap = (fn) => !fn ? Promise.resolve() : new Promise((resolve, reject) => {
const timeoutMs = this.config.bootstrapTimeout || 3500;
const timer = setTimeout(() => {
this.log.warn(`Bootstrap is taking unusually long to execute its callback ${timeoutMs} miliseconds).`);
this.log.warn('Perhaps you forgot to call it?');
}, timeoutMs);
const execBootstrap = fn =>
!fn
? Promise.resolve()
: new Promise((resolve, reject) => {
const timeoutMs = this.config.bootstrapTimeout || 3500;
const timer = setTimeout(() => {
this.log.warn(
`Bootstrap is taking unusually long to execute its callback ${timeoutMs} miliseconds).`,
);
this.log.warn('Perhaps you forgot to call it?');
}, timeoutMs);
let ranBootstrapFn = false;
let ranBootstrapFn = false;
try {
fn(err => {
if (ranBootstrapFn) {
this.log.error('You called the callback in `strapi.config.boostrap` more than once!');
try {
fn(err => {
if (ranBootstrapFn) {
this.log.error('You called the callback in `strapi.config.boostrap` more than once!');
return reject();
return reject();
}
ranBootstrapFn = true;
clearTimeout(timer);
return resolve(err);
});
} catch (e) {
if (ranBootstrapFn) {
this.log.error('The bootstrap function threw an error after its callback was called.');
return reject(e);
}
ranBootstrapFn = true;
clearTimeout(timer);
return resolve(e);
}
ranBootstrapFn = true;
clearTimeout(timer);
return resolve(err);
});
} catch (e) {
if (ranBootstrapFn) {
this.log.error('The bootstrap function threw an error after its callback was called.');
return reject(e);
}
ranBootstrapFn = true;
clearTimeout(timer);
return resolve(e);
}
});
return Promise.all(
Object.values(this.plugins)
.map(x => execBootstrap(get(x, 'config.functions.bootstrap')))
Object.values(this.plugins).map(x => execBootstrap(get(x, 'config.functions.bootstrap'))),
).then(() => execBootstrap(this.config.functions.bootstrap));
}
@ -299,19 +315,24 @@ class Strapi extends EventEmitter {
// Remove object from tree.
delete this.propertiesToNotFreeze;
return Object.keys(this).filter(x => !includes(propertiesToNotFreeze, x)).forEach(key => {
Object.freeze(this[key]);
});
return Object.keys(this)
.filter(x => !includes(propertiesToNotFreeze, x))
.forEach(key => {
Object.freeze(this[key]);
});
}
query(entity, plugin) {
if (!entity) {
return this.log.error(`You can't call the query method without passing the model's name as a first argument.`);
return this.log.error(
`You can't call the query method without passing the model's name as a first argument.`,
);
}
const model = entity.toLowerCase();
const Model = get(strapi.plugins, [plugin, 'models', model]) || get(strapi, ['models', model]) || undefined;
const Model =
get(strapi.plugins, [plugin, 'models', model]) || get(strapi, ['models', model]) || undefined;
if (!Model) {
return this.log.error(`The model ${model} can't be found.`);
@ -355,12 +376,15 @@ class Strapi extends EventEmitter {
}
// Bind queries with the current model to allow the use of `this`.
const bindQueries = Object.keys(queries).reduce((acc, current) => {
return acc[current] = queries[current].bind(Model), acc;
}, {
orm: connector,
primaryKey: Model.primaryKey
});
const bindQueries = Object.keys(queries).reduce(
(acc, current) => {
return (acc[current] = queries[current].bind(Model)), acc;
},
{
orm: connector,
primaryKey: Model.primaryKey,
},
);
return bindQueries;
}

View File

@ -25,7 +25,8 @@ module.exports = function() {
'./node_modules/strapi-middleware-*',
'./node_modules/strapi-upload-*',
'./node_modules/strapi-lint'
]
],
cwd: this.config.appPath
}, (err, files) => {
if (err) {
return reject(err);

View File

@ -8,13 +8,13 @@ const _ = require('lodash');
module.exports = function() {
return new Promise((resolve, reject) => {
const folder = ((url = _.get(strapi.config.currentEnvironment.server, 'admin.path', 'admin')) =>
url[0] === '/' ? url.substring(1) : url
)().replace(/\/$/, '') ;
url[0] === '/' ? url.substring(1) : url)().replace(/\/$/, '');
const configuratePlugin = (acc, current, source, name) => {
switch (source) {
case 'host': {
const host = _.get(this.config.environments[current].server, 'admin.build.host').replace(/\/$/, '') || '/';
const host =
_.get(this.config.environments[current].server, 'admin.build.host').replace(/\/$/, '') || '/';
if (!host) {
throw new Error(`You can't use \`remote\` as a source without set the \`host\` configuration.`);
@ -32,12 +32,20 @@ module.exports = function() {
}
case 'custom':
if (!_.isEmpty(_.get(this.plugins[name].config, `sources.${current}`, {}))) {
return acc[current] = this.plugins[name].config.sources[current];
return (acc[current] = this.plugins[name].config.sources[current]);
}
throw new Error(`You have to define the source URL for each environment in \`./plugins/**/config/sources.json\``);
throw new Error(
`You have to define the source URL for each environment in \`./plugins/**/config/sources.json\``,
);
case 'backend': {
const backend = _.get(this.config.environments[current], 'server.admin.build.backend', `http://${this.config.environments[current].server.host}:${this.config.environments[current].server.port}`).replace(/\/$/, '');
const backend = _.get(
this.config.environments[current],
'server.admin.build.backend',
`http://${this.config.environments[current].server.host}:${
this.config.environments[current].server.port
}`,
).replace(/\/$/, '');
return `${backend}/${folder.replace(/\/$/, '')}/${name}/main.js`;
}
@ -46,12 +54,30 @@ module.exports = function() {
}
};
const sourcePath = process.env.NODE_ENV !== 'test' ?
path.resolve(this.config.appPath, 'admin', 'admin', 'src', 'config', 'plugins.json'):
path.resolve(this.config.appPath, 'packages', 'strapi-admin', 'admin', 'src', 'config', 'plugins.json');
const buildPath = process.env.NODE_ENV !== 'test' ?
path.resolve(this.config.appPath, 'admin', 'admin', 'build', 'config', 'plugins.json'):
path.resolve(this.config.appPath, 'packages', 'strapi-admin', 'admin', 'build', 'config', 'plugins.json');
const sourcePath =
this.config.environment !== 'test'
? path.resolve(this.config.appPath, 'admin', 'admin', 'src', 'config', 'plugins.json')
: path.resolve(
this.config.appPath,
'packages',
'strapi-admin',
'admin',
'src',
'config',
'plugins.json',
);
const buildPath =
this.config.environment !== 'test'
? path.resolve(this.config.appPath, 'admin', 'admin', 'build', 'config', 'plugins.json')
: path.resolve(
this.config.appPath,
'packages',
'strapi-admin',
'admin',
'build',
'config',
'plugins.json',
);
try {
fs.access(path.resolve(this.config.appPath, 'admin', 'admin'), err => {
@ -115,14 +141,16 @@ module.exports = function() {
// Create `plugins.json` file.
// Don't inject the plugins without an Admin
const data = Object.keys(this.plugins)
const data = Object.keys(this.plugins)
.filter(plugin => {
let hasAdminFolder;
try {
fs.accessSync(path.resolve(this.config.appPath, 'plugins', plugin, 'admin', 'src', 'containers', 'App'));
fs.accessSync(
path.resolve(this.config.appPath, 'plugins', plugin, 'admin', 'src', 'containers', 'App'),
);
hasAdminFolder = true;
} catch(err) {
} catch (err) {
hasAdminFolder = false;
}
return hasAdminFolder;
@ -130,7 +158,11 @@ module.exports = function() {
.map(name => ({
id: name,
source: Object.keys(this.config.environments).reduce((acc, current) => {
const source = _.get(this.config.environments[current].server, 'admin.build.plugins.source', 'default');
const source = _.get(
this.config.environments[current].server,
'admin.build.plugins.source',
'default',
);
if (_.isString(source)) {
acc[current] = configuratePlugin(acc, current, source, name);
@ -139,7 +171,7 @@ module.exports = function() {
}
return acc;
}, {})
}, {}),
}));
fs.writeFileSync(sourcePath, JSON.stringify(data, null, 2), 'utf8');

View File

@ -6,7 +6,7 @@ const { after, includes, indexOf, drop, dropRight, uniq, defaultsDeep, get, set,
module.exports = async function() {
// Set if is admin destination for middleware application.
this.app.use(async (ctx, next) => {
if (ctx.request.header['origin'] === 'http://localhost:4000' || ctx.request.method === 'OPTIONS') {
if (ctx.request.header['origin'] === 'http://localhost:4000') {
ctx.request.header['x-forwarded-host'] = 'strapi';
}

View File

@ -0,0 +1,129 @@
{
"article": {
"attributes":[
{
"name":"title",
"params":{
"appearance":{
"WYSIWYG":false
},
"multiple":false,
"type":"string"
}
},
{
"name":"content",
"params":{
"appearance":{
"WYSIWYG":true
},
"multiple":false,
"type":"text"
}
},
{
"name":"published",
"params":{
"appearance":{
"WYSIWYG":false
},
"multiple":false,
"type":"boolean"
}
},
{
"name":"author",
"params":{
"nature":"manyToOne",
"target":"user",
"pluginValue":"users-permissions",
"key":"articles",
"plugin":true
}
}
],
"connection":"default",
"name":"article",
"description":"",
"collectionName":""
},
"tag": {
"attributes":[
{
"name":"name",
"params":{
"appearance":{
"WYSIWYG":false
},
"multiple":false,
"type":"string"
}
},
{
"name":"articles",
"params":{
"dominant":true,
"nature":"manyToMany",
"target":"article",
"key":"tags"
}
}
],
"connection":"default",
"name":"tag",
"description":"",
"collectionName":""
},
"category": {
"attributes":[
{
"name":"name",
"params":{
"appearance":{
"WYSIWYG":false
},
"multiple":false,
"type":"string"
}
},
{
"name":"articles",
"params":{
"nature":"oneToMany",
"target":"article",
"key":"category"
}
}
],
"connection":"default",
"name":"category",
"description":"",
"collectionName":""
},
"reference": {
"attributes":[
{
"name":"name",
"params":{
"appearance":{
"WYSIWYG":false
},
"multiple":false,
"type":"string"
}
},
{
"name":"article",
"params":{
"target":"article",
"key":"reference",
"nature":"oneToOne"
}
}
],
"connection":"default",
"name":"reference",
"description":"",
"collectionName":""
}
}

32
test/helpers/restart.js Normal file
View File

@ -0,0 +1,32 @@
module.exports = function (rq) {
return new Promise(async (resolve) => {
const ping = async () => {
try {
await rq({
url: '/_health',
method: 'HEAD',
mode: 'no-cors',
json: true,
headers: {
'Content-Type': 'application/json',
'Keep-Alive': false,
}
});
return resolve();
} catch (err) {
if (err.statusCode) {
return resolve();
} else {
return setTimeout(() => {
ping();
}, 1000);
}
}
};
setTimeout(() => {
ping();
}, 1000);
});
};

774
test/index.test.js Normal file
View File

@ -0,0 +1,774 @@
let request = require('request');
const form = require('./helpers/generators');
const restart = require('./helpers/restart');
request = request.defaults({
baseUrl: 'http://localhost:1337'
});
const rq = (options) => {
const params = JSON.parse(JSON.stringify(options));
for (let key in params.formData) {
if (typeof params.formData[key] === 'object') {
params.formData[key] = JSON.stringify(params.formData[key]);
}
}
return new Promise((resolve, reject) => {
request(params, (err, res, body) => {
if (err || res.statusCode < 200 || res.statusCode >= 300) {
return reject(err || body);
}
return resolve(body);
});
});
}
const cleanDate = (entry) => {
delete entry.updatedAt;
delete entry.createdAt;
delete entry.created_at;
delete entry.updated_at;
};
let data;
describe('App setup auth', () => {
test(
'Register admin user',
async () => {
const body = await rq({
url: `/auth/local/register`,
method: 'POST',
body: {
username: 'admin',
email: 'admin@strapi.io',
password: 'pcw123'
},
json: true
});
request = request.defaults({
headers: {
'Authorization': `Bearer ${body.jwt}`
}
});
}
);
});
describe('Generate test APIs', () => {
beforeEach(async () => {
await restart(rq);
}, 60000);
test(
'Create new article API',
async () => {
await rq({
url: `/content-type-builder/models`,
method: 'POST',
body: form.article,
json: true
});
}
);
test(
'Create new tag API',
async () => {
await rq({
url: `/content-type-builder/models`,
method: 'POST',
body: form.tag,
json: true
});
}
);
test(
'Create new category API',
async () => {
await rq({
url: `/content-type-builder/models`,
method: 'POST',
body: form.category,
json: true
});
}
);
test(
'Create new reference API',
async () => {
await rq({
url: `/content-type-builder/models`,
method: 'POST',
body: form.reference,
json: true
});
}
);
});
describe('Test manyToMany relation (article - tag) with Content Manager', () => {
beforeAll(() => {
data = {
articles: [],
tags: []
};
});
beforeEach(async () => {
await restart(rq);
}, 60000);
test(
'Create tag1',
async () => {
let body = await rq({
url: `/content-manager/explorer/tag/?source=content-manager`,
method: 'POST',
formData: {
name: 'tag1'
}
});
body = JSON.parse(body);
data.tags.push(body);
expect(body.id);
expect(Array.isArray(body.articles)).toBeTruthy();
expect(body.name).toBe('tag1');
}
);
test(
'Create tag2',
async () => {
let body = await rq({
url: `/content-manager/explorer/tag/?source=content-manager`,
method: 'POST',
formData: {
name: 'tag2'
}
});
body = JSON.parse(body);
data.tags.push(body);
expect(body.id);
expect(Array.isArray(body.articles)).toBeTruthy();
expect(body.name).toBe('tag2');
}
);
test(
'Create tag3',
async () => {
let body = await rq({
url: `/content-manager/explorer/tag/?source=content-manager`,
method: 'POST',
formData: {
name: 'tag3'
}
});
body = JSON.parse(body);
data.tags.push(body);
expect(body.id);
expect(Array.isArray(body.articles)).toBeTruthy();
expect(body.name).toBe('tag3');
}
);
test(
'Create article1 without relation',
async () => {
const entry = {
title: 'Article 1',
content: 'My super content 1'
};
let body = await rq({
url: `/content-manager/explorer/article/?source=content-manager`,
method: 'POST',
formData: entry
});
body = JSON.parse(body);
data.articles.push(body);
expect(body.id);
expect(body.title).toBe(entry.title);
expect(body.content).toBe(entry.content);
expect(Array.isArray(body.tags)).toBeTruthy();
expect(body.tags.length).toBe(0);
}
);
test(
'Create article2 with tag1',
async () => {
const entry = {
title: 'Article 2',
content: 'Content 2',
tags: [data.tags[0]]
};
let body = await rq({
url: `/content-manager/explorer/article/?source=content-manager`,
method: 'POST',
formData: entry
});
body = JSON.parse(body);
data.articles.push(body);
expect(body.id);
expect(body.title).toBe(entry.title);
expect(body.content).toBe(entry.content);
expect(Array.isArray(body.tags)).toBeTruthy();
expect(body.tags.length).toBe(1);
expect(body.tags[0].id).toBe(data.tags[0].id);
}
);
test(
'Update article1 add tag2',
async () => {
const entry = Object.assign({}, data.articles[0], {
tags: [data.tags[1]]
});
cleanDate(entry);
let body = await rq({
url: `/content-manager/explorer/article/${entry.id}?source=content-manager`,
method: 'PUT',
formData: entry
});
body = JSON.parse(body);
data.articles[0] = body;
expect(body.id);
expect(body.title).toBe(entry.title);
expect(body.content).toBe(entry.content);
expect(Array.isArray(body.tags)).toBeTruthy();
expect(body.tags.length).toBe(1);
expect(body.tags[0].id).toBe(data.tags[1].id);
}
);
test(
'Update article1 add tag1 and tag3',
async () => {
const entry = Object.assign({}, data.articles[0]);
entry.tags.push(data.tags[0]);
entry.tags.push(data.tags[2]);
cleanDate(entry);
let body = await rq({
url: `/content-manager/explorer/article/${entry.id}?source=content-manager`,
method: 'PUT',
formData: entry
});
body = JSON.parse(body);
data.articles[0] = body;
expect(body.id);
expect(body.title).toBe(entry.title);
expect(body.content).toBe(entry.content);
expect(Array.isArray(body.tags)).toBeTruthy();
expect(body.tags.length).toBe(3);
}
);
test(
'Update article1 remove one tag',
async () => {
const entry = Object.assign({}, data.articles[0]);
entry.tags = entry.tags.slice(1);
cleanDate(entry);
let body = await rq({
url: `/content-manager/explorer/article/${entry.id}?source=content-manager`,
method: 'PUT',
formData: entry
});
body = JSON.parse(body);
data.articles[0] = body;
expect(body.id);
expect(body.title).toBe(entry.title);
expect(body.content).toBe(entry.content);
expect(Array.isArray(body.tags)).toBeTruthy();
expect(body.tags.length).toBe(2);
}
);
test(
'Update article1 remove all tag',
async () => {
const entry = Object.assign({}, data.articles[0], {
tags: []
});
cleanDate(entry);
let body = await rq({
url: `/content-manager/explorer/article/${entry.id}?source=content-manager`,
method: 'PUT',
formData: entry
});
body = JSON.parse(body);
data.articles[0] = body;
expect(body.id);
expect(body.title).toBe(entry.title);
expect(body.content).toBe(entry.content);
expect(Array.isArray(body.tags)).toBeTruthy();
expect(body.tags.length).toBe(0);
}
);
});
describe('Test oneToMany - manyToOne relation (article - category) with Content Manager', () => {
beforeAll(() => {
data = {
articles: [],
categories: []
};
});
beforeEach(async () => {
await restart(rq);
}, 60000);
test(
'Create cat1',
async () => {
let body = await rq({
url: `/content-manager/explorer/category/?source=content-manager`,
method: 'POST',
formData: {
name: 'cat1'
}
});
body = JSON.parse(body);
data.categories.push(body);
expect(body.id);
expect(Array.isArray(body.articles)).toBeTruthy();
expect(body.name).toBe('cat1');
}
);
test(
'Create cat2',
async () => {
let body = await rq({
url: `/content-manager/explorer/category/?source=content-manager`,
method: 'POST',
formData: {
name: 'cat2'
}
});
body = JSON.parse(body);
data.categories.push(body);
expect(body.id);
expect(Array.isArray(body.articles)).toBeTruthy();
expect(body.name).toBe('cat2');
}
);
test(
'Create article1 with cat1',
async () => {
const entry = {
title: 'Article 1',
content: 'Content 1',
category: data.categories[0]
};
let body = await rq({
url: `/content-manager/explorer/article/?source=content-manager`,
method: 'POST',
formData: entry
});
body = JSON.parse(body);
data.articles.push(body);
expect(body.id);
expect(body.title).toBe(entry.title);
expect(body.content).toBe(entry.content);
expect(body.category.name).toBe(entry.category.name);
expect(Array.isArray(body.tags)).toBeTruthy();
}
);
test(
'Update article1 with cat2',
async () => {
const entry = Object.assign({}, data.articles[0], {
category: data.categories[1]
});
cleanDate(entry);
let body = await rq({
url: `/content-manager/explorer/article/${entry.id}?source=content-manager`,
method: 'PUT',
formData: entry
});
body = JSON.parse(body);
data.articles[0] = body;
expect(body.id);
expect(body.title).toBe(entry.title);
expect(body.content).toBe(entry.content);
expect(body.category.name).toBe(entry.category.name);
expect(Array.isArray(body.tags)).toBeTruthy();
}
);
test(
'Create article2',
async () => {
const entry = {
title: 'Article 2',
content: 'Content 2'
};
let body = await rq({
url: `/content-manager/explorer/article?source=content-manager`,
method: 'POST',
formData: entry
});
body = JSON.parse(body);
data.articles.push(body);
expect(body.id);
expect(body.title).toBe(entry.title);
expect(body.content).toBe(entry.content);
expect(Array.isArray(body.tags)).toBeTruthy();
}
);
test(
'Update article2 with cat2',
async () => {
const entry = Object.assign({}, data.articles[1], {
category: data.categories[1]
});
cleanDate(entry);
let body = await rq({
url: `/content-manager/explorer/article/${entry.id}?source=content-manager`,
method: 'PUT',
formData: entry
});
body = JSON.parse(body);
data.articles[1] = body;
expect(body.id);
expect(body.title).toBe(entry.title);
expect(body.content).toBe(entry.content);
expect(body.category.name).toBe(entry.category.name);
expect(Array.isArray(body.tags)).toBeTruthy();
}
);
test(
'Update cat1 with article1',
async () => {
const entry = Object.assign({}, data.categories[0]);
entry.articles.push(data.articles[0]);
cleanDate(entry);
let body = await rq({
url: `/content-manager/explorer/category/${entry.id}?source=content-manager`,
method: 'PUT',
formData: entry
});
body = JSON.parse(body);
data.categories[0] = body;
expect(body.id);
expect(Array.isArray(body.articles)).toBeTruthy();
expect(body.articles.length).toBe(1);
expect(body.name).toBe(entry.name);
}
);
test(
'Create cat3 with article1',
async () => {
const entry = {
name: 'cat3',
articles: [data.articles[0]]
};
let body = await rq({
url: `/content-manager/explorer/category/?source=content-manager`,
method: 'POST',
formData: entry
});
body = JSON.parse(body);
data.categories.push(body);
expect(body.id);
expect(Array.isArray(body.articles)).toBeTruthy();
expect(body.articles.length).toBe(1);
expect(body.name).toBe(entry.name);
}
);
test(
'Get article1 with cat3',
async () => {
let body = await rq({
url: `/content-manager/explorer/article/${data.articles[0].id}?source=content-manager`,
method: 'GET'
});
body = JSON.parse(body);
expect(body.id);
expect(body.category.id).toBe(data.categories[2].id)
}
);
test(
'Get article2 with cat2',
async () => {
let body = await rq({
url: `/content-manager/explorer/article/${data.articles[1].id}?source=content-manager`,
method: 'GET'
});
body = JSON.parse(body);
expect(body.id);
expect(body.category.id).toBe(data.categories[1].id)
}
);
test(
'Get cat1 without relations',
async () => {
let body = await rq({
url: `/content-manager/explorer/category/${data.categories[0].id}?source=content-manager`,
method: 'GET'
});
body = JSON.parse(body);
expect(body.id);
expect(body.articles.length).toBe(0);
}
);
test(
'Get cat2 with article2',
async () => {
let body = await rq({
url: `/content-manager/explorer/category/${data.categories[1].id}?source=content-manager`,
method: 'GET'
});
body = JSON.parse(body);
expect(body.id);
expect(body.articles.length).toBe(1);
expect(body.articles[0].id).toBe(data.articles[1].id);
}
);
test(
'Get cat3 with article1',
async () => {
let body = await rq({
url: `/content-manager/explorer/category/${data.categories[2].id}?source=content-manager`,
method: 'GET'
});
body = JSON.parse(body);
expect(body.id);
expect(body.articles.length).toBe(1);
expect(body.articles[0].id).toBe(data.articles[0].id);
}
);
});
describe('Test oneToOne relation (article - reference) with Content Manager', () => {
beforeAll(() => {
data = {
articles: [],
references: []
};
});
beforeEach(async () => {
await restart(rq);
}, 60000);
test(
'Create ref1',
async () => {
let body = await rq({
url: `/content-manager/explorer/reference/?source=content-manager`,
method: 'POST',
formData: {
name: 'ref1'
}
});
body = JSON.parse(body);
data.references.push(body);
expect(body.id);
expect(body.name).toBe('ref1');
}
);
test(
'Create article1',
async () => {
const entry = {
title: 'Article 1',
content: 'Content 1'
};
let body = await rq({
url: `/content-manager/explorer/article?source=content-manager`,
method: 'POST',
formData: entry
});
body = JSON.parse(body);
data.articles.push(body);
expect(body.id);
expect(body.title).toBe(entry.title);
expect(body.content).toBe(entry.content);
}
);
test(
'Update article1 with ref1',
async () => {
const entry = Object.assign({}, data.articles[0], {
reference: data.references[0].id
});
cleanDate(entry);
let body = await rq({
url: `/content-manager/explorer/article/${entry.id}?source=content-manager`,
method: 'PUT',
formData: entry
});
body = JSON.parse(body);
data.articles[0] = body;
expect(body.id);
expect(body.title).toBe(entry.title);
expect(body.content).toBe(entry.content);
expect(body.reference.id).toBe(entry.reference);
}
);
test(
'Create article2 with ref1',
async () => {
const entry = {
title: 'Article 2',
content: 'Content 2',
reference: data.references[0].id
};
let body = await rq({
url: `/content-manager/explorer/article?source=content-manager`,
method: 'POST',
formData: entry
});
body = JSON.parse(body);
data.articles.push(body);
expect(body.id);
expect(body.title).toBe(entry.title);
expect(body.content).toBe(entry.content);
expect(body.reference.id).toBe(entry.reference);
}
);
test(
'Get article1 without relations',
async () => {
let body = await rq({
url: `/content-manager/explorer/article/${data.articles[0].id}?source=content-manager`,
method: 'GET'
});
body = JSON.parse(body);
expect(body.id);
expect(body.reference).toBe(null);
}
);
});
describe('Delete test APIs', () => {
beforeEach(async () => {
await restart(rq);
}, 60000);
test(
'Delete article API',
async () => {
await rq({
url: `/content-type-builder/models/article`,
method: 'DELETE',
json: true
});
}
);
test(
'Delete tag API',
async () => {
await rq({
url: `/content-type-builder/models/tag`,
method: 'DELETE',
json: true
});
}
);
test(
'Delete category API',
async () => {
await rq({
url: `/content-type-builder/models/category`,
method: 'DELETE',
json: true
});
}
);
});

96
test/start.js Normal file
View File

@ -0,0 +1,96 @@
const exec = require('child_process').exec;
const fs = require('fs-extra');
const path = require('path');
const strapiBin = path.resolve('./packages/strapi/bin/strapi.js');
const appName = 'testApp';
let appStart;
const databases = {
mongo: `--dbclient=mongo --dbhost=127.0.0.1 --dbport=27017 --dbname=strapi-test-${new Date().getTime()} --dbusername="" --dbpassword=""`,
postgres: `--dbclient=postgres --dbhost=127.0.0.1 --dbport=5432 --dbname=strapi-test --dbusername="" --dbpassword=""`,
mysql: `--dbclient=mysql --dbhost=127.0.0.1 --dbport=3306 --dbname=strapi-test --dbusername="root" --dbpassword="root"`
};
const {runCLI: jest} = require('jest-cli/build/cli');
const main = async () => {
const clean = () => {
return new Promise((resolve) => {
fs.exists(appName, exists => {
if (exists) {
fs.removeSync(appName);
}
resolve();
});
});
};
const generate = (database) => {
return new Promise((resolve, reject) => {
const appCreation = exec(
`node ${strapiBin} new ${appName} --dev ${database}`,
);
appCreation.stdout.on('data', data => {
console.log(data.toString());
if (data.includes('is ready at')) {
appCreation.kill();
return resolve();
}
if (data.includes('Database connection has failed')) {
appCreation.kill();
return reject();
}
});
});
};
const start = () => {
return new Promise((resolve) => {
appStart = exec(
`node ${strapiBin} start ${appName}`,
);
appStart.stdout.on('data', data => {
console.log(data.toString());
if (data.includes('To shut down your server')) {
return resolve();
}
});
});
};
const test = () => {
console.log('Launch test suits');
return new Promise(async (resolve) => {
const options = {
projects: [process.cwd()],
silent: false
};
await jest(options, options.projects);
resolve();
});
};
const testProcess = async (database) => {
await clean();
await generate(database);
await start();
await test();
appStart.kill();
};
await testProcess(databases.mongo);
await testProcess(databases.postgres);
await testProcess(databases.mysql);
};
main();