Add GraphQL plugin and inject middleware

This commit is contained in:
Aurelsicoko 2018-03-27 17:15:28 +02:00
parent 15dda2f2cb
commit 4541b6f30b
40 changed files with 946 additions and 1 deletions

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,7 @@
root = true
[*]
end_of_line = lf
insert_final_newline = false
indent_style = space
indent_size = 2

103
packages/strapi-plugin-graphql/.gitattributes vendored Executable file
View File

@ -0,0 +1,103 @@
# From https://github.com/Danimoth/gitattributes/blob/master/Web.gitattributes
# Handle line endings automatically for files detected as text
# and leave all files detected as binary untouched.
* text=auto
#
# The above will handle all files NOT found below
#
#
## These files are text and should be normalized (Convert crlf => lf)
#
# source code
*.php text
*.css text
*.sass text
*.scss text
*.less text
*.styl text
*.js text eol=lf
*.coffee text
*.json text
*.htm text
*.html text
*.xml text
*.svg text
*.txt text
*.ini text
*.inc text
*.pl text
*.rb text
*.py text
*.scm text
*.sql text
*.sh text
*.bat text
# templates
*.ejs text
*.hbt text
*.jade text
*.haml text
*.hbs text
*.dot text
*.tmpl text
*.phtml text
# git config
.gitattributes text
.gitignore text
.gitconfig text
# code analysis config
.jshintrc text
.jscsrc text
.jshintignore text
.csslintrc text
# misc config
*.yaml text
*.yml text
.editorconfig text
# build config
*.npmignore text
*.bowerrc text
# Heroku
Procfile text
.slugignore text
# Documentation
*.md text
LICENSE text
AUTHORS text
#
## These files are binary and should be left untouched
#
# (binary is a macro for -text -diff)
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.mov binary
*.mp4 binary
*.mp3 binary
*.flv binary
*.fla binary
*.swf binary
*.gz binary
*.zip binary
*.7z binary
*.ttf binary
*.eot binary
*.woff binary
*.pyc binary
*.pdf binary

View File

@ -0,0 +1,10 @@
# Don't check auto-generated stuff into git
coverage
node_modules
stats.json
package-lock.json
# Cruft
.DS_Store
npm-debug.log
.idea

View File

@ -0,0 +1,101 @@
############################
# OS X
############################
.DS_Store
.AppleDouble
.LSOverride
Icon
.Spotlight-V100
.Trashes
._*
############################
# Linux
############################
*~
############################
# Windows
############################
Thumbs.db
ehthumbs.db
Desktop.ini
$RECYCLE.BIN/
*.cab
*.msi
*.msm
*.msp
############################
# Packages
############################
*.7z
*.csv
*.dat
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip
*.com
*.class
*.dll
*.exe
*.o
*.seed
*.so
*.swo
*.swp
*.swn
*.swm
*.out
*.pid
############################
# Logs and databases
############################
*.log
*.sql
############################
# Misc.
############################
*#
ssl
.idea
nbproject
############################
# Node.js
############################
lib-cov
lcov.info
pids
logs
results
node_modules
.node_history
############################
# Tests
############################
test
testApp
coverage

View File

@ -0,0 +1 @@
# Strapi plugin

View File

@ -0,0 +1,101 @@
############################
# OS X
############################
.DS_Store
.AppleDouble
.LSOverride
Icon
.Spotlight-V100
.Trashes
._*
############################
# Linux
############################
*~
############################
# Windows
############################
Thumbs.db
ehthumbs.db
Desktop.ini
$RECYCLE.BIN/
*.cab
*.msi
*.msm
*.msp
############################
# Packages
############################
*.7z
*.csv
*.dat
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip
*.com
*.class
*.dll
*.exe
*.o
*.seed
*.so
*.swo
*.swp
*.swn
*.swm
*.out
*.pid
############################
# Logs and databases
############################
*.log
*.sql
############################
# Misc.
############################
*#
ssl
.idea
nbproject
############################
# Node.js
############################
lib-cov
lcov.info
pids
logs
results
node_modules
.node_history
############################
# Tests
############################
test
testApp
coverage

