From 0b0dc404c224dee5fed48bbddbc29d9ff76d028c Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 13 Sep 2019 11:36:24 +1000 Subject: [PATCH] graphql: Added new package providing a graphql based backend --- .gitignore | 1 + packages/graphql/.babelrc.js | 14 +++ packages/graphql/.eslintrc | 8 ++ packages/graphql/package.json | 96 ++++++++++++++++++++ packages/graphql/rollup.config.js | 61 +++++++++++++ packages/graphql/src/index.js | 145 ++++++++++++++++++++++++++++++ 6 files changed, 325 insertions(+) create mode 100644 packages/graphql/.babelrc.js create mode 100644 packages/graphql/.eslintrc create mode 100644 packages/graphql/package.json create mode 100644 packages/graphql/rollup.config.js create mode 100644 packages/graphql/src/index.js diff --git a/.gitignore b/.gitignore index b615b04..b898df0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ dist build lib **/lib +yarn-error.log diff --git a/packages/graphql/.babelrc.js b/packages/graphql/.babelrc.js new file mode 100644 index 0000000..fc85202 --- /dev/null +++ b/packages/graphql/.babelrc.js @@ -0,0 +1,14 @@ +const { NODE_ENV, BABEL_ENV } = process.env +const cjs = NODE_ENV === 'test' || BABEL_ENV === 'commonjs' +const loose = true + +module.exports = { + presets: [['@babel/env', { loose, modules: false }]], + plugins: [ + ['@babel/proposal-decorators', { legacy: true }], + ['@babel/proposal-object-rest-spread', { loose }], + '@babel/transform-react-jsx', + cjs && ['@babel/transform-modules-commonjs', { loose }], + ['@babel/transform-runtime', { useESModules: !cjs }], + ].filter(Boolean), +} diff --git a/packages/graphql/.eslintrc b/packages/graphql/.eslintrc new file mode 100644 index 0000000..3a7c00c --- /dev/null +++ b/packages/graphql/.eslintrc @@ -0,0 +1,8 @@ +{ + "extends": "airbnb", + "rules": { + "react/jsx-props-no-spreading": "off", + "import/prefer-default-export": "off", + "react/prop-types": "off", + }, +} \ No newline at end of file diff --git a/packages/graphql/package.json b/packages/graphql/package.json new file mode 100644 index 0000000..a8ace03 --- /dev/null +++ b/packages/graphql/package.json @@ -0,0 +1,96 @@ +{ + "name": "@pihanga/graphql", + "version": "0.1.0", + "description": "Implemnts a graphql based backend for local state management", + "homepage": "https://github.com/n1analytics/pihanga#readme", + "main": "lib/index.js", + "repository": { + "type": "git", + "url": "git+https://github.com/n1analytics/pihanga.git" + }, + "bugs": "https://github.com/n1analytics/pihanga/issues", + "keywords": [ + "graphql", + "framework" + ], + "contributors": [ + "Max Ott (http://linkedin.com/in/max-ott)", + "Pihanga Team" + ], + "license": "MIT", + "unpkg": "dist/pihanga-graphql.min.js", + "module": "lib/index.js", + "files": [ + "dist", + "lib", + "src" + ], + "dependencies": { + "apollo-cache-inmemory": "^1.6.0", + "apollo-client": "^2.6.0", + "apollo-link": "^1.2.0", + "apollo-link-error": "^1.1.0", + "apollo-link-http": "^1.5.0", + "graphql": "^14.5.0", + "graphql-tag": "^2.10.0" + }, + "peerDependencies": { + "@pihanga/core": "^0.3.0" + }, + "devDependencies": { + "@pihanga/core": "^0.3.0", + "@babel/cli": "^7.5.0", + "@babel/core": "^7.5.0", + "@babel/plugin-proposal-decorators": "^7.4.0", + "@babel/plugin-proposal-object-rest-spread": "^7.5.0", + "@babel/plugin-transform-react-display-name": "^7.2.0", + "@babel/plugin-transform-react-jsx": "^7.3.0", + "@babel/plugin-transform-runtime": "^7.5.0", + "@babel/preset-env": "^7.5.0", + "babel-core": "^6.23.0", + "babel-eslint": "^10.0.2", + "babel-jest": "^24.9.0", + "codecov": "^3.5.0", + "cross-env": "^5.2.0", + "cross-spawn": "^6.0.5", + "es3ify": "^0.2.0", + "eslint": "^6.1.0", + "eslint-config-airbnb": "^18.0.1", + "eslint-plugin-import": "^2.18.0", + "eslint-plugin-jsx-a11y": "^6.2.0", + "eslint-plugin-react": "^7.14.0", + "eslint-plugin-react-hooks": "^1.7.0", + "glob": "^7.1.4", + "jest": "^24.9.0", + "jest-dom": "^3.5.0", + "npm-run": "^5.0.1", + "prettier": "^1.18.2", + "rimraf": "^2.7.1", + "rollup": "^1.19.4", + "rollup-plugin-babel": "^4.3.3", + "rollup-plugin-commonjs": "^9.3.4", + "rollup-plugin-node-resolve": "^4.2.4", + "rollup-plugin-replace": "^2.2.0", + "rollup-plugin-terser": "^4.0.4", + "semver": "^5.7.1" + }, + "scripts": { + "build:commonjs": "cross-env BABEL_ENV=commonjs babel src --out-dir lib && touch lib/DO_NOT_EDIT", + "build:es": "babel src --out-dir dist/es", + "build:umd": "cross-env NODE_ENV=development rollup -c -o dist/pihanga-core.js", + "build:umd:min": "cross-env NODE_ENV=production rollup -c -o dist/pihanga-core.min.js", + "xxx-build": "npm run build:commonjs && npm run build:es && npm run build:umd && npm run build:umd:min", + "build": "npm run build:commonjs && npm run build:es", + "clean": "rimraf lib dist coverage", + "format": "prettier --write \"{src,test}/**/*.{js,ts}\" index.d.ts \"docs/**/*.md\"", + "lint": "eslint src test/utils test/components", + "prepare": "npm run clean && npm run build", + "pretest": "npm run lint", + "test": "node ./test/run-tests.js", + "coverage": "codecov", + "publish": "npm publish --tag latest --access=public", + "x-prepublishOnly": "npm run build", + "x-prepublish": "yarn run lint && yarn run test && yarn run build" + }, + "gitHead": "aa43c27ff6344d6ead75a83e1d23ff69a7f4feb4" +} diff --git a/packages/graphql/rollup.config.js b/packages/graphql/rollup.config.js new file mode 100644 index 0000000..de892e3 --- /dev/null +++ b/packages/graphql/rollup.config.js @@ -0,0 +1,61 @@ +import nodeResolve from 'rollup-plugin-node-resolve' +import babel from 'rollup-plugin-babel' +import replace from 'rollup-plugin-replace' +import commonjs from 'rollup-plugin-commonjs' +import { terser } from 'rollup-plugin-terser' +import pkg from './package.json' + +const env = process.env.NODE_ENV + +const config = { + input: 'src/index.js', + external: Object.keys(pkg.peerDependencies || {}), + output: { + format: 'umd', + name: 'ReactRedux', + globals: { + react: 'React', + redux: 'Redux' + } + }, + plugins: [ + nodeResolve({ + mainFields: ['module', 'main'], + //jsnext: true, + extensions: ['.js', '.jsx', '.json'] + }), + babel({ + exclude: '**/node_modules/**', + runtimeHelpers: true + }), + replace({ + 'process.env.NODE_ENV': JSON.stringify(env) + }), + commonjs({ + namedExports: { + 'node_modules/react-is/index.js': [ + 'isValidElementType', + 'isContextConsumer' + ], + 'node_modules/@pihanga/core/lib/index.js': [ + 'dispatch' + ] + } + }) + ] +} + +if (env === 'production') { + config.plugins.push( + terser({ + compress: { + pure_getters: true, + unsafe: true, + unsafe_comps: true, + warnings: false + } + }) + ) +} + +export default config diff --git a/packages/graphql/src/index.js b/packages/graphql/src/index.js new file mode 100644 index 0000000..534f17a --- /dev/null +++ b/packages/graphql/src/index.js @@ -0,0 +1,145 @@ +import { ApolloClient } from 'apollo-client'; +import { InMemoryCache } from 'apollo-cache-inmemory'; +import { HttpLink } from 'apollo-link-http'; +import { onError } from 'apollo-link-error'; +import { ApolloLink } from 'apollo-link'; +import { registerActions, dispatch, dispatchFromReducer } from '@pihanga/core'; +import gql from 'graphql-tag'; +import { visit } from 'graphql'; + + +const Domain = 'GRAPHQL'; +export const ACTION_TYPES = registerActions(Domain, + ['QUERY_SUBMITTED', 'QUERY_RESULT', 'QUERY_ERROR', 'QL_ERROR', 'NETWORK_ERROR', 'NOT_INITIALISED']); + +let client; +let registerReducer; + +/** + * Standard pihanga init function to initialize an Apollo client. + * + * Expects all relevant configuration options to be found in + * `register.environment.GRAPHQL_URI` (needs most likely more). + * + * @param {*} register + */ +export function init(register) { + const uri = register.environment.GRAPHQL_URI || '/graphql'; + client = createClient(uri); + registerReducer = register.reducer; +} + +/** + * Register a GraphQL query and all related processing steps. + * + * The argument to this function is a map with the following key/value pairs. + * + * * query: The GraphQL query to be issued + * * trigger: The Redux action type to potentially trigger this query + * * request: A function expected to return a map of the variable assignmets for this query. + * If undefined is returned, no query is issued. The function is called with paramters: + * triggering action, current redux state, and a map describing the declared variables + * and their respective types. + * * reply: A function expected to return a possibly updated redux state in respond to a + * successful query result. The function is called witht paramters: the current redux state + * and the 'data' element of the returned query. + * * error: An optional function expected to return a possibly updated redux state in respond to a + * failed query. The function is called witht paramters: the current redux state + * and the error condition. + * + * + * @param {*} opts + */ +export function registerQuery({query, trigger, request, reply, error}) { + const {name, variables, doc} = parseQuery(query); + if (!trigger) { + throw Error('Missing "trigger"'); + } + if (!request) { + throw Error('Missing "request"'); + } + if (!reply) { + throw Error('Missing "reply"'); + } + + const submitType = `${ACTION_TYPES.QUERY_SUBMITTED}:${name}`; + const resultType = `${ACTION_TYPES.QUERY_RESULT}:${name}`; + const errorType = `${ACTION_TYPES.QUERY_ERROR}:${name}`; + + registerReducer(trigger, (state, action) => { + const vars = request(action, state, variables); + if (vars) { + runQuery(name, doc, vars, resultType, errorType); + dispatchFromReducer({type: submitType, queryID: name, vars}); + } + return state; + }); + + registerReducer(resultType, (state, action) => { + return reply(state, action.data); + }); + + if (error) { + registerReducer(errorType, (state, action) => { + return reply(state, error); + }); + } +} + +function parseQuery(query) { + const doc = gql(query); + let name = null; + const variables = {}; + const visitor = { + enter: { + OperationDefinition: (node) => { + name = node.name.value; + return undefined; + }, + VariableDefinition: (node) => { + const type = node.type.name.value; + const name = node.variable.name.value; + const defValue = node.defaultValue ? node.defaultValue.value : null; + variables[name] = {name, type, defValue}; + return undefined; + }, + } + }; + visit(doc, visitor); + return {name, variables, doc, query}; +} + +function runQuery(queryID, query, variables, resultType, errorType) { + if (client === null) { + dispatch({ type: ACTION_TYPES.NOT_INITIALISED, queryID }); + } + client.query({query, variables}) + .then(data => { + dispatch({ type: resultType, queryID, data: data.data }); + }) + .catch(error => { + dispatch({ type: errorType, queryID, error }); + }); +} + +function createClient(uri) { + return new ApolloClient({ + link: ApolloLink.from([ + onError(({ graphQLErrors, networkError }) => { + if (graphQLErrors) { + graphQLErrors.forEach(({ message, locations, path }) => { + dispatch({ type: ACTION_TYPES.QL_ERROR, message, locations, path }); + }); + } + if (networkError) { + dispatch({ type: ACTION_TYPES.NETWORK_ERROR, networkError }); + } + }), + new HttpLink({ + uri, + // credentials: 'same-origin' + }) + ]), + cache: new InMemoryCache() + }); +} \ No newline at end of file