View File

@ -0,0 +1,5 @@
/*
*
* App actions
*
*/

View File

@ -0,0 +1,5 @@
/*
*
* App constants
*
*/

View File

@ -0,0 +1,69 @@
/**
*
* This component is the skeleton around the actual pages, and should only
* contain code that should be seen on all pages. (e.g. navigation bar)
*
*/
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
import { Switch, Route } from 'react-router-dom';
import { bindActionCreators, compose } from 'redux';
// Utils
import { pluginId } from 'app';
// Containers
import HomePage from 'containers/HomePage';
import NotFoundPage from 'containers/NotFoundPage';
// When you're done studying the ExamplePage container, remove the following line and delete the ExamplePage container
import ExamplePage from 'containers/ExamplePage';
class App extends React.Component {
// When you're done studying the ExamplePage container, remove the following lines and delete the ExamplePage container
componentDidMount() {
this.props.history.push(`/plugins/${pluginId}/example`);
}
render() {
return (
<div className={pluginId}>
<Switch>
<Route path={`/plugins/${pluginId}`} component={HomePage} exact />
{/* When you're done studying the ExamplePage container, remove the following line and delete the ExamplePage container */}
<Route path={`/plugins/${pluginId}/example`} component={ExamplePage} exact />
<Route component={NotFoundPage} />
</Switch>
</div>
);
}
}
App.contextTypes = {
plugins: PropTypes.object,
router: PropTypes.object.isRequired,
updatePlugin: PropTypes.func,
};
App.propTypes = {
history: PropTypes.object.isRequired,
};
export function mapDispatchToProps(dispatch) {
return bindActionCreators(
{},
dispatch,
);
}
const mapStateToProps = createStructuredSelector({});
// Wrap the component to inject dispatch and state into it
const withConnect = connect(mapStateToProps, mapDispatchToProps);
export default compose(
withConnect,
)(App);

View File

@ -0,0 +1,18 @@
/*
*
* App reducer
*
*/
import { fromJS } from 'immutable';
const initialState = fromJS({});
function appReducer(state = initialState, action) {
switch (action.type) {
default:
return state;
}
}
export default appReducer;

View File

@ -0,0 +1,9 @@
// import { createSelector } from 'reselect';
/**
* Direct selector to the list state domain
*/
// const selectGlobalDomain = () => state => state.get('global');
export {};

View File

@ -0,0 +1,20 @@
/*
*
* ExamplePage actions
*
*/
import { LOAD_DATA, LOADED_DATA } from './constants';
export function loadData() {
return {
type: LOAD_DATA,
};
}
export function loadedData(data) {
return {
type: LOADED_DATA,
data,
};
}

View File

@ -0,0 +1,8 @@
/*
*
* ExamplePage constants
*
*/
export const LOAD_DATA = 'ExamplePage/LOAD_DATA';
export const LOADED_DATA = 'ExamplePage/LOADED_DATA';

View File

@ -0,0 +1,100 @@
/*
*
* ExamplePage
*
*/
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
import { injectIntl } from 'react-intl';
import { bindActionCreators, compose } from 'redux';
import injectReducer from 'utils/injectReducer';
import injectSaga from 'utils/injectSaga';
import Button from 'components/Button';
import styles from './styles.scss';
import { loadData } from './actions';
import { makeSelectLoading, makeSelectData } from './selectors';
import reducer from './reducer';
import saga from './saga';
export class ExamplePage extends React.Component {
generateDataBlock() {
if (this.props.data) {
const items = this.props.data.map((item, i) => <li key={i}>{item}</li>);
return (
<div>
<p>Data:</p>
<ul>{items}</ul>
</div>
);
}
return;
}
render() {
console.log('Don\'t forget to delete the ExampleContainer when you\'re done studying it');
// Generate the data block
const dataBlock = this.generateDataBlock();
return (
<div className={styles.examplePage}>
<div className="row">
<div className="col-md-12">
<p>This is an example of a fake API call.</p>
<p>Loading: {this.props.loading ? 'yes' : 'no'}.</p>
{dataBlock}
<Button
label={this.props.loading ? 'Loading...' : 'Submit'}
disabled={this.props.loading}
onClick={this.props.loadData}
primary
/>
</div>
</div>
</div>
);
}
}
ExamplePage.contextTypes = {
router: PropTypes.object,
};
ExamplePage.propTypes = {
data: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.object,
]).isRequired,
loadData: PropTypes.func.isRequired,
loading: PropTypes.bool.isRequired,
};
function mapDispatchToProps(dispatch) {
return bindActionCreators(
{
loadData,
},
dispatch,
);
}
const mapStateToProps = createStructuredSelector({
loading: makeSelectLoading(),
data: makeSelectData(),
});
const withConnect = connect(mapStateToProps, mapDispatchToProps);
const withReducer = injectReducer({ key: 'examplePage', reducer });
const withSaga = injectSaga({ key: 'examplePage', saga });
export default compose(
withReducer,
withSaga,
withConnect,
)(injectIntl(ExamplePage));

View File

@ -0,0 +1,27 @@
/*
*
* ExamplePage reducer
*
*/
import { fromJS } from 'immutable';
import { LOAD_DATA, LOADED_DATA } from './constants';
const initialState = fromJS({
loading: false,
data: false,
});
function examplePageReducer(state = initialState, action) {
switch (action.type) {
case LOAD_DATA:
return state.set('loading', true);
case LOADED_DATA:
return state.set('loading', false).set('data', fromJS(action.data));
default:
return state;
}
}
export default examplePageReducer;

View File

@ -0,0 +1,31 @@
import { LOCATION_CHANGE } from 'react-router-redux';
import { takeLatest, put, fork, take, cancel } from 'redux-saga/effects';
import { loadedData } from './actions';
import { LOAD_DATA } from './constants';
export function* loadData() {
// Fake API request delay
yield new Promise(resolve => {
setTimeout(() => {
resolve();
}, 1000);
});
// Generate a random array
const data = Array(4).fill(0).map(() => Math.floor(Math.random() * 100));
yield put(loadedData(data));
}
// Individual exports for testing
export function* defaultSaga() {
const loadDataWatcher = yield fork(takeLatest, LOAD_DATA, loadData);
// Suspend execution until location changes
yield take(LOCATION_CHANGE);
yield cancel(loadDataWatcher);
}
// All sagas to be loaded
export default defaultSaga;

View File

@ -0,0 +1,18 @@
import { createSelector } from 'reselect';
/**
* Direct selector to the examplePage state domain
*/
const selectExamplePageDomain = () => state => state.get('examplePage');
/**
* Default selector used by HomePage
*/
const makeSelectLoading = () =>
createSelector(selectExamplePageDomain(), substate => substate.get('loading'));
const makeSelectData = () =>
createSelector(selectExamplePageDomain(), substate => substate.get('data'));
export { makeSelectLoading, makeSelectData };

View File

@ -0,0 +1,3 @@
.examplePage {
}

View File

@ -0,0 +1,13 @@
/*
*
* HomePage actions
*
*/
import { DEFAULT_ACTION } from './constants';
export function defaultAction() {
return {
type: DEFAULT_ACTION,
};
}

View File

@ -0,0 +1,7 @@
/*
*
* HomePage constants
*
*/
export const DEFAULT_ACTION = 'HomePage/DEFAULT_ACTION';

View File

@ -0,0 +1,65 @@
/*
*
* HomePage
*
*/
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
import { injectIntl } from 'react-intl';
import { bindActionCreators, compose } from 'redux';
import injectReducer from 'utils/injectReducer';
import injectSaga from 'utils/injectSaga';
// Selectors
import selectHomePage from './selectors';
// Styles
import styles from './styles.scss';
import reducer from './reducer';
import saga from './saga';
export class HomePage extends React.Component {
render() {
return (
<div className={styles.homePage}>
</div>
);
}
}
HomePage.contextTypes = {
router: PropTypes.object,
};
HomePage.propTypes = {
// homePage: PropTypes.object,
};
function mapDispatchToProps(dispatch) {
return bindActionCreators(
{
// Your actions here
},
dispatch,
);
}
const mapStateToProps = createStructuredSelector({
homePage: selectHomePage(),
});
const withConnect = connect(mapStateToProps, mapDispatchToProps);
const withReducer = injectReducer({ key: 'homePage', reducer });
const withSaga = injectSaga({ key: 'homePage', saga });
export default compose(
withReducer,
withSaga,
withConnect,
)(injectIntl(HomePage));

View File

@ -0,0 +1,22 @@
/*
*
* HomePage reducer
*
*/
import { fromJS } from 'immutable';
import { DEFAULT_ACTION } from './constants';
const initialState = fromJS({});
function homePageReducer(state = initialState, action) {
switch (action.type) {
case DEFAULT_ACTION:
return state;
default:
return state;
}
}
export default homePageReducer;

View File

@ -0,0 +1,9 @@
// import { LOCATION_CHANGE } from 'react-router-redux';
// import { takeLatest, put, fork, take, cancel } from 'redux-saga/effects';
// Individual exports for testing
export function* defaultSaga() {
}
// All sagas to be loaded
export default defaultSaga;

View File

@ -0,0 +1,17 @@
import { createSelector } from 'reselect';
/**
* Direct selector to the homePage state domain
*/
const selectHomePageDomain = () => state => state.get('homePage');
/**
* Default selector used by HomePage
*/
const selectHomePage = () => createSelector(
selectHomePageDomain(),
(substate) => substate.toJS(),
);
export default selectHomePage;

View File

@ -0,0 +1,3 @@
.homePage {
}

View File

@ -0,0 +1,20 @@
/**
* NotFoundPage
*
* This is the page we show when the user visits a url that doesn't have a route
*
* NOTE: while this component should technically be a stateless functional
* component (SFC), hot reloading does not currently support SFCs. If hot
* reloading is not a neccessity for you then you can refactor it and remove
* the linting exception.
*/
import React from 'react';
import NotFound from 'components/NotFound';
export default class NotFoundPage extends React.Component {
render() {
return <NotFound {...this.props} />;
}
}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,3 @@
{
"routes": []
}

View File

@ -0,0 +1,10 @@
'use strict';
/**
* GraphQL.js controller
*
* @description: A set of functions called "actions" of the `GraphQL` plugin.
*/
module.exports = {};

View File

@ -0,0 +1,5 @@
{
"graphql": {
"enabled": true
}
}

View File

@ -0,0 +1,66 @@
'use strict';
/**
* Module dependencies
*/
// Public node modules.
const { graphqlKoa, graphiqlKoa } = require('apollo-server-koa');
const { makeExecutableSchema } = require('graphql-tools');
const books = [
{
title: "Harry Potter and the Sorcerer's stone",
author: 'J.K. Rowling',
},
{
title: 'Jurassic Park',
author: 'Michael Crichton',
},
];
// The GraphQL schema in string form
const typeDefs = `
type Query { books: [Book] }
type Book { title: String, author: String }
`;
// The resolvers
const resolvers = {
Query: { books: () => books },
};
// Put together a schema
const schema = makeExecutableSchema({
typeDefs,
resolvers,
});
module.exports = strapi => {
return {
beforeInitialize: function() {
// Try to inject this middleware just after the parser to skip the router processing.
const index = strapi.config.middleware.load.after.indexOf('parser');
if (index !== -1) {
strapi.config.middleware.load.after.splice(index + 1, 0, 'graphql');
} else {
strapi.config.middleware.load.after.push('graphql');
}
},
initialize: function(cb) {
const endpoint = '/graphql';
const router = strapi.koaMiddlewares.routerJoi();
router.post(endpoint, graphqlKoa({ schema }));
router.get(endpoint, graphqlKoa({ schema }));
router.get('/graphiql', graphiqlKoa({ endpointURL: endpoint }));
strapi.app.use(router.middleware());
cb();
}
};
};

View File

@ -0,0 +1,50 @@
{
"name": "strapi-plugin-graphql",
"version": "0.0.0",
"description": "This is the description of the plugin.",
"strapi": {
"name": "graphql",
"icon": "plug",
"description": "Description of graphql plugin."
},
"scripts": {
"analyze:clean": "node ./node_modules/strapi-helper-plugin/node_modules/.bin/rimraf stats.json",
"preanalyze": "npm run analyze:clean",
"analyze": "node ./node_modules/strapi-helper-plugin/lib/internals/scripts/analyze.js",
"prebuild": "npm run build:clean && npm run test",
"build:dev": "node ./node_modules/strapi-helper-plugin/node_modules/.bin/cross-env NODE_ENV=development node ./node_modules/strapi-helper-plugin/node_modules/.bin/webpack --config node_modules/strapi-helper-plugin/lib/internals/webpack/webpack.prod.babel.js --color -p --progress",
"build": "node ./node_modules/strapi-helper-plugin/node_modules/.bin/cross-env NODE_ENV=production node node_modules/strapi-helper-plugin/node_modules/.bin/webpack --config node_modules/strapi-helper-plugin/lib/internals/webpack/webpack.prod.babel.js --color -p --progress",
"build:clean": "node ./node_modules/strapi-helper-plugin/node_modules/.bin/rimraf admin/build",
"start": "node ./node_modules/strapi-helper-plugin/node_modules/.bin/cross-env NODE_ENV=development node ./node_modules/strapi-helper-plugin/lib/server",
"generate": "node ./node_modules/plop/plop.js --plopfile node_modules/strapi-helper-plugin/lib/internals/generators/index.js",
"lint": "node ./node_modules/strapi-helper-plugin/node_modules/.bin/eslint --ignore-path .gitignore --ignore-pattern '/admin/build/' --config ./node_modules/strapi-helper-plugin/lib/internals/eslint/.eslintrc.json admin",
"prettier": "node ./node_modules/strapi-helper-plugin/node_modules/.bin/prettier --single-quote --trailing-comma es5 --write \"{admin,__{tests,mocks}__}/**/*.js\"",
"test": "npm run lint",
"prepublishOnly": "npm run build"
},
"dependencies": {
"apollo-server-koa": "^1.3.3",
"graphql": "^0.13.2",
"graphql-tools": "^2.23.1"
},
"devDependencies": {
"strapi-helper-plugin": "3.0.0-alpha.11.1"
},
"author": {
"name": "A Strapi developer",
"email": "",
"url": ""
},
"maintainers": [
{
"name": "A Strapi developer",
"email": "",
"url": ""
}
],
"engines": {
"node": ">= 7.0.0",
"npm": ">= 3.0.0"
},
"license": "MIT"
}

View File

@ -0,0 +1,9 @@
'use strict';
/**
* GraphQL.js service
*
* @description: A set of functions similar to controller's actions to avoid code duplication.
*/
module.exports = {};

View File

@ -120,6 +120,10 @@ shell.rm('-f', 'package-lock.json');
watcher('📦 Linking strapi-plugin-content-type-builder...', 'npm link --no-optional', false);
watcher('🏗 Building...', 'npm run build');
shell.cd('../strapi-plugin-graphql');
shell.rm('-f', 'package-lock.json');
watcher('📦 Linking strapi-plugin-graphql...', 'npm link --no-optional', false);
// Log installation duration.
const installationEndDate = new Date();
const duration = (installationEndDate.getTime() - installationStartDate.getTime()) / 1000;