├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── bin ├── _lib.sh ├── check_nulls ├── create_migration ├── fixtures ├── migrate └── sync_tests ├── build ├── config.js ├── utilities.js └── webpack │ ├── base.js │ ├── fixtures.js │ ├── frontend.js │ ├── karma.js │ ├── migrate.js │ └── webserver.js ├── gulpfile.babel.js ├── karma.conf.js ├── package.json ├── public └── humans.txt ├── src ├── api │ ├── index.js │ └── usersPageApiRequest.js ├── commands │ ├── fixtures.js │ ├── migrate.js │ └── webserver.js ├── components │ ├── AboutPage │ │ ├── AboutPage.js │ │ └── index.js │ ├── Application │ │ ├── Application.js │ │ └── index.js │ ├── ErrorPage │ │ ├── ErrorPage.js │ │ └── index.js │ ├── HomePage │ │ ├── HomePage.js │ │ └── index.js │ ├── Link │ │ ├── Link.js │ │ └── index.js │ ├── NotFoundPage │ │ ├── NotFoundPage.js │ │ └── index.js │ ├── SignIn │ │ ├── SignIn.js │ │ └── index.js │ ├── UsersPage │ │ ├── Item.js │ │ ├── Order.js │ │ ├── Position.js │ │ ├── User.js │ │ ├── UsersPage.js │ │ └── index.js │ └── pages.js ├── config │ ├── index.js │ └── routing.js ├── fixtures │ ├── 01-users.js │ ├── 02-items.js │ ├── 03-orders.js │ └── 04-orderpositions.js ├── flux │ ├── resources │ │ ├── resourcesActions.js │ │ ├── resourcesConstants.js │ │ └── resourcesStore.js │ ├── router │ │ ├── routerActions.js │ │ ├── routerConstants.js │ │ └── routerStore.js │ ├── signIn │ │ ├── signInActions.js │ │ ├── signInConstants.js │ │ ├── signInStore.js │ │ └── signInValidator.js │ ├── stores.js │ ├── token │ │ ├── tokenActions.js │ │ ├── tokenConstants.js │ │ └── tokenStore.js │ └── transition │ │ ├── createFetchTransition.js │ │ ├── createStaticTransition.js │ │ └── transitionActions.js ├── frontend │ ├── index.js │ └── polyfills.js ├── migrations │ ├── m20150624_180904_user.js │ ├── m20150625_113856_item.js │ ├── m20150625_113950_order.js │ ├── m20150625_114024_userorder.js │ ├── m20150625_114102_orderposition.js │ ├── m20150625_161547_accesstoken.js │ └── m20150625_161713_usertoken.js ├── schemas │ ├── AccessToken.js │ ├── Item.js │ ├── Order.js │ ├── OrderPosition.js │ ├── User.js │ └── index.js ├── utilities │ ├── apiRequest.js │ ├── bindAction.js │ ├── collectResources.js │ ├── createDispatcher.js │ ├── createStore.js │ ├── getCurrentUser.js │ ├── patchReact.js │ ├── preventEvent.js │ └── progressIndicator.js └── webserver │ ├── db │ ├── applyTransaction.js │ ├── errors.js │ ├── fulfillQuery.js │ ├── mapObjectToResource.js │ └── mapResourceToTransaction.js │ ├── di │ ├── app.js │ ├── config.js │ ├── cookieAuthMiddleware.js │ ├── orientoDb.js │ ├── orientoDbMiddleware.js │ ├── orientoServer.js │ ├── passport.js │ └── wrapPassportMiddleware.js │ ├── handlers │ ├── index.js │ ├── tokens.js │ ├── users.js │ └── xhr.js │ ├── index.js │ ├── polyfills.js │ └── utilities │ ├── appTemplate.js │ ├── getParamsFromRequest.js │ ├── renderPage.js │ ├── validateResourceBySchema.js │ └── wrapHandler.js ├── test ├── .eslintrc ├── api │ ├── indexTest.js │ └── usersPageApiRequestTest.js ├── components │ ├── AboutPage │ │ ├── AboutPageTest.js │ │ └── indexTest.js │ ├── Application │ │ ├── ApplicationTest.js │ │ └── indexTest.js │ ├── ErrorPage │ │ ├── ErrorPageTest.js │ │ └── indexTest.js │ ├── HomePage │ │ ├── HomePageTest.js │ │ └── indexTest.js │ ├── Link │ │ ├── LinkTest.js │ │ └── indexTest.js │ ├── NotFoundPage │ │ ├── NotFoundPageTest.js │ │ └── indexTest.js │ ├── SignIn │ │ ├── SignInTest.js │ │ └── indexTest.js │ ├── UsersPage │ │ ├── ItemTest.js │ │ ├── OrderTest.js │ │ ├── PositionTest.js │ │ ├── UserTest.js │ │ ├── UsersPageTest.js │ │ └── indexTest.js │ └── pagesTest.js ├── config │ └── routingTest.js ├── flux │ ├── resources │ │ ├── resourcesActionsTest.js │ │ ├── resourcesConstantsTest.js │ │ └── resourcesStoreTest.js │ ├── router │ │ ├── routerActionsTest.js │ │ ├── routerConstantsTest.js │ │ └── routerStoreTest.js │ ├── signIn │ │ ├── signInActionsTest.js │ │ ├── signInConstantsTest.js │ │ ├── signInStoreTest.js │ │ └── signInValidatorTest.js │ ├── storesTest.js │ ├── token │ │ ├── tokenActionsTest.js │ │ ├── tokenConstantsTest.js │ │ └── tokenStoreTest.js │ └── transition │ │ ├── createFetchTransitionTest.js │ │ ├── createStaticTransitionTest.js │ │ └── transitionActionsTest.js ├── schemas │ ├── AccessTokenTest.js │ ├── ItemTest.js │ ├── OrderPositionTest.js │ ├── OrderTest.js │ ├── UserTest.js │ └── indexTest.js ├── utilities │ ├── apiRequestTest.js │ ├── bindActionTest.js │ ├── collectResourcesTest.js │ ├── createDispatcherTest.js │ ├── createStoreTest.js │ ├── getCurrentUserTest.js │ ├── patchReactTest.js │ ├── preventEventTest.js │ └── progressIndicatorTest.js └── webserver │ ├── db │ ├── applyTransactionTest.js │ ├── errorsTest.js │ ├── fulfillQueryTest.js │ ├── mapObjectToResourceTest.js │ └── mapResourceToTransactionTest.js │ ├── handlers │ ├── indexTest.js │ ├── tokensTest.js │ ├── usersTest.js │ └── xhrTest.js │ └── utilities │ ├── appTemplateTest.js │ ├── getParamsFromRequestTest.js │ ├── renderPageTest.js │ ├── validateResourceBySchemaTest.js │ └── wrapHandlerTest.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0, 3 | "optional": [ 4 | "runtime" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "plugins": [ 4 | "react", 5 | "babel" 6 | ], 7 | "ecmaFeatures": { 8 | "modules": true 9 | }, 10 | "env": { 11 | "browser": true, 12 | "node": true, 13 | "es6": true 14 | }, 15 | "rules": { 16 | "comma-dangle": [2, "never"], 17 | "no-cond-assign": [2, "always"], 18 | "no-console": 2, 19 | "no-constant-condition": 2, 20 | "no-control-regex": 2, 21 | "no-debugger": 2, 22 | "no-dupe-args": 2, 23 | "no-dupe-keys": 2, 24 | "no-duplicate-case": 2, 25 | "no-empty-character-class": 2, 26 | "no-empty": 2, 27 | "no-ex-assign": 2, 28 | "no-extra-boolean-cast": 0, 29 | "no-extra-parens": 0, 30 | "no-extra-semi": 2, 31 | "no-func-assign": 2, 32 | "no-inner-declarations": [2, "both"], 33 | "no-invalid-regexp": 2, 34 | "no-irregular-whitespace": 2, 35 | "no-negated-in-lhs": 2, 36 | "no-obj-calls": 2, 37 | "no-regex-spaces": 2, 38 | "no-reserved-keys": 0, 39 | "no-sparse-arrays": 2, 40 | "no-unreachable": 2, 41 | "use-isnan": 2, 42 | "valid-jsdoc": 0, 43 | "valid-typeof": 2, 44 | 45 | "accessor-pairs": [2, {"setWithoutGet": false}], 46 | "block-scoped-var": 0, 47 | "complexity": 0, 48 | "consistent-return": 0, 49 | "curly": [2, "all"], 50 | "default-case": 2, 51 | "dot-notation": 0, 52 | "dot-location": [2, "property"], 53 | "eqeqeq": 2, 54 | "guard-for-in": 2, 55 | "no-alert": 2, 56 | "no-caller": 2, 57 | "no-div-regex": 2, 58 | "no-else-return": 0, 59 | "no-empty-label": 2, 60 | "no-eq-null": 2, 61 | "no-eval": 2, 62 | "no-extend-native": 2, 63 | "no-extra-bind": 2, 64 | "no-fallthrough": 0, 65 | "no-floating-decimal": 2, 66 | "no-implied-eval": 2, 67 | "no-iterator": 2, 68 | "no-labels": 2, 69 | "no-lone-blocks": 0, 70 | "no-loop-func": 0, 71 | "no-multi-spaces": 2, 72 | "no-multi-str": 2, 73 | "no-native-reassign": 2, 74 | "no-new-func": 2, 75 | "no-new-wrappers": 2, 76 | "no-new": 2, 77 | "no-octal-escape": 2, 78 | "no-octal": 2, 79 | "no-param-reassign": 0, 80 | "no-process-env": 0, 81 | "no-proto": 2, 82 | "no-redeclare": 2, 83 | "no-return-assign": 2, 84 | "no-script-url": 2, 85 | "no-self-compare": 2, 86 | "no-sequences": 2, 87 | "no-throw-literal": 2, 88 | "no-unused-expressions": 2, 89 | "no-void": 2, 90 | "no-warning-comments": [1, {"terms": ["todo", "fixme", "xxx"], "location": "start"}], 91 | "no-with": 2, 92 | "radix": 0, 93 | "vars-on-top": 0, 94 | "wrap-iife": [2, "inside"], 95 | "yoda": [2, "never"], 96 | 97 | "strict": [2, "never"], 98 | 99 | "no-catch-shadow": 2, 100 | "no-delete-var": 2, 101 | "no-label-var": 2, 102 | "no-shadow-restricted-names": 2, 103 | "no-shadow": 0, 104 | "no-undef-init": 2, 105 | "no-undef": 2, 106 | "no-undefined": 0, 107 | "no-unused-vars": [2, {"vars": "all", "args": "after-used"}], 108 | "no-use-before-define": [2, "nofunc"], 109 | 110 | "handle-callback-err": [2, "error"], 111 | "no-mixed-requires": [2, false], 112 | "no-new-require": 2, 113 | "no-path-concat": 2, 114 | "no-process-exit": 2, 115 | "no-restricted-modules": 0, 116 | "no-sync": 0, 117 | 118 | "brace-style": [2, "1tbs"], 119 | "camelcase": 2, 120 | "comma-spacing": 2, 121 | "comma-style": [2, "last"], 122 | "computed-property-spacing": [2, "never"], 123 | "consistent-this": [2, "that"], 124 | "eol-last": 2, 125 | "func-names": 0, 126 | "func-style": 0, 127 | "indent": [2, 2], 128 | "key-spacing": [2, {"beforeColon": false, "afterColon": true}], 129 | "lines-around-comment": 2, 130 | "linebreak-style": [2, "unix"], 131 | "max-nested-callbacks": [1, 3], 132 | "new-cap": 2, 133 | "new-parens": 2, 134 | "newline-after-var": 2, 135 | "no-array-constructor": 2, 136 | "no-continue": 0, 137 | "no-inline-comments": 0, 138 | "no-lonely-if": 2, 139 | "no-mixed-spaces-and-tabs": [2, false], 140 | "no-multiple-empty-lines": [2, {"max": 2}], 141 | "no-nested-ternary": 2, 142 | "no-new-object": 2, 143 | "no-spaced-func": 2, 144 | "no-ternary": 0, 145 | "no-trailing-spaces": 2, 146 | "no-underscore-dangle": 2, 147 | "no-unneeded-ternary": 2, 148 | "no-wrap-func": 2, 149 | "object-curly-spacing": [2, "never"], 150 | "one-var": [2, "never"], 151 | "operator-assignment": [2, "always"], 152 | "operator-linebreak": [2, "before"], 153 | "padded-blocks": 0, 154 | "quote-props": 0, 155 | "quotes": [2, "single"], 156 | "semi-spacing": [2, {"before": false, "after": true}], 157 | "semi": [2, "never"], 158 | "sort-vars": [2, {"ignoreCase": false}], 159 | "space-after-keywords": [2, "always"], 160 | "space-before-blocks": [2, "always"], 161 | "space-before-function-paren": [2, "never"], 162 | "space-in-brackets": [2, "never"], 163 | "space-in-parens": [2, "never"], 164 | "space-infix-ops": 2, 165 | "space-return-throw-case": 2, 166 | "space-unary-ops": [2, {"words": true, "nonwords": false}], 167 | "spaced-comment": [2, "always"], 168 | "wrap-regex": 0, 169 | 170 | "generator-star-spacing": 0, 171 | "no-var": 2, 172 | "object-shorthand": 0, 173 | "prefer-const": 2, 174 | 175 | "max-depth": [1, 3], 176 | "max-len": [1, 120, 2], 177 | "max-params": [1, 3], 178 | "max-statements": [1, 25], 179 | "no-bitwise": 2, 180 | "no-plusplus": 0, 181 | 182 | "react/display-name": 0, 183 | "react/jsx-boolean-value": [2, "never"], 184 | "react/jsx-no-undef": 2, 185 | "react/jsx-quotes": [2, "double"], 186 | "react/jsx-sort-prop-types": 0, 187 | "react/jsx-sort-props": 0, 188 | "react/jsx-uses-react": 2, 189 | "react/jsx-uses-vars": 2, 190 | "react/no-did-mount-set-state": 2, 191 | "react/no-did-update-set-state": 2, 192 | "react/no-multi-comp": 0, 193 | "react/no-unknown-property": 2, 194 | "react/prop-types": 0, 195 | "react/react-in-jsx-scope": 0, 196 | "react/self-closing-comp": 2, 197 | "react/sort-comp": 2, 198 | "react/wrap-multilines": 2, 199 | 200 | "babel/object-shorthand": 2, 201 | "babel/generator-star-spacing": [2, "before"] 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.app/ 2 | /.test/ 3 | /node_modules/ 4 | 5 | /env.sh 6 | /npm-debug.log 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Vyacheslav Slinko 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED 2 | 3 | Look for [ripster](https://github.com/vslinko/ripster) instead. 4 | -------------------------------------------------------------------------------- /bin/_lib.sh: -------------------------------------------------------------------------------- 1 | create_env_sh() { 2 | cat << END > env.sh 3 | # Define your environment variables here 4 | 5 | \$* 6 | END 7 | 8 | chmod a+x env.sh 9 | } 10 | 11 | ensure_env_sh() { 12 | if [ ! -x env.sh ]; then 13 | create_env_sh 14 | fi 15 | } 16 | -------------------------------------------------------------------------------- /bin/check_nulls: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd "$(dirname $0)/.." 4 | 5 | grep null -R build src test webpack.config.js gulpfile.babel.js | grep -v null-reasonable 6 | 7 | if [ $? == 0 ]; then 8 | exit 1 9 | else 10 | exit 0 11 | fi 12 | -------------------------------------------------------------------------------- /bin/create_migration: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | name="$1" 4 | 5 | if [ "x$name" == "x" ]; then 6 | echo "Usage: $0 MIGRATION_NAME" 7 | exit 1 8 | fi 9 | 10 | cd "$(dirname $0)/.." 11 | 12 | mkdir -p src/migrations 13 | 14 | file_name="m$(date "+%Y%m%d")_$(date "+%H%M%S")_${name}.js" 15 | 16 | echo $file_name 17 | 18 | cat << END > "src/migrations/$file_name" 19 | export async function up(db) { 20 | 21 | } 22 | 23 | export async function down(db) { 24 | 25 | } 26 | END 27 | -------------------------------------------------------------------------------- /bin/fixtures: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd "$(dirname $0)/.." 4 | 5 | . ./bin/_lib.sh 6 | 7 | ensure_env_sh 8 | 9 | ./node_modules/.bin/gulp _build-compile-fixtures >/dev/null 10 | ./env.sh node ./.app/fixtures.js 11 | -------------------------------------------------------------------------------- /bin/migrate: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd "$(dirname $0)/.." 4 | 5 | . ./bin/_lib.sh 6 | 7 | ensure_env_sh 8 | 9 | ./node_modules/.bin/gulp _build-compile-migrate >/dev/null 10 | ./env.sh node ./.app/migrate.js 11 | -------------------------------------------------------------------------------- /bin/sync_tests: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd "$(dirname $0)/.." 4 | 5 | files=$(find src -regex '.*.js') 6 | 7 | for file in $files; do 8 | if [[ $file == "src/webserver/di/"* ]]; then 9 | continue 10 | fi 11 | if [[ $file == "src/webserver/index.js" ]]; then 12 | continue 13 | fi 14 | if [[ $file == "src/frontend/index.js" ]]; then 15 | continue 16 | fi 17 | if [[ $file == *"/polyfills.js" ]]; then 18 | continue 19 | fi 20 | if [[ $file == "src/migrations/"* ]]; then 21 | continue 22 | fi 23 | if [[ $file == "src/fixtures/"* ]]; then 24 | continue 25 | fi 26 | if [[ $file == "src/commands/"* ]]; then 27 | continue 28 | fi 29 | 30 | test_file=${file/src/test} 31 | test_file=${test_file/.js/Test.js} 32 | 33 | test_dir=$(dirname $test_file) 34 | 35 | var_name=$(basename $file) 36 | var_name=${var_name/.js/} 37 | 38 | test_name=${file/src\//} 39 | test_name=${test_name/.js/} 40 | 41 | levels=$(echo $test_dir | tr -cd / | wc -c) 42 | path_to_file="" 43 | 44 | while (( $levels >= 0 )); do 45 | path_to_file="$path_to_file../" 46 | ((levels--)) 47 | done 48 | 49 | path_to_file="$path_to_file$file" 50 | path_to_file=${path_to_file/.js/} 51 | 52 | mkdir -p $test_dir 53 | 54 | if [ -r $test_file ]; then 55 | continue 56 | fi 57 | 58 | cat << END > $test_file 59 | import $var_name from '$path_to_file' // eslint-disable-line 60 | 61 | describe('$test_name', () => { 62 | it('should work') 63 | }) 64 | END 65 | 66 | echo "Created $test_file" 67 | done 68 | -------------------------------------------------------------------------------- /build/config.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs' 3 | 4 | const rootDirectory = path.join(__dirname, '..') 5 | const sourceDirectory = path.join(rootDirectory, 'src') 6 | const publicDirectory = path.join(rootDirectory, 'public') 7 | const testDirectory = path.join(rootDirectory, 'test') 8 | const destinationDirectory = path.join(rootDirectory, '.app') 9 | const destinationPublicDirectory = path.join(destinationDirectory, 'public') 10 | 11 | const production = process.env.NODE_ENV === 'production' 12 | const development = !production 13 | const hotReload = development && process.env.HOT_RELOAD === 'react-hot-loader' 14 | 15 | const nodeModulesExternals = fs.readdirSync(path.join(rootDirectory, 'node_modules')) 16 | .filter(dir => dir !== '.bin') 17 | .reduce((acc, name) => (acc[name] = `commonjs ${name}`, acc), {}) 18 | 19 | { 20 | const envScript = path.join(rootDirectory, 'env.sh') 21 | 22 | if (fs.existsSync(envScript)) { 23 | fs.readFileSync(envScript) 24 | .toString() 25 | .split('\n') 26 | .filter(line => /^export /.test(line)) 27 | .map(line => line.split('=')) 28 | .map(parts => [parts[0].split(' ').pop(), parts[1]]) 29 | .forEach(([key, value]) => process.env[key] = value) 30 | } 31 | } 32 | 33 | export default { 34 | rootDirectory, 35 | sourceDirectory, 36 | publicDirectory, 37 | testDirectory, 38 | destinationDirectory, 39 | destinationPublicDirectory, 40 | 41 | production, 42 | development, 43 | hotReload, 44 | 45 | nodeModulesExternals 46 | } 47 | -------------------------------------------------------------------------------- /build/utilities.js: -------------------------------------------------------------------------------- 1 | import childProcess from 'child_process' 2 | 3 | export const useIf = (condition, value) => 4 | condition ? value : undefined 5 | 6 | export const prepareArray = (array) => 7 | array.filter(item => item !== undefined) 8 | 9 | export const mergeArrays = (...arrays) => 10 | arrays.reduce((left, right) => [...left, ...right]) 11 | 12 | export const mergeObjects = (...objects) => 13 | Object.assign({}, ...objects) 14 | 15 | export const webpackCallback = ({onReady, onChange}) => { 16 | let first = true 17 | 18 | return (error, stats) => { 19 | if (error) { 20 | console.log(error) // eslint-disable-line no-console 21 | } else { 22 | console.log(stats.toString()) // eslint-disable-line no-console 23 | } 24 | 25 | if (first) { 26 | first = false 27 | if (onReady) { 28 | onReady() 29 | } 30 | } else if (onChange) { 31 | onChange() 32 | } 33 | } 34 | } 35 | 36 | export const runCommand = (command, args, callback) => { 37 | const options = { 38 | stdio: 'inherit' 39 | } 40 | 41 | childProcess.spawn(command, args, options) 42 | .on('close', code => 43 | code > 0 44 | ? callback(new Error(command + ' exited with code ' + code)) 45 | : callback() 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /build/webpack/base.js: -------------------------------------------------------------------------------- 1 | import {DefinePlugin, ProvidePlugin, optimize} from 'webpack' 2 | 3 | import {useIf, prepareArray} from '../utilities' 4 | import config from '../config' 5 | 6 | export default { 7 | module: { 8 | loaders: [ 9 | { 10 | test: /\.js$/, 11 | include: [config.sourceDirectory], 12 | loaders: ['babel'] 13 | } 14 | ] 15 | }, 16 | 17 | debug: config.development, 18 | 19 | devtool: 'source-map', 20 | 21 | plugins: prepareArray([ 22 | new DefinePlugin({ 23 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development') 24 | }), 25 | 26 | new ProvidePlugin({ 27 | 'React': 'react' 28 | }), 29 | 30 | useIf(config.production, new optimize.UglifyJsPlugin()) 31 | ]) 32 | } 33 | -------------------------------------------------------------------------------- /build/webpack/fixtures.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | import {mergeObjects} from '../utilities' 4 | import config from '../config' 5 | import base from './base' 6 | 7 | export default mergeObjects(base, { 8 | entry: path.join(config.sourceDirectory, 'commands', 'fixtures'), 9 | 10 | output: { 11 | path: path.join(config.destinationDirectory), 12 | filename: 'fixtures.js' 13 | }, 14 | 15 | target: 'node', 16 | node: { 17 | console: false, 18 | process: false, 19 | global: false, 20 | Buffer: false, 21 | __filename: false, 22 | __dirname: false 23 | }, 24 | 25 | externals: [ 26 | config.nodeModulesExternals 27 | ] 28 | }) 29 | -------------------------------------------------------------------------------- /build/webpack/frontend.js: -------------------------------------------------------------------------------- 1 | import {DefinePlugin, HotModuleReplacementPlugin} from 'webpack' 2 | import path from 'path' 3 | 4 | import {useIf, prepareArray, mergeArrays, mergeObjects} from '../utilities' 5 | import config from '../config' 6 | import base from './base' 7 | 8 | export default mergeObjects(base, { 9 | entry: prepareArray([ 10 | useIf(config.hotReload, `webpack-dev-server/client?`), 11 | useIf(config.hotReload, 'webpack/hot/dev-server'), 12 | path.join(config.sourceDirectory, 'frontend') 13 | ]), 14 | 15 | output: { 16 | path: config.destinationPublicDirectory, 17 | filename: 'frontend.js', 18 | publicPath: '/', 19 | library: 'frontend' 20 | }, 21 | 22 | module: mergeObjects(base.module, { 23 | loaders: mergeArrays( 24 | [ 25 | { 26 | test: /\.js$/, 27 | include: [config.sourceDirectory], 28 | loaders: ['react-hot-loader'] 29 | }, 30 | { 31 | test: /\.css$/, 32 | loaders: ['style', 'css'] 33 | } 34 | ], 35 | base.module.loaders 36 | ) 37 | }), 38 | 39 | plugins: mergeArrays( 40 | prepareArray([ 41 | new DefinePlugin({ 42 | 'process.env.BACKEND_ADDRESS': JSON.stringify(process.env.BACKEND_ADDRESS || '') 43 | }), 44 | useIf(config.hotReload, new HotModuleReplacementPlugin()) 45 | ]), 46 | base.plugins 47 | ), 48 | 49 | devServer: { 50 | contentBase: config.destinationPublicDirectory, 51 | filename: 'frontend.js', 52 | publicPath: '/', 53 | hot: config.hotReload, 54 | proxy: { 55 | '*': `http://localhost:${process.env.PORT}` 56 | } 57 | } 58 | }) 59 | -------------------------------------------------------------------------------- /build/webpack/karma.js: -------------------------------------------------------------------------------- 1 | import {mergeObjects, mergeArrays} from '../utilities' 2 | import config from '../config' 3 | import base from './base' 4 | 5 | export default mergeObjects(base, { 6 | module: mergeObjects(base.module, { 7 | loaders: mergeArrays( 8 | base.module.loaders, 9 | [ 10 | { 11 | test: /\.js$/, 12 | include: [config.testDirectory], 13 | loaders: ['babel'] 14 | } 15 | ] 16 | ).map(loaderConfig => { 17 | if (loaderConfig.include && loaderConfig.include.indexOf(config.sourceDirectory) >= 0) { 18 | loaderConfig = mergeObjects(loaderConfig, { 19 | loaders: ['isparta'] 20 | }) 21 | } 22 | 23 | return loaderConfig 24 | }) 25 | }), 26 | 27 | devtool: undefined 28 | }) 29 | -------------------------------------------------------------------------------- /build/webpack/migrate.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | import {mergeObjects} from '../utilities' 4 | import config from '../config' 5 | import base from './base' 6 | 7 | export default mergeObjects(base, { 8 | entry: path.join(config.sourceDirectory, 'commands', 'migrate'), 9 | 10 | output: { 11 | path: path.join(config.destinationDirectory), 12 | filename: 'migrate.js' 13 | }, 14 | 15 | target: 'node', 16 | node: { 17 | console: false, 18 | process: false, 19 | global: false, 20 | Buffer: false, 21 | __filename: false, 22 | __dirname: false 23 | }, 24 | 25 | externals: [ 26 | config.nodeModulesExternals, 27 | { 28 | 'oriento/lib/migration': 'commonjs oriento/lib/migration', 29 | 'oriento/lib/migration/manager': 'commonjs oriento/lib/migration/manager' 30 | } 31 | ] 32 | }) 33 | -------------------------------------------------------------------------------- /build/webpack/webserver.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | import {mergeObjects} from '../utilities' 4 | import config from '../config' 5 | import base from './base' 6 | 7 | export default mergeObjects(base, { 8 | entry: path.join(config.sourceDirectory, 'commands', 'webserver'), 9 | 10 | output: { 11 | path: path.join(config.destinationDirectory), 12 | filename: 'webserver.js' 13 | }, 14 | 15 | target: 'node', 16 | node: { 17 | console: false, 18 | process: false, 19 | global: false, 20 | Buffer: false, 21 | __filename: false, 22 | __dirname: false 23 | }, 24 | 25 | externals: [config.nodeModulesExternals] 26 | }) 27 | -------------------------------------------------------------------------------- /gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp' 2 | import webpack from 'webpack' 3 | import WebpackDevServer from 'webpack-dev-server' 4 | import nodemon from 'nodemon' 5 | import path from 'path' 6 | import fs from 'fs-promise' 7 | 8 | import loadGulpPlugins from 'gulp-load-plugins' 9 | import runSequence from 'run-sequence' 10 | import del from 'del' 11 | 12 | import {webpackCallback, runCommand} from './build/utilities' 13 | import config from './build/config' 14 | 15 | const webpackFrontendConfig = () => require('./build/webpack/frontend') 16 | const webpackWebserverConfig = () => require('./build/webpack/webserver') 17 | const webpackMigrateConfig = () => require('./build/webpack/migrate') 18 | const webpackFixturesConfig = () => require('./build/webpack/fixtures') 19 | const plugins = loadGulpPlugins() 20 | 21 | /* BUILD */ 22 | 23 | gulp.task('_build-clean', callback => { 24 | del(config.destinationDirectory, callback) 25 | }) 26 | 27 | gulp.task('_build-copy-package-json', () => 28 | gulp.src('package.json') 29 | .pipe(gulp.dest(config.destinationDirectory)) 30 | ) 31 | 32 | gulp.task('_build-copy-public-files', () => 33 | gulp.src(`${config.publicDirectory}/**/*`) 34 | .pipe(gulp.dest(config.destinationPublicDirectory)) 35 | ) 36 | 37 | gulp.task('_build-compile-frontend', callback => { 38 | webpack(webpackFrontendConfig()) 39 | .run(webpackCallback({onReady: callback})) 40 | }) 41 | 42 | gulp.task('_build-compile-webserver', callback => { 43 | webpack(webpackWebserverConfig()) 44 | .run(webpackCallback({onReady: callback})) 45 | }) 46 | 47 | gulp.task('_build-compile-migrate', callback => { 48 | webpack(webpackMigrateConfig()) 49 | .run(webpackCallback({onReady: callback})) 50 | }) 51 | 52 | gulp.task('_build-compile-fixtures', callback => { 53 | webpack(webpackFixturesConfig()) 54 | .run(webpackCallback({onReady: callback})) 55 | }) 56 | 57 | gulp.task('build', callback => { 58 | runSequence( 59 | '_build-clean', 60 | [ 61 | '_build-copy-package-json', 62 | '_build-copy-public-files', 63 | '_build-compile-frontend', 64 | '_build-compile-webserver', 65 | '_build-compile-migrate', 66 | '_build-compile-fixtures' 67 | ], 68 | callback 69 | ) 70 | }) 71 | 72 | /* START */ 73 | 74 | gulp.task('_start-run-webserver', callback => { 75 | let first = true 76 | 77 | nodemon({ 78 | script: path.join(config.destinationDirectory, 'webserver'), 79 | execMap: { 80 | '': 'node' 81 | }, 82 | env: { 83 | PUBLIC_DIR: config.destinationPublicDirectory 84 | } 85 | }).on('start', function() { 86 | if (first) { 87 | first = false 88 | callback() 89 | } 90 | }) 91 | }) 92 | 93 | gulp.task('start', callback => 94 | runSequence( 95 | 'build', 96 | '_start-run-webserver', 97 | callback 98 | ) 99 | ) 100 | 101 | /* WATCH */ 102 | 103 | gulp.task('_watch-copy-public-files', ['_build-copy-public-files'], () => 104 | gulp.watch(`${config.publicDirectory}/**/*`, ['_build-copy-public-files']) 105 | ) 106 | 107 | gulp.task('_watch-compile-webserver', callback => 108 | webpack(webpackWebserverConfig()) 109 | .watch(100, webpackCallback({ 110 | onReady: callback, 111 | onChange: () => { 112 | nodemon.restart() 113 | } 114 | })) 115 | ) 116 | 117 | gulp.task('_watch-compile-migrate', callback => 118 | webpack(webpackMigrateConfig()) 119 | .watch(100, webpackCallback({ 120 | onReady: callback 121 | })) 122 | ) 123 | 124 | gulp.task('_watch-compile-fixtures', callback => 125 | webpack(webpackFixturesConfig()) 126 | .watch(100, webpackCallback({ 127 | onReady: callback 128 | })) 129 | ) 130 | 131 | gulp.task('_watch-webpack-dev-server', callback => { 132 | const config = webpackFrontendConfig() 133 | 134 | const server = new WebpackDevServer( 135 | webpack(config), 136 | config.devServer 137 | ) 138 | 139 | server.listen(3000, '0.0.0.0', callback) 140 | }) 141 | 142 | gulp.task('watch-webserver', callback => 143 | runSequence( 144 | '_build-clean', 145 | [ 146 | '_watch-copy-public-files', 147 | '_build-compile-frontend', 148 | '_watch-compile-webserver' 149 | ], 150 | '_start-run-webserver', 151 | callback 152 | ) 153 | ) 154 | 155 | gulp.task('watch-frontend', callback => { 156 | process.env.PORT = 3001 157 | process.env.HOT_RELOAD = 'react-hot-loader' 158 | config.hotReload = true 159 | 160 | runSequence( 161 | '_build-clean', 162 | [ 163 | '_watch-copy-public-files', 164 | '_build-compile-webserver' 165 | ], 166 | '_start-run-webserver', 167 | '_watch-webpack-dev-server', 168 | callback 169 | ) 170 | }) 171 | 172 | gulp.task('watch-migrate', callback => 173 | runSequence( 174 | '_build-clean', 175 | '_watch-compile-migrate', 176 | callback 177 | ) 178 | ) 179 | 180 | gulp.task('watch-fixtures', callback => 181 | runSequence( 182 | '_build-clean', 183 | '_watch-compile-fixtures', 184 | callback 185 | ) 186 | ) 187 | 188 | gulp.task('watch-karma', callback => 189 | runCommand( 190 | './node_modules/.bin/karma', 191 | ['start', '--log-level', 'debug', '--no-single-run'], 192 | callback 193 | ) 194 | ) 195 | 196 | /* ESLINT */ 197 | 198 | gulp.task('eslint', () => 199 | gulp.src([ 200 | 'build/**/*.js', 201 | 'src/**/*.js', 202 | 'test/**/*.js', 203 | 'gulpfile.babel.js', 204 | 'webpack.config.js' 205 | ]) 206 | .pipe(plugins.eslint()) 207 | .pipe(plugins.eslint.format()) 208 | .pipe(plugins.eslint.failAfterError()) 209 | ) 210 | 211 | /* KARMA */ 212 | 213 | gulp.task('_karma-clean', callback => 214 | del('.test', callback) 215 | ) 216 | 217 | gulp.task('_karma-run', callback => 218 | runCommand( 219 | './node_modules/.bin/karma', 220 | ['start', '--log-level', 'debug'], 221 | callback 222 | ) 223 | ) 224 | 225 | gulp.task('_karma-hide', async () => { 226 | await fs.mkdir('.test') 227 | await* [ 228 | fs.rename('coverage', '.test/coverage'), 229 | fs.rename('test-results.xml', '.test/test-results.xml') 230 | ] 231 | }) 232 | 233 | gulp.task('karma', callback => 234 | runSequence( 235 | '_karma-clean', 236 | '_karma-run', 237 | '_karma-hide', 238 | callback 239 | ) 240 | ) 241 | 242 | /* GLOBAL */ 243 | 244 | gulp.task('lint', ['eslint']) 245 | gulp.task('test', ['karma']) 246 | gulp.task('clean', ['_build-clean', '_karma-clean']) 247 | gulp.task('default', ['start']) 248 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-var */ 2 | 3 | require('babel-core/register') 4 | 5 | var webpack = require('webpack') 6 | var webpackKarmaConfig = require('./build/webpack/karma') 7 | 8 | module.exports = function(config) { 9 | config.set({ 10 | frameworks: ['mocha', 'sinon-chai'], 11 | 12 | files: [ 13 | 'test/**/*Test.js' 14 | ], 15 | 16 | preprocessors: { 17 | 'test/**/*Test.js': ['webpack'] 18 | }, 19 | 20 | reporters: ['progress', 'junit', 'coverage'], 21 | 22 | browsers: ['Chrome'], 23 | 24 | singleRun: true, 25 | 26 | coverageReporter: { 27 | type: 'lcov', 28 | subdir: '.' 29 | }, 30 | 31 | webpack: webpackKarmaConfig, 32 | 33 | webpackMiddleware: { 34 | quiet: true 35 | } 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esex", 3 | "version": "1.0.0", 4 | "description": "Full-stack Universal JavaScript Framework", 5 | "keywords": [ 6 | "full-stack", 7 | "universal", 8 | "isomorphic", 9 | "framework" 10 | ], 11 | "homepage": "https://github.com/esex/esex", 12 | "bugs": { 13 | "url": "https://github.com/esex/esex/issues", 14 | "email": "vslinko@yahoo.com" 15 | }, 16 | "license": "MIT", 17 | "author": { 18 | "name": "Vyacheslav Slinko", 19 | "email": "vslinko@yahoo.com", 20 | "url": "https://twitter.com/vslinko" 21 | }, 22 | "contributors": [], 23 | "main": "dist", 24 | "bin": {}, 25 | "repository": { 26 | "type": "git", 27 | "url": "esex/esex" 28 | }, 29 | "scripts": { 30 | "build": "gulp build", 31 | "check-nulls": "./bin/check_nulls", 32 | "clean": "gulp clean", 33 | "eslint": "gulp eslint", 34 | "karma": "gulp karma", 35 | "lint": "gulp lint", 36 | "start": "gulp start", 37 | "sync-tests": "./bin/sync_tests", 38 | "test": "gulp test", 39 | "watch-fixtures": "gulp watch-fixtures", 40 | "watch-frontend": "gulp watch-frontend", 41 | "watch-karma": "gulp watch-karma", 42 | "watch-migrate": "gulp watch-migrate", 43 | "watch-webserver": "gulp watch-webserver" 44 | }, 45 | "config": {}, 46 | "dependencies": { 47 | "access-rule": "1.0.0", 48 | "bcryptjs": "2.1.0", 49 | "body-parser": "1.13.1", 50 | "components-data-tree": "1.0.0", 51 | "cookie-parser": "1.3.5", 52 | "express": "4.13.0", 53 | "html5-history-api": "4.2.1", 54 | "lodash": "3.9.3", 55 | "match-route-pattern": "1.2.4", 56 | "morgan": "1.6.0", 57 | "node-fetch": "1.3.0", 58 | "nprogress": "0.2.0", 59 | "oriento": "1.2.0", 60 | "passport": "0.2.2", 61 | "passport-http": "0.2.2", 62 | "passport-http-bearer": "1.0.1", 63 | "ramda": "0.15.1", 64 | "react": "0.13.3", 65 | "react-observable": "1.1.3", 66 | "redux": "0.12.0", 67 | "rx": "2.5.3", 68 | "source-map-support": "0.3.1", 69 | "strulidator": "1.2.0", 70 | "whatwg-fetch": "0.9.0" 71 | }, 72 | "devDependencies": { 73 | "babel": "5.6.5", 74 | "babel-core": "5.6.5", 75 | "babel-eslint": "3.1.18", 76 | "babel-loader": "5.1.4", 77 | "babel-runtime": "5.6.5", 78 | "css-loader": "0.15.1", 79 | "del": "1.2.0", 80 | "eslint": "0.23.0", 81 | "eslint-plugin-babel": "1.0.0", 82 | "eslint-plugin-react": "2.5.2", 83 | "fs-promise": "0.3.1", 84 | "gulp": "3.9.0", 85 | "gulp-eslint": "0.14.0", 86 | "gulp-load-plugins": "1.0.0-rc.1", 87 | "isparta-loader": "0.2.0", 88 | "karma": "0.12.37", 89 | "karma-chrome-launcher": "0.2.0", 90 | "karma-coverage": "0.4.2", 91 | "karma-junit-reporter": "0.2.2", 92 | "karma-mocha": "0.2.0", 93 | "karma-sinon-chai": "1.0.0", 94 | "karma-webpack": "1.5.1", 95 | "mocha": "2.2.5", 96 | "node-libs-browser": "0.5.2", 97 | "nodemon": "1.3.7", 98 | "react-hot-loader": "1.2.7", 99 | "run-sequence": "1.1.1", 100 | "style-loader": "0.12.3", 101 | "webpack": "1.9.11", 102 | "webpack-dev-server": "1.9.0" 103 | }, 104 | "peerDependencies": {}, 105 | "bundledDependencies": [], 106 | "optionalDependencies": {}, 107 | "engines": {}, 108 | "os": [], 109 | "cpu": [], 110 | "preferGlobal": false, 111 | "private": true, 112 | "publishConfig": {} 113 | } 114 | -------------------------------------------------------------------------------- /public/humans.txt: -------------------------------------------------------------------------------- 1 | /* TEAM */ 2 | Founder and Core Developer: Vyacheslav Slinko 3 | Contact: vslinko@yahoo.com 4 | Twitter: @vslinko 5 | From: Moscow, Russian Federation 6 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | export {default as usersPageApiRequest} from './usersPageApiRequest' 2 | -------------------------------------------------------------------------------- /src/api/usersPageApiRequest.js: -------------------------------------------------------------------------------- 1 | import apiRequest from '../utilities/apiRequest' 2 | 3 | export default async function usersPageApiRequest(queryParams, token) { 4 | const data = await apiRequest(`/api/v1/users`, { 5 | token, 6 | include: [ 7 | 'orders', 8 | 'orders.positions', 9 | 'orders.positions.item' 10 | ] 11 | }) 12 | 13 | return data 14 | } 15 | -------------------------------------------------------------------------------- /src/commands/fixtures.js: -------------------------------------------------------------------------------- 1 | import 'source-map-support/register' 2 | 3 | import orientoServer from '../webserver/di/orientoServer' 4 | import orientoDb from '../webserver/di/orientoDb' 5 | 6 | const fixtures = require.context('../fixtures', false, /\.js$/) 7 | 8 | async function cleanDatabase() { 9 | const classNames = (await orientoDb.class.list()) 10 | .map(cls => cls.name) 11 | .filter(name => 12 | name !== 'V' && name !== 'E' 13 | && !/^O[A-Z]/.test(name) && !/^_/.test(name) 14 | && name !== 'Migration' 15 | ) 16 | 17 | await* classNames 18 | .map(name => orientoDb.exec(`TRUNCATE CLASS ${name}`)) 19 | } 20 | 21 | async function applyFixtures() { 22 | const files = fixtures.keys() 23 | let stash = {} 24 | 25 | for (const file of files) { 26 | const fixture = fixtures(file) 27 | const stashChanges = (await fixture(orientoDb, stash)) || {} 28 | 29 | stash = {...stash, ...stashChanges} 30 | } 31 | } 32 | 33 | async function main() { 34 | try { 35 | await cleanDatabase() 36 | await applyFixtures() 37 | } catch (error) { 38 | console.log(error.stack) // eslint-disable-line no-console 39 | } finally { 40 | orientoServer.close() 41 | } 42 | } 43 | 44 | main() 45 | -------------------------------------------------------------------------------- /src/commands/migrate.js: -------------------------------------------------------------------------------- 1 | import 'source-map-support/register' 2 | 3 | import Migration from 'oriento/lib/migration' 4 | import MigrationManager from 'oriento/lib/migration/manager' 5 | 6 | import orientoDb from '../webserver/di/orientoDb' 7 | 8 | const migrations = require.context('../migrations', false, /\.js$/) 9 | 10 | class EsexMigrationManager extends MigrationManager { 11 | listAvailable() { 12 | return migrations 13 | .keys() 14 | .map(file => file.replace(/^\.\//, '')) 15 | .map(file => file.replace(/\.js$/, '')) 16 | } 17 | 18 | loadMigration(name) { 19 | return new Migration(migrations(`./${name}.js`)) 20 | } 21 | } 22 | 23 | async function main() { 24 | let code = 0 25 | 26 | try { 27 | const manager = new EsexMigrationManager({ 28 | db: orientoDb, 29 | dir: '' 30 | }) 31 | 32 | const applied = await manager.up() 33 | 34 | applied.forEach(name => console.log(`Applied migration ${name}`)) // eslint-disable-line no-console 35 | 36 | } catch (error) { 37 | code = 1 38 | console.error(error.stack) // eslint-disable-line no-console 39 | } 40 | 41 | process.exit(code) // eslint-disable-line no-process-exit 42 | } 43 | 44 | main() 45 | -------------------------------------------------------------------------------- /src/commands/webserver.js: -------------------------------------------------------------------------------- 1 | import 'source-map-support/register' 2 | 3 | import webserver from '../webserver' 4 | import config from '../webserver/di/config' 5 | 6 | webserver.listen( 7 | config.webserver.port, 8 | config.webserver.host 9 | ) 10 | -------------------------------------------------------------------------------- /src/components/AboutPage/AboutPage.js: -------------------------------------------------------------------------------- 1 | import createObservableComponent from 'react-observable' 2 | import Link from '../Link' 3 | 4 | function AboutPage({dispatcher}) { 5 | return ( 6 |
7 |

About Page

8 |
9 | Home 10 |
11 |
12 | ) 13 | } 14 | 15 | export default createObservableComponent(AboutPage) 16 | -------------------------------------------------------------------------------- /src/components/AboutPage/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | export default from './AboutPage' 4 | -------------------------------------------------------------------------------- /src/components/Application/Application.js: -------------------------------------------------------------------------------- 1 | import createObservableComponent from 'react-observable' 2 | import * as pages from '../pages' 3 | 4 | function Application({dispatcher}) { 5 | return dispatcher.observable 6 | .map(state => ({ 7 | Component: pages[state.router.componentKey], 8 | props: state.router.props 9 | })) 10 | .map(({Component, props}) => ) 11 | } 12 | 13 | export default createObservableComponent(Application) 14 | -------------------------------------------------------------------------------- /src/components/Application/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | export default from './Application' 4 | -------------------------------------------------------------------------------- /src/components/ErrorPage/ErrorPage.js: -------------------------------------------------------------------------------- 1 | import createObservableComponent from 'react-observable' 2 | 3 | function ErrorPage({errorMessage, errorStack}) { 4 | return ( 5 |
6 |

Application Error

7 |

{errorMessage}

8 |
{errorStack}
9 |
10 | ) 11 | } 12 | 13 | export default createObservableComponent(ErrorPage) 14 | -------------------------------------------------------------------------------- /src/components/ErrorPage/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | export default from './ErrorPage' 4 | -------------------------------------------------------------------------------- /src/components/HomePage/HomePage.js: -------------------------------------------------------------------------------- 1 | import createObservableComponent from 'react-observable' 2 | import {fulfillTreeSchema, defineSchema, attribute} from 'components-data-tree' 3 | import getCurrentUser from '../../utilities/getCurrentUser' 4 | import bindAction from '../../utilities/bindAction' 5 | import preventEvent from '../../utilities/preventEvent' 6 | import {deauthorize} from '../../flux/token/tokenActions' 7 | import Link from '../Link' 8 | import SignIn from '../SignIn' 9 | 10 | const currentUserSchema = defineSchema({ 11 | email: attribute 12 | }) 13 | 14 | function DumbHomePage({dispatcher, currentUser, onSignOut}) { 15 | return ( 16 |
17 |

Welcome

18 | 19 |
20 | {currentUser && `You are signed in as ${currentUser.email}`} 21 | {currentUser && } 22 |
23 |
24 | About 25 |
26 |
27 | Users 28 |
29 |
30 | ) 31 | } 32 | 33 | function HomePage({dispatcher}) { 34 | return dispatcher.observable 35 | .map(state => ({ 36 | dispatcher, 37 | onSignOut: bindAction(dispatcher, deauthorize), 38 | currentUser: fulfillTreeSchema( 39 | state.resources, 40 | getCurrentUser( 41 | state.resources, 42 | state.token 43 | ), 44 | currentUserSchema 45 | ) 46 | })) 47 | .map(DumbHomePage) 48 | } 49 | 50 | export default createObservableComponent(HomePage) 51 | -------------------------------------------------------------------------------- /src/components/HomePage/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | export default from './HomePage' 4 | -------------------------------------------------------------------------------- /src/components/Link/Link.js: -------------------------------------------------------------------------------- 1 | import createObservableComponent from 'react-observable' 2 | import {navigateTo} from '../../flux/router/routerActions' 3 | 4 | function Link(props) { 5 | const {dispatcher, href} = props 6 | 7 | // TODO: handle edge cases like ctrl+click 8 | const handleClick = event => { 9 | event.preventDefault() 10 | dispatcher.dispatch(navigateTo(href)) 11 | } 12 | 13 | return ( 14 | 15 | ) 16 | } 17 | 18 | export default createObservableComponent(Link) 19 | -------------------------------------------------------------------------------- /src/components/Link/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | export default from './Link' 4 | -------------------------------------------------------------------------------- /src/components/NotFoundPage/NotFoundPage.js: -------------------------------------------------------------------------------- 1 | import createObservableComponent from 'react-observable' 2 | 3 | function NotFoundPage() { 4 | return ( 5 |

404 Not Found

6 | ) 7 | } 8 | 9 | export default createObservableComponent(NotFoundPage) 10 | -------------------------------------------------------------------------------- /src/components/NotFoundPage/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | export default from './NotFoundPage' 4 | -------------------------------------------------------------------------------- /src/components/SignIn/SignIn.js: -------------------------------------------------------------------------------- 1 | import createObservableComponent from 'react-observable' 2 | import { 3 | setEmail, 4 | setPassword, 5 | submit 6 | } from '../../flux/signIn/signInActions' 7 | import bindAction from '../../utilities/bindAction' 8 | import preventEvent from '../../utilities/preventEvent' 9 | 10 | function DumbSignIn(props) { 11 | const {dispatcher} = props 12 | const {data, validationMessagesVisible, validation, disabled, error} = props 13 | const {onEmailChange, onPasswordChange, onSubmit} = props 14 | 15 | const emailLink = { 16 | value: data.email, 17 | requestChange: onEmailChange 18 | } 19 | 20 | const passwordLink = { 21 | value: data.password, 22 | requestChange: onPasswordChange 23 | } 24 | 25 | return ( 26 |
27 |
Email
28 |
29 | 33 | {validationMessagesVisible && validation.children.email.message} 34 |
35 |
Password
36 |
37 | 42 | {validationMessagesVisible && validation.children.password.message} 43 |
44 |
45 | 46 |
47 | {error &&
{error.message}
} 48 |
49 | ) 50 | } 51 | 52 | function SignIn({dispatcher}) { 53 | return dispatcher.observable 54 | .map(state => ({ 55 | dispatcher, 56 | ...state.signIn, 57 | onEmailChange: bindAction(dispatcher, setEmail), 58 | onPasswordChange: bindAction(dispatcher, setPassword), 59 | onSubmit: bindAction(dispatcher, submit) 60 | })) 61 | .map(DumbSignIn) 62 | } 63 | 64 | export default createObservableComponent(SignIn) 65 | -------------------------------------------------------------------------------- /src/components/SignIn/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | export default from './SignIn' 4 | -------------------------------------------------------------------------------- /src/components/UsersPage/Item.js: -------------------------------------------------------------------------------- 1 | import createObservableComponent from 'react-observable' 2 | import {defineSchema, attribute} from 'components-data-tree' 3 | 4 | export const schema = defineSchema({ 5 | id: attribute, 6 | title: attribute, 7 | price: attribute 8 | }) 9 | 10 | function Item({item: {id, title, price}}) { 11 | return ( 12 |
13 |

Item #{id}

14 |
title {title}
15 |
price {price}
16 |
17 | ) 18 | } 19 | 20 | export default createObservableComponent(Item) 21 | -------------------------------------------------------------------------------- /src/components/UsersPage/Order.js: -------------------------------------------------------------------------------- 1 | import createObservableComponent from 'react-observable' 2 | import {defineSchema, attribute, relationship} from 'components-data-tree' 3 | import Position, {schema as positionSchema} from './Position' 4 | 5 | export const schema = defineSchema({ 6 | id: attribute, 7 | positions: relationship(positionSchema) 8 | }) 9 | 10 | function Order({order: {id, positions}}) { 11 | return ( 12 |
13 |

Order #{id}

14 | {positions.map((position, index) => )} 15 |
16 | ) 17 | } 18 | 19 | export default createObservableComponent(Order) 20 | -------------------------------------------------------------------------------- /src/components/UsersPage/Position.js: -------------------------------------------------------------------------------- 1 | import createObservableComponent from 'react-observable' 2 | import {defineSchema, attribute, relationship} from 'components-data-tree' 3 | import Item, {schema as itemSchema} from './Item' 4 | 5 | export const schema = defineSchema({ 6 | id: attribute, 7 | amount: attribute, 8 | item: relationship(itemSchema) 9 | }) 10 | 11 | function Position({position: {id, amount, item}}) { 12 | return ( 13 |
14 |

Position #{id}

15 |
amount {amount}
16 | 17 |
18 | ) 19 | } 20 | 21 | export default createObservableComponent(Position) 22 | -------------------------------------------------------------------------------- /src/components/UsersPage/User.js: -------------------------------------------------------------------------------- 1 | import createObservableComponent from 'react-observable' 2 | import {defineSchema, attribute, relationship} from 'components-data-tree' 3 | import Order, {schema as orderSchema} from './Order' 4 | 5 | export const schema = defineSchema({ 6 | id: attribute, 7 | email: attribute, 8 | password: attribute, 9 | orders: relationship(orderSchema) 10 | }) 11 | 12 | function User({user: {id, email, password, orders}}) { 13 | return ( 14 |
15 |

User #{id}

16 |
email {email}
17 |
password {password}
18 | {orders.map((order, index) => )} 19 |
20 | ) 21 | } 22 | 23 | export default createObservableComponent(User) 24 | -------------------------------------------------------------------------------- /src/components/UsersPage/UsersPage.js: -------------------------------------------------------------------------------- 1 | import createObservableComponent from 'react-observable' 2 | import {fulfillTreeSchema} from 'components-data-tree' 3 | import User, {schema as userSchema} from './User' 4 | 5 | function DumbUsersPage({users}) { 6 | return ( 7 |
8 | {users.map((user, index) => )} 9 |
10 | ) 11 | } 12 | 13 | function UsersPage({dispatcher, apiResponse}) { 14 | return dispatcher.observable 15 | .map(state => ({ 16 | dispatcher, 17 | users: fulfillTreeSchema( 18 | state.resources, 19 | apiResponse.data, 20 | userSchema 21 | ) 22 | })) 23 | .map(DumbUsersPage) 24 | } 25 | 26 | export default createObservableComponent(UsersPage) 27 | -------------------------------------------------------------------------------- /src/components/UsersPage/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | export default from './UsersPage' 4 | -------------------------------------------------------------------------------- /src/components/pages.js: -------------------------------------------------------------------------------- 1 | export {default as HomePage} from './HomePage' 2 | export {default as AboutPage} from './AboutPage' 3 | export {default as NotFoundPage} from './NotFoundPage' 4 | export {default as ErrorPage} from './ErrorPage' 5 | export {default as UsersPage} from './UsersPage' 6 | -------------------------------------------------------------------------------- /src/config/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | bcrypt: { 3 | rounds: process.env.BCRYPT_ROUNDS || 10 4 | }, 5 | database: { 6 | host: process.env.DATABASE_HOST || 'localhost', 7 | port: process.env.DATABASE_PORT || '2424', 8 | user: process.env.DATABASE_USER || 'root', 9 | password: process.env.DATABASE_PASSWORD, 10 | dbname: process.env.DATABASE_DBNAME, 11 | dbuser: process.env.DATABASE_DBUSER, 12 | dbpassword: process.env.DATABASE_DBPASSWORD 13 | }, 14 | webserver: { 15 | publicDir: process.env.PUBLIC_DIR, 16 | host: process.env.IP || '0.0.0.0', 17 | port: process.env.PORT || '3000' 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/config/routing.js: -------------------------------------------------------------------------------- 1 | export const notFound = {component: 'NotFoundPage', transition: 'notFound'} 2 | export const error = {component: 'ErrorPage', transition: 'error'} 3 | 4 | export default [ 5 | {route: '/', component: 'HomePage', transition: 'home'}, 6 | {route: '/about', component: 'AboutPage', transition: 'about'}, 7 | {route: '/users', component: 'UsersPage', transition: 'users'} 8 | ] 9 | -------------------------------------------------------------------------------- /src/fixtures/01-users.js: -------------------------------------------------------------------------------- 1 | import {range} from 'ramda' 2 | import bcrypt from 'bcryptjs' 3 | import config from '../webserver/di/config' 4 | 5 | export default async function(db) { 6 | const users = await* range(0, 3) 7 | .map(i => { 8 | const email = i + '@example.com' 9 | const password = bcrypt.hashSync('12345Wq', config.bcrypt.rounds) 10 | 11 | return db.insert().into('User').set({email, password}).one() 12 | }) 13 | 14 | return { 15 | users 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/fixtures/02-items.js: -------------------------------------------------------------------------------- 1 | import {range} from 'ramda' 2 | 3 | export default async function(db) { 4 | const items = await* range(1, 4) 5 | .map(i => { 6 | const title = `Item #${i}` 7 | const price = i * 1000 8 | 9 | return db.insert().into('Item').set({title, price}).one() 10 | }) 11 | 12 | return { 13 | items 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/fixtures/03-orders.js: -------------------------------------------------------------------------------- 1 | import {range, xprod} from 'ramda' 2 | 3 | export default async function(db, {users}) { 4 | const orders = await* xprod(users, range(1, 4)) 5 | .map(async ([user, i]) => { 6 | const contactEmail = `${i}+${user.email}` 7 | 8 | const order = await db.insert().into('Order').set({contactEmail}).one() 9 | 10 | await db.create('EDGE', 'UserOrder').from(user['@rid']).to(order['@rid']).one() 11 | 12 | return order 13 | }) 14 | 15 | return { 16 | orders 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/fixtures/04-orderpositions.js: -------------------------------------------------------------------------------- 1 | import {xprod} from 'ramda' 2 | 3 | export default async function(db, {orders, items}) { 4 | const orderPositions = await* xprod(orders, items) 5 | .map(([order, item]) => { 6 | const amount = Math.round(Math.random() * 99) + 1 // between 1 and 100 7 | 8 | return db.create('EDGE', 'OrderPosition').from(order['@rid']).to(item['@rid']).set({amount}).one() 9 | }) 10 | 11 | return { 12 | orderPositions 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/flux/resources/resourcesActions.js: -------------------------------------------------------------------------------- 1 | import { 2 | RESOURCES_UPDATE 3 | } from './resourcesConstants' 4 | 5 | export function mergeResources(resources) { 6 | return { 7 | type: RESOURCES_UPDATE, 8 | resources 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/flux/resources/resourcesConstants.js: -------------------------------------------------------------------------------- 1 | export const RESOURCES_UPDATE = 'RESOURCES_UPDATE' 2 | -------------------------------------------------------------------------------- /src/flux/resources/resourcesStore.js: -------------------------------------------------------------------------------- 1 | import {findIndex, merge} from 'ramda' 2 | import createStore from '../../utilities/createStore' 3 | 4 | import { 5 | RESOURCES_UPDATE 6 | } from './resourcesConstants' 7 | 8 | const initialState = [] 9 | 10 | function mergeResources(resources, action) { 11 | resources = resources.concat() 12 | 13 | function mergeResource(resource) { 14 | const {type, id} = resource 15 | 16 | const index = findIndex( 17 | r => r.type === type && r.id === id, 18 | resources 19 | ) 20 | 21 | if (index === -1) { 22 | resources.push(resource) 23 | return 24 | } 25 | 26 | const previousResource = resources[index] 27 | 28 | const attributes = merge( 29 | previousResource.attributes || {}, 30 | resource.attributes || {} 31 | ) 32 | const relationships = merge( 33 | previousResource.relationships || {}, 34 | resource.relationships || {} 35 | ) 36 | 37 | resources.splice(index, 1, { 38 | type, 39 | id, 40 | attributes, 41 | relationships 42 | }) 43 | } 44 | 45 | action.resources.forEach(mergeResource) 46 | 47 | return resources 48 | } 49 | 50 | export default createStore(initialState, { 51 | [RESOURCES_UPDATE]: mergeResources 52 | }) 53 | -------------------------------------------------------------------------------- /src/flux/router/routerActions.js: -------------------------------------------------------------------------------- 1 | import routing, {notFound as notFoundRoute, error as errorRoute} from '../../config/routing' 2 | import * as transitions from '../transition/transitionActions' 3 | import progressIndicator from '../../utilities/progressIndicator' 4 | import matchRoutePattern from 'match-route-pattern' 5 | 6 | import { 7 | ROUTER_PAGE_CHANGE 8 | } from './routerConstants' 9 | 10 | function changePage(componentKey, props) { 11 | return { 12 | type: ROUTER_PAGE_CHANGE, 13 | componentKey, 14 | props 15 | } 16 | } 17 | 18 | function runNotFoundTransition(url) { 19 | return async dispatch => { 20 | const transition = transitions[notFoundRoute.transition] 21 | const componentKey = notFoundRoute.component 22 | 23 | const {title, status, ...props} = await dispatch(transition()) 24 | 25 | dispatch(changePage(componentKey, props)) 26 | 27 | return {url, title, status} 28 | } 29 | } 30 | 31 | function runErrorTransition(url, originalProps) { 32 | return async dispatch => { 33 | const transition = transitions[errorRoute.transition] 34 | const componentKey = errorRoute.component 35 | 36 | const {title, status, ...props} = await dispatch(transition()) 37 | 38 | dispatch(changePage(componentKey, {...originalProps, ...props})) 39 | 40 | return {url, title, status} 41 | } 42 | } 43 | 44 | export function runTransition(url) { 45 | return async dispatch => { 46 | const route = routing 47 | .map(route => { 48 | return {...route, match: matchRoutePattern(route.route, url)} 49 | }) 50 | .filter(({match}) => !!match) 51 | .shift() 52 | 53 | if (!route) { 54 | return dispatch(runNotFoundTransition(url)) 55 | } 56 | 57 | const transition = transitions[route.transition] 58 | const componentKey = route.component 59 | 60 | const {title, status, ...props} = await dispatch(transition(route.match || {})) 61 | 62 | if (status === 401) { 63 | if (global.history) { // TODO: fix this shit 64 | dispatch(navigateTo('/')) 65 | } 66 | } else if (status === 404) { 67 | return dispatch(runNotFoundTransition(url)) 68 | } else if (status < 200 || status >= 300) { 69 | return dispatch(runErrorTransition(url, props)) 70 | } else { 71 | dispatch(changePage(componentKey, props)) 72 | } 73 | 74 | return {url, title, status} 75 | } 76 | } 77 | 78 | export function navigateTo(url) { 79 | return async dispatch => { 80 | await progressIndicator(async () => { 81 | const {title} = await dispatch(runTransition(url)) 82 | 83 | document.title = title 84 | history.pushState({url, title}, title, url) 85 | }) 86 | } 87 | } 88 | 89 | export function popState(event) { 90 | return async dispatch => { 91 | if (!event.state || !event.state.url) { 92 | window.location.reload() 93 | return 94 | } 95 | 96 | const {url, title} = event.state 97 | 98 | document.title = title 99 | 100 | await progressIndicator(async () => { 101 | await dispatch(runTransition(url)) 102 | }) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/flux/router/routerConstants.js: -------------------------------------------------------------------------------- 1 | export const ROUTER_PAGE_CHANGE = 'ROUTER_PAGE_CHANGE' 2 | -------------------------------------------------------------------------------- /src/flux/router/routerStore.js: -------------------------------------------------------------------------------- 1 | import createStore from '../../utilities/createStore' 2 | 3 | import { 4 | ROUTER_PAGE_CHANGE 5 | } from './routerConstants' 6 | 7 | const initialState = { 8 | componentKey: undefined, 9 | props: undefined 10 | } 11 | 12 | function page(state, {componentKey, props}) { 13 | return {componentKey, props} 14 | } 15 | 16 | export default createStore(initialState, { 17 | [ROUTER_PAGE_CHANGE]: page 18 | }) 19 | -------------------------------------------------------------------------------- /src/flux/signIn/signInActions.js: -------------------------------------------------------------------------------- 1 | import {authorize} from '../token/tokenActions' 2 | 3 | import { 4 | SIGN_IN_EMAIL_CHANGE, 5 | SIGN_IN_PASSWORD_CHANGE, 6 | SIGN_IN_VALIDATION_MESSAGES_VISIBLE, 7 | SIGN_IN_ERROR, 8 | SIGN_IN_DISABLED, 9 | SIGN_IN_RESET 10 | } from './signInConstants' 11 | 12 | export function setEmail(email) { 13 | return { 14 | type: SIGN_IN_EMAIL_CHANGE, 15 | email 16 | } 17 | } 18 | 19 | export function setPassword(password) { 20 | return { 21 | type: SIGN_IN_PASSWORD_CHANGE, 22 | password 23 | } 24 | } 25 | 26 | export function setValidationMessagesVisible(visible) { 27 | return { 28 | type: SIGN_IN_VALIDATION_MESSAGES_VISIBLE, 29 | visible 30 | } 31 | } 32 | 33 | export function setError(error) { 34 | return { 35 | type: SIGN_IN_ERROR, 36 | error 37 | } 38 | } 39 | 40 | export function setDisabled(disabled) { 41 | return { 42 | type: SIGN_IN_DISABLED, 43 | disabled 44 | } 45 | } 46 | 47 | export function reset() { 48 | return { 49 | type: SIGN_IN_RESET 50 | } 51 | } 52 | 53 | export function submit() { 54 | return async (dispatch, getState) => { 55 | const {signIn} = getState() 56 | const {disabled, validation: {valid}, data} = signIn 57 | const {email, password} = data 58 | 59 | if (disabled) { 60 | return 61 | } 62 | 63 | if (!valid) { 64 | dispatch(setValidationMessagesVisible(true)) 65 | return 66 | } 67 | 68 | dispatch(setDisabled(true)) 69 | dispatch(setError(undefined)) 70 | dispatch(setValidationMessagesVisible(false)) 71 | 72 | try { 73 | await dispatch(authorize( 74 | email, 75 | password 76 | )) 77 | dispatch(reset()) 78 | } catch (error) { 79 | dispatch(setError(error)) 80 | } finally { 81 | dispatch(setDisabled(false)) 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/flux/signIn/signInConstants.js: -------------------------------------------------------------------------------- 1 | export const SIGN_IN_EMAIL_CHANGE = 'SIGN_IN_EMAIL_CHANGE' 2 | export const SIGN_IN_PASSWORD_CHANGE = 'SIGN_IN_PASSWORD_CHANGE' 3 | export const SIGN_IN_VALIDATION_MESSAGES_VISIBLE = 'SIGN_IN_VALIDATION_MESSAGES_VISIBLE' 4 | export const SIGN_IN_ERROR = 'SIGN_IN_ERROR' 5 | export const SIGN_IN_DISABLED = 'SIGN_IN_DISABLED' 6 | export const SIGN_IN_RESET = 'SIGN_IN_RESET' 7 | -------------------------------------------------------------------------------- /src/flux/signIn/signInStore.js: -------------------------------------------------------------------------------- 1 | import {merge} from 'ramda' 2 | import createStore from '../../utilities/createStore' 3 | import signInValidator from './signInValidator' 4 | 5 | import { 6 | SIGN_IN_EMAIL_CHANGE, 7 | SIGN_IN_PASSWORD_CHANGE, 8 | SIGN_IN_VALIDATION_MESSAGES_VISIBLE, 9 | SIGN_IN_ERROR, 10 | SIGN_IN_DISABLED, 11 | SIGN_IN_RESET 12 | } from './signInConstants' 13 | 14 | const initialData = { 15 | email: '', 16 | password: '' 17 | } 18 | 19 | const initialState = { 20 | data: initialData, 21 | validationMessagesVisible: false, 22 | validation: signInValidator(initialData), 23 | error: undefined, 24 | disabled: false 25 | } 26 | 27 | function mergeData(state, changes) { 28 | const data = merge(state.data, changes) 29 | const validation = signInValidator(data) 30 | 31 | return merge(state, {data, validation}) 32 | } 33 | 34 | function email(state, {email}) { 35 | return mergeData(state, {email}) 36 | } 37 | 38 | function password(state, {password}) { 39 | return mergeData(state, {password}) 40 | } 41 | 42 | function validationMessagesVisible(state, {visible}) { 43 | return merge(state, {validationMessagesVisible: visible}) 44 | } 45 | 46 | function error(state, {error}) { 47 | return merge(state, {error}) 48 | } 49 | 50 | function disabled(state, {disabled}) { 51 | return merge(state, {disabled}) 52 | } 53 | 54 | export default createStore(initialState, { 55 | [SIGN_IN_EMAIL_CHANGE]: email, 56 | [SIGN_IN_PASSWORD_CHANGE]: password, 57 | [SIGN_IN_VALIDATION_MESSAGES_VISIBLE]: validationMessagesVisible, 58 | [SIGN_IN_ERROR]: error, 59 | [SIGN_IN_DISABLED]: disabled, 60 | [SIGN_IN_RESET]: () => initialState 61 | }) 62 | -------------------------------------------------------------------------------- /src/flux/signIn/signInValidator.js: -------------------------------------------------------------------------------- 1 | import {constraints} from 'strulidator' 2 | import User from '../../schemas/User' 3 | 4 | const {createObjectConstraint} = constraints 5 | 6 | export default createObjectConstraint({ 7 | email: User.attributes.email, 8 | password: User.attributes.password 9 | }) 10 | -------------------------------------------------------------------------------- /src/flux/stores.js: -------------------------------------------------------------------------------- 1 | export {default as router} from './router/routerStore' 2 | export {default as token} from './token/tokenStore' 3 | export {default as resources} from './resources/resourcesStore' 4 | export {default as signIn} from './signIn/signInStore' 5 | -------------------------------------------------------------------------------- /src/flux/token/tokenActions.js: -------------------------------------------------------------------------------- 1 | import apiRequest from '../../utilities/apiRequest' 2 | import collectResources from '../../utilities/collectResources' 3 | import {mergeResources} from '../resources/resourcesActions' 4 | import { 5 | TOKEN 6 | } from './tokenConstants' 7 | 8 | export function setToken(token) { 9 | return { 10 | type: TOKEN, 11 | token 12 | } 13 | } 14 | 15 | export function authorize(username, password) { 16 | return async dispatch => { 17 | const response = await apiRequest('/xhr/login?include=user', { 18 | method: 'POST', 19 | headers: { 20 | 'Authorization': `Basic ${btoa(`${username}:${password}`)}` // TODO: use polyfill? 21 | } 22 | }) 23 | 24 | dispatch(setToken(response.data.attributes.hash)) 25 | dispatch(mergeResources(collectResources(response))) 26 | } 27 | } 28 | 29 | export function deauthorize() { 30 | return async (dispatch, getState) => { 31 | const {token} = getState() 32 | 33 | if (!token) { 34 | return 35 | } 36 | 37 | await apiRequest('/xhr/logout', { 38 | method: 'POST', 39 | headers: { 40 | 'Authorization': `Bearer ${token}` 41 | } 42 | }) 43 | 44 | dispatch(setToken(undefined)) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/flux/token/tokenConstants.js: -------------------------------------------------------------------------------- 1 | export const TOKEN = 'TOKEN' 2 | -------------------------------------------------------------------------------- /src/flux/token/tokenStore.js: -------------------------------------------------------------------------------- 1 | import createStore from '../../utilities/createStore' 2 | 3 | import { 4 | TOKEN 5 | } from './tokenConstants' 6 | 7 | function newToken(previousToken, {token}) { 8 | return token 9 | } 10 | 11 | export default createStore(undefined, { 12 | [TOKEN]: newToken 13 | }) 14 | -------------------------------------------------------------------------------- /src/flux/transition/createFetchTransition.js: -------------------------------------------------------------------------------- 1 | import {mergeResources} from '../resources/resourcesActions' 2 | import collectResources from '../../utilities/collectResources' 3 | 4 | export default function createFetchTransition({status = 200, title, apiRequest}) { 5 | return function transition(queryParams) { 6 | return async (dispatch, getState) => { 7 | const {token} = getState() 8 | 9 | const result = { 10 | status, 11 | queryParams 12 | } 13 | 14 | try { 15 | result.apiResponse = (await apiRequest(queryParams, token)) || {} 16 | 17 | dispatch(mergeResources(collectResources(result.apiResponse))) 18 | 19 | } catch (error) { 20 | result.status = error.responseStatus || 500 21 | result.errorMessage = error.message 22 | result.errorStack = error.stack 23 | } 24 | 25 | // TODO: 404 500 error title 26 | result.title = typeof title === 'function' 27 | ? title(result) 28 | : title 29 | 30 | return result 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/flux/transition/createStaticTransition.js: -------------------------------------------------------------------------------- 1 | export default function createStaticTransition({status = 200, title, ...other}) { 2 | return function transition(queryParams = {}) { 3 | return () => { 4 | return { 5 | status, 6 | title, 7 | queryParams, 8 | ...other 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/flux/transition/transitionActions.js: -------------------------------------------------------------------------------- 1 | import * as api from '../../api' 2 | import createFetchTransition from './createFetchTransition' 3 | import createStaticTransition from './createStaticTransition' 4 | 5 | export const home = createStaticTransition({ 6 | title: 'Home' 7 | }) 8 | 9 | export const about = createStaticTransition({ 10 | title: 'About' 11 | }) 12 | 13 | export const users = createFetchTransition({ 14 | title: 'Users', 15 | apiRequest: api.usersPageApiRequest 16 | }) 17 | 18 | export const notFound = createStaticTransition({ 19 | status: 404, 20 | title: 'Not Found' 21 | }) 22 | 23 | export const error = createStaticTransition({ 24 | status: 500, 25 | title: 'Application Error' 26 | }) 27 | -------------------------------------------------------------------------------- /src/frontend/index.js: -------------------------------------------------------------------------------- 1 | import './polyfills' 2 | 3 | import React from 'react' 4 | import Application from '../components/Application' 5 | import createDispatcher from '../utilities/createDispatcher' 6 | import {popState} from '../flux/router/routerActions' 7 | 8 | import 'nprogress/nprogress.css' 9 | 10 | function bootstrapDispatcher(initialState) { 11 | const dispatcher = createDispatcher(initialState) 12 | 13 | const url = document.location.pathname + document.location.search 14 | const title = document.title 15 | 16 | history.replaceState({url, title}, title, url) 17 | 18 | window.addEventListener('popstate', event => { 19 | dispatcher.dispatch(popState(event)) 20 | }) 21 | 22 | return dispatcher 23 | } 24 | 25 | export function bootstrapApplication(container, initialState) { 26 | const dispatcher = bootstrapDispatcher(initialState) 27 | 28 | React.render( 29 | , 30 | container 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /src/frontend/polyfills.js: -------------------------------------------------------------------------------- 1 | import '../utilities/patchReact' 2 | import 'html5-history-api' 3 | import 'whatwg-fetch' 4 | -------------------------------------------------------------------------------- /src/migrations/m20150624_180904_user.js: -------------------------------------------------------------------------------- 1 | export async function up(db) { 2 | const cls = await db.class.create('User', 'V') 3 | 4 | await cls.property.create({ 5 | name: 'email', 6 | type: 'string', 7 | mandatory: true, 8 | notNull: true 9 | }) 10 | 11 | await cls.property.create({ 12 | name: 'password', 13 | type: 'string', 14 | mandatory: true, 15 | notNull: true 16 | }) 17 | 18 | await db.index.create({ 19 | name: 'User.email', 20 | type: 'unique' 21 | }) 22 | } 23 | 24 | export async function down(db) { 25 | await db.exec('DROP INDEX User.email') 26 | await db.exec('TRUNCATE CLASS User') 27 | await db.exec('DROP CLASS User') 28 | } 29 | -------------------------------------------------------------------------------- /src/migrations/m20150625_113856_item.js: -------------------------------------------------------------------------------- 1 | export async function up(db) { 2 | const cls = await db.class.create('Item', 'V') 3 | 4 | await cls.property.create({ 5 | name: 'title', 6 | type: 'string', 7 | mandatory: true, 8 | notNull: true 9 | }) 10 | 11 | await cls.property.create({ 12 | name: 'price', 13 | type: 'decimal', 14 | mandatory: true, 15 | notNull: true, 16 | min: 0 17 | }) 18 | } 19 | 20 | export async function down(db) { 21 | await db.exec('TRUNCATE CLASS Item') 22 | await db.exec('DROP CLASS Item') 23 | } 24 | -------------------------------------------------------------------------------- /src/migrations/m20150625_113950_order.js: -------------------------------------------------------------------------------- 1 | export async function up(db) { 2 | const cls = await db.class.create('Order', 'V') 3 | 4 | await cls.property.create({ 5 | name: 'contactEmail', 6 | type: 'string', 7 | mandatory: true, 8 | notNull: true 9 | }) 10 | } 11 | 12 | export async function down(db) { 13 | await db.exec('TRUNCATE CLASS Order') 14 | await db.exec('DROP CLASS Order') 15 | } 16 | -------------------------------------------------------------------------------- /src/migrations/m20150625_114024_userorder.js: -------------------------------------------------------------------------------- 1 | export async function up(db) { 2 | const cls = await db.class.create('UserOrder', 'E') 3 | 4 | await cls.property.create({ 5 | name: 'out', 6 | type: 'link', 7 | mandatory: true, 8 | notNull: true, 9 | linkedClass: 'User' 10 | }) 11 | 12 | await cls.property.create({ 13 | name: 'in', 14 | type: 'link', 15 | mandatory: true, 16 | notNull: true, 17 | linkedClass: 'Order' 18 | }) 19 | } 20 | 21 | export async function down(db) { 22 | await db.exec('TRUNCATE CLASS UserOrder') 23 | await db.exec('DROP CLASS UserOrder') 24 | } 25 | -------------------------------------------------------------------------------- /src/migrations/m20150625_114102_orderposition.js: -------------------------------------------------------------------------------- 1 | export async function up(db) { 2 | const cls = await db.class.create('OrderPosition', 'E') 3 | 4 | await cls.property.create({ 5 | name: 'out', 6 | type: 'link', 7 | mandatory: true, 8 | notNull: true, 9 | linkedClass: 'Order' 10 | }) 11 | 12 | await cls.property.create({ 13 | name: 'in', 14 | type: 'link', 15 | mandatory: true, 16 | notNull: true, 17 | linkedClass: 'Item' 18 | }) 19 | 20 | await cls.property.create({ 21 | name: 'amount', 22 | type: 'integer', 23 | mandatory: true, 24 | notNull: true, 25 | min: 1 26 | }) 27 | 28 | await db.index.create({ 29 | name: 'OrderPosition.out_in', 30 | class: 'OrderPosition', 31 | properties: ['out', 'in'], 32 | type: 'unique' 33 | }) 34 | } 35 | 36 | export async function down(db) { 37 | await db.exec('DROP INDEX OrderPosition.out_in') 38 | await db.exec('TRUNCATE CLASS OrderPosition') 39 | await db.exec('DROP CLASS OrderPosition') 40 | } 41 | -------------------------------------------------------------------------------- /src/migrations/m20150625_161547_accesstoken.js: -------------------------------------------------------------------------------- 1 | export async function up(db) { 2 | const cls = await db.class.create('AccessToken', 'V') 3 | 4 | await cls.property.create({ 5 | name: 'hash', 6 | type: 'string', 7 | mandatory: true, 8 | notNull: true 9 | }) 10 | 11 | await db.index.create({ 12 | name: 'AccessToken.hash', 13 | type: 'unique' 14 | }) 15 | } 16 | 17 | export async function down(db) { 18 | await db.exec('DROP INDEX AccessToken.hash') 19 | await db.exec('TRUNCATE CLASS AccessToken') 20 | await db.exec('DROP CLASS AccessToken') 21 | } 22 | -------------------------------------------------------------------------------- /src/migrations/m20150625_161713_usertoken.js: -------------------------------------------------------------------------------- 1 | export async function up(db) { 2 | const cls = await db.class.create('UserToken', 'E') 3 | 4 | await cls.property.create({ 5 | name: 'out', 6 | type: 'link', 7 | mandatory: true, 8 | notNull: true, 9 | linkedClass: 'User' 10 | }) 11 | 12 | await cls.property.create({ 13 | name: 'in', 14 | type: 'link', 15 | mandatory: true, 16 | notNull: true, 17 | linkedClass: 'AccessToken' 18 | }) 19 | } 20 | 21 | export async function down(db) { 22 | await db.exec('TRUNCATE CLASS UserToken') 23 | await db.exec('DROP CLASS UserToken') 24 | } 25 | -------------------------------------------------------------------------------- /src/schemas/AccessToken.js: -------------------------------------------------------------------------------- 1 | import {constraints} from 'strulidator' 2 | 3 | const { 4 | combineConstraints, 5 | notNull, 6 | notEmpty, 7 | string 8 | } = constraints 9 | 10 | export default { 11 | type: 'AccessToken', 12 | attributes: { 13 | hash: combineConstraints({ 14 | notNull, 15 | notEmpty, 16 | string 17 | }) 18 | }, 19 | relationships: { 20 | user: { 21 | class: 'User', 22 | reverseThroughEdge: 'UserToken' 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/schemas/Item.js: -------------------------------------------------------------------------------- 1 | import {constraints} from 'strulidator' 2 | 3 | const { 4 | combineConstraints, 5 | notNull, 6 | notEmpty, 7 | string, 8 | number, 9 | createMinConstraint, 10 | createMinLengthConstraint 11 | } = constraints 12 | 13 | export default { 14 | type: 'Item', 15 | attributes: { 16 | title: combineConstraints({ 17 | notNull, 18 | notEmpty, 19 | string, 20 | minLength: createMinLengthConstraint(3) 21 | }), 22 | price: combineConstraints({ 23 | notNull, 24 | number, 25 | min: createMinConstraint(0) 26 | }) 27 | }, 28 | relationships: { 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/schemas/Order.js: -------------------------------------------------------------------------------- 1 | export default { 2 | type: 'Order', 3 | attributes: { 4 | }, 5 | relationships: { 6 | positions: { 7 | class: 'OrderPosition' 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/schemas/OrderPosition.js: -------------------------------------------------------------------------------- 1 | import {constraints} from 'strulidator' 2 | 3 | const { 4 | combineConstraints, 5 | notNull, 6 | number, 7 | createMinConstraint 8 | } = constraints 9 | 10 | export default { 11 | type: 'OrderPosition', 12 | attributes: { 13 | amount: combineConstraints({ 14 | notNull, 15 | number, 16 | min: createMinConstraint(0) 17 | }) 18 | }, 19 | relationships: { 20 | item: { 21 | class: 'Item', 22 | throughEdge: 'OrderPosition' 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/schemas/User.js: -------------------------------------------------------------------------------- 1 | import {constraints} from 'strulidator' 2 | import {merge} from 'ramda' 3 | import bcrypt from 'bcryptjs' // TODO: use only in webserver environment 4 | import {authorized} from 'access-rule' 5 | import config from '../config' 6 | 7 | const { 8 | combineConstraints, 9 | notNull, 10 | notEmpty, 11 | string, 12 | email, 13 | createMinLengthConstraint, 14 | createMaxLengthConstraint 15 | } = constraints 16 | 17 | export default { 18 | type: 'User', 19 | attributes: { 20 | email: combineConstraints({ 21 | notNull, 22 | notEmpty, 23 | string, 24 | email 25 | }), 26 | password: combineConstraints({ 27 | notNull, 28 | notEmpty, 29 | string, 30 | minLength: createMinLengthConstraint(5), 31 | maxLength: createMaxLengthConstraint(18) 32 | }) 33 | }, 34 | relationships: { 35 | orders: { 36 | class: 'Order', 37 | throughEdge: 'UserOrder' 38 | } 39 | }, 40 | schemaAccessRule: authorized, 41 | resourceAccessRule: authorized, 42 | normalize: user => { 43 | if (!user.attributes.password) { 44 | return user 45 | } 46 | 47 | const password = bcrypt.hashSync(user.attributes.password, config.bcrypt.rounds) 48 | 49 | return merge(user, { 50 | attributes: merge(user.attributes, {password}) 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/schemas/index.js: -------------------------------------------------------------------------------- 1 | export {default as User} from './User' 2 | export {default as Item} from './Item' 3 | export {default as Order} from './Order' 4 | export {default as OrderPosition} from './OrderPosition' 5 | export {default as AccessToken} from './AccessToken' 6 | -------------------------------------------------------------------------------- /src/utilities/apiRequest.js: -------------------------------------------------------------------------------- 1 | export default async function apiRequest(url, spec) { 2 | const {method = 'GET', body} = spec 3 | const {fields = {}, include = []} = spec 4 | const {token} = spec 5 | const {headers = {}} = spec 6 | 7 | let queryParams = Object.keys(fields) 8 | .reduce((acc, key) => { 9 | return [...acc, `fields[${key}]=${fields[key].join(',')}`] 10 | }, []) 11 | 12 | if (include.length > 0) { 13 | queryParams = queryParams.concat([`include=${include.join(',')}`]) 14 | } 15 | 16 | let apiUrl = `${process.env.BACKEND_ADDRESS}${url}` 17 | 18 | if (queryParams) { 19 | if (apiUrl.indexOf('?') === -1) { 20 | apiUrl = `${apiUrl}?` 21 | } else { 22 | apiUrl = `${apiUrl}&` 23 | } 24 | apiUrl = `${apiUrl}${queryParams.join('&')}` 25 | } 26 | 27 | headers['Accept'] = 'application/vnd.api+json' 28 | headers['Content-Type'] = 'application/vnd.api+json' 29 | 30 | if (token) { 31 | headers['Authorization'] = `Bearer ${token}` 32 | } 33 | 34 | const response = await fetch(apiUrl, { 35 | credentials: 'include', 36 | method, 37 | body: body && JSON.stringify(body), 38 | headers 39 | }) 40 | 41 | if (response.status < 200 || response.status >= 300) { 42 | let errorMessage 43 | let errorStack 44 | let json 45 | 46 | try { 47 | json = await response.json() 48 | } catch (error) { 49 | errorMessage = response.statusText 50 | } 51 | 52 | if (json && json.errors && json.errors.length > 0) { 53 | errorMessage = json.errors[0].title 54 | errorStack = json.errors[0].detail 55 | } else { 56 | errorMessage = response.statusText 57 | } 58 | 59 | { 60 | const error = new Error('API Error: ' + errorMessage) 61 | 62 | error.responseStatus = response.status 63 | 64 | if (errorStack) { 65 | error.stack = errorStack 66 | } 67 | 68 | throw error 69 | } 70 | } 71 | 72 | const json = await response.json() 73 | 74 | return json 75 | } 76 | -------------------------------------------------------------------------------- /src/utilities/bindAction.js: -------------------------------------------------------------------------------- 1 | export default function bindAction(dispatcher, action) { 2 | return (...args) => { 3 | return dispatcher.dispatch(action(...args)) 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/utilities/collectResources.js: -------------------------------------------------------------------------------- 1 | export default function collectResources(apiResponse) { 2 | let resources = [] 3 | 4 | if (Array.isArray(apiResponse.data)) { 5 | resources = resources.concat(apiResponse.data) 6 | } else if (apiResponse.data) { 7 | resources.push(apiResponse.data) 8 | } 9 | 10 | if (apiResponse.included) { 11 | resources = resources.concat(apiResponse.included) 12 | } 13 | 14 | return resources 15 | } 16 | -------------------------------------------------------------------------------- /src/utilities/createDispatcher.js: -------------------------------------------------------------------------------- 1 | import {createRedux} from 'redux' 2 | import * as stores from '../flux/stores' 3 | import Rx from 'rx' 4 | 5 | export default function createDispatcher(initialState) { 6 | const dispatcher = createRedux(stores, initialState) 7 | const {subscribe, dispatch, getState} = dispatcher 8 | 9 | const observable = Rx.Observable.create(observer => { 10 | const listener = () => observer.onNext(getState()) 11 | const unsubscribe = subscribe(listener) 12 | 13 | listener() 14 | 15 | return unsubscribe 16 | }) 17 | 18 | return { 19 | observable, 20 | dispatch, 21 | getState 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/utilities/createStore.js: -------------------------------------------------------------------------------- 1 | export default function createStore(initialState, handlers) { 2 | return (state = initialState, action) => { 3 | const handler = handlers[action.type] 4 | 5 | return handler ? handler(state, action) : state 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/utilities/getCurrentUser.js: -------------------------------------------------------------------------------- 1 | export default function getCurrentUser(resources, token) { 2 | if (!token) { 3 | return 4 | } 5 | 6 | const tokenResource = resources 7 | .filter(resource => resource.type === 'AccessToken') 8 | .filter(resource => resource.attributes.hash === token) 9 | .shift() 10 | 11 | if (!tokenResource) { 12 | return 13 | } 14 | 15 | const userId = tokenResource.relationships.user.data[0].id // TODO: fix 16 | 17 | const userResource = resources 18 | .filter(resource => resource.type === 'User' && resource.id === userId) 19 | .shift() 20 | 21 | return userResource 22 | } 23 | -------------------------------------------------------------------------------- /src/utilities/patchReact.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | // TODO: remove after https://github.com/facebook/react/pull/3995 4 | 5 | const originalCreateElement = React.createElement 6 | 7 | React.createElement = function createElement(element, ...rest) { 8 | let type = element 9 | 10 | if (typeof element === 'function' && !element.prototype.render) { 11 | if (!element.ReactComponent) { 12 | element.ReactComponent = class Component { 13 | static displayName = element.name 14 | 15 | render() { 16 | return element(this.props) 17 | } 18 | } 19 | } 20 | 21 | type = element.ReactComponent 22 | } 23 | 24 | return originalCreateElement(type, ...rest) 25 | } 26 | -------------------------------------------------------------------------------- /src/utilities/preventEvent.js: -------------------------------------------------------------------------------- 1 | export default function preventEvent(callback) { 2 | return event => { 3 | event.preventDefault() 4 | callback() 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/utilities/progressIndicator.js: -------------------------------------------------------------------------------- 1 | import NProgress from 'nprogress' 2 | 3 | export default async function progressIndicator(executor) { 4 | NProgress.start() 5 | NProgress.set(0.3) 6 | 7 | const interval = setInterval(() => NProgress.inc(), 500) 8 | 9 | try { 10 | return await executor() 11 | } finally { 12 | clearInterval(interval) 13 | NProgress.done() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/webserver/db/applyTransaction.js: -------------------------------------------------------------------------------- 1 | export default function applyTransaction(db, {steps, returnKey, returnType}) { 2 | let query = db 3 | 4 | if (returnType !== 'all' && returnType !== 'one') { 5 | throw new Error(`Unexpected return type "${returnType}", expected "all" or "one"`) 6 | } 7 | 8 | for (const step of steps) { 9 | switch (step.type) { 10 | case 'createVertex': 11 | query = query 12 | .let(step.key, s => { 13 | s.insert().into(step.class).set(step.attributes) 14 | }) 15 | break 16 | 17 | case 'updateVertex': 18 | query = query 19 | .let(step.key, s => { 20 | s.update(step.class).set(step.attributes).where({'@rid': step.id}) 21 | }) 22 | break 23 | 24 | case 'removeEdges': 25 | query = query 26 | .let(step.key, s => { 27 | s.delete('EDGE', step.class).where({'out': step.from}) 28 | }) 29 | break 30 | 31 | case 'findOne': 32 | query = query 33 | .let(step.key, s => { 34 | s.select().from(step.class).where({'@rid': step.id}) 35 | }) 36 | break 37 | 38 | case 'createEdge': 39 | query = query 40 | .let(step.key, s => { 41 | s.create('edge', step.class).from(step.from).to(step.to) 42 | }) 43 | break 44 | 45 | default: 46 | throw new Error(`Unknown transaction step "${step.type}"`) 47 | } 48 | } 49 | 50 | query = query 51 | .return(returnKey) 52 | .commit() 53 | 54 | return returnType === 'one' 55 | ? query.one() 56 | : query.all() 57 | } 58 | -------------------------------------------------------------------------------- /src/webserver/db/errors.js: -------------------------------------------------------------------------------- 1 | export class ParametersError { 2 | constructor(message) { 3 | this.message = message 4 | this.stack = (new Error()).stack 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/webserver/db/fulfillQuery.js: -------------------------------------------------------------------------------- 1 | import {uniqWith, flatten} from 'ramda' 2 | import {ParametersError} from './errors' 3 | import mapObjectToResource from './mapObjectToResource' 4 | import * as schemas from '../../schemas' 5 | 6 | function objectToResource(db, object, fields) { 7 | return mapObjectToResource(db, object, fields[object['@class']]) 8 | } 9 | 10 | function reduceOuts({schema, queryPath}, field) { 11 | if (!schema.relationships || !schema.relationships[field]) { 12 | throw new ParametersError(`Class "${schema.type}" haven't relationship "${field}"`) 13 | } 14 | 15 | const relationship = schema.relationships[field] 16 | 17 | if (relationship.throughEdge === schema.type) { 18 | queryPath.pop() 19 | queryPath.push(`OUT('${relationship.throughEdge}')`) 20 | 21 | } else if (relationship.throughEdge) { 22 | queryPath.push(`OUT('${relationship.throughEdge}')`) 23 | 24 | } else if (relationship.reverseThroughEdge) { 25 | queryPath.push(`IN('${relationship.reverseThroughEdge}')`) 26 | 27 | } else if (relationship.class) { 28 | queryPath.push(`OUTE('${relationship.class}')`) 29 | 30 | } else { 31 | throw new Error(`Invalid relationship definition "${field}" in class "${schema.type}"`) 32 | } 33 | 34 | return { 35 | schema: schemas[relationship.class], 36 | queryPath 37 | } 38 | } 39 | 40 | async function fetchInclude({db, object, path, fields}) { 41 | const type = object['@class'] 42 | const id = object['@rid'] 43 | const classSchema = schemas[type] 44 | 45 | const queryPath = path 46 | .reduce(reduceOuts, {schema: classSchema, queryPath: []}) 47 | .queryPath 48 | .join('.') 49 | 50 | const query = `SELECT EXPAND(${queryPath}) FROM ${type} WHERE @rid = ${id}` 51 | const objects = await db.query(query) 52 | const resources = await* objects.map(object => objectToResource(db, object, fields)) 53 | 54 | return resources 55 | } 56 | 57 | 58 | export default async function fulfillQuery(db, root, {fields, include}) { 59 | if (!root) { 60 | return {data: null} // null-reasonable 61 | } 62 | 63 | const data = Array.isArray(root) 64 | ? (await * root.map(object => objectToResource(db, object, fields))) 65 | : (await objectToResource(db, root, fields)) 66 | 67 | const response = { 68 | data 69 | } 70 | 71 | const objects = Array.isArray(root) ? root : [root] 72 | let included = [] 73 | 74 | for (const oneInclude of include) { 75 | const path = oneInclude.split('.') 76 | 77 | const includeResources = await* objects.map(object => 78 | fetchInclude({db, object, path, fields}) 79 | ) 80 | 81 | included = included.concat(flatten(includeResources)) 82 | } 83 | 84 | included = uniqWith( 85 | (a, b) => a.type === b.type && a.id === b.id, 86 | included 87 | ) 88 | 89 | if (included.length > 0) { 90 | response.included = included 91 | } 92 | 93 | return response 94 | } 95 | -------------------------------------------------------------------------------- /src/webserver/db/mapObjectToResource.js: -------------------------------------------------------------------------------- 1 | import {ParametersError} from './errors' 2 | import * as schemas from '../../schemas' 3 | 4 | function prepareId(id) { 5 | return id.toString().replace(/^#/, '') 6 | } 7 | 8 | async function fetchRelationshipDataThroughIn({db, relationship, type, id}) { 9 | const links = await db.query(`SELECT in FROM ${type} WHERE @rid = ${id}`) 10 | 11 | if (!links[0]) { 12 | return 13 | } 14 | 15 | return {type: relationship.class, id: prepareId(links[0].in)} 16 | } 17 | 18 | async function fetchRelationshipDataThroughEdge({db, relationship, type, id, reverse = false}) { 19 | const edgeClass = reverse ? relationship.reverseThroughEdge : relationship.throughEdge 20 | const fn = reverse ? 'IN' : 'OUT' 21 | 22 | const links = await db.query(`SELECT ${fn}('${edgeClass}') FROM ${type} WHERE @rid = ${id}`) 23 | 24 | if (!links[0]) { 25 | return [] 26 | } 27 | 28 | return links[0][fn].map(linkId => ({type: relationship.class, id: prepareId(linkId)})) 29 | } 30 | 31 | async function fetchRelationshipDataEdge({db, relationship, type, id}) { 32 | const links = await db.query(`SELECT OUTE('${relationship.class}') FROM ${type} WHERE @rid = ${id}`) 33 | 34 | if (!links[0]) { 35 | return [] 36 | } 37 | 38 | return links[0].OUTE.map(linkId => ({type: relationship.class, id: prepareId(linkId)})) 39 | } 40 | 41 | async function fetchRelationshipData({db, relationship, type, id, field}) { 42 | if (relationship.throughEdge === type) { 43 | return await fetchRelationshipDataThroughIn({db, relationship, type, id}) 44 | } 45 | 46 | if (relationship.throughEdge) { 47 | return await fetchRelationshipDataThroughEdge({db, relationship, type, id}) 48 | } 49 | 50 | if (relationship.reverseThroughEdge) { 51 | return await fetchRelationshipDataThroughEdge({db, relationship, type, id, reverse: true}) 52 | } 53 | 54 | if (relationship.class) { 55 | return await fetchRelationshipDataEdge({db, relationship, type, id}) 56 | } 57 | 58 | throw new Error(`Invalid relationship definition "${field}" in class "${type}"`) 59 | } 60 | 61 | export default async function mapObjectToResource(db, obj, fields) { 62 | const type = obj['@class'] 63 | const id = obj['@rid'] 64 | const classSchema = schemas[type] 65 | 66 | if (!classSchema) { 67 | throw new Error(`Schema for class "${type}" is not defined`) 68 | } 69 | 70 | if (!fields) { 71 | fields = Object.keys(classSchema.attributes || {}) 72 | .concat(Object.keys(classSchema.relationships || {})) 73 | } 74 | 75 | const attributes = {} 76 | const relationships = {} 77 | 78 | for (const field of fields) { 79 | if (field === 'id' || field === 'type') { 80 | continue 81 | 82 | } else if (classSchema.attributes[field]) { 83 | attributes[field] = obj[field] 84 | 85 | } else if (classSchema.relationships[field]) { 86 | relationships[field] = { 87 | data: await fetchRelationshipData({ 88 | db, 89 | relationship: classSchema.relationships[field], 90 | type, 91 | id, 92 | field 93 | }) 94 | } 95 | 96 | } else { 97 | throw new ParametersError(`Class "${type}" haven't field "${field}"`) 98 | } 99 | } 100 | 101 | const resource = { 102 | type, 103 | id: prepareId(id), 104 | attributes, 105 | relationships 106 | } 107 | 108 | return resource 109 | } 110 | -------------------------------------------------------------------------------- /src/webserver/db/mapResourceToTransaction.js: -------------------------------------------------------------------------------- 1 | export default function mapResourceToTransaction(resource, schema) { 2 | if (!resource) { 3 | return 4 | } 5 | 6 | const className = resource.type 7 | 8 | if (className !== schema.type) { 9 | throw new Error(`Unexpected resource type "${className}", expected "${schema.type}"`) 10 | } 11 | 12 | const id = resource.id && `#${resource.id}` 13 | const resourceAttributes = resource.attributes || {} 14 | const resourceRelationships = resource.relationships || {} 15 | const steps = [] 16 | 17 | const attributes = Object.keys(schema.attributes) 18 | .reduce((acc, key) => { 19 | if (resourceAttributes[key]) { 20 | acc[key] = resourceAttributes[key] 21 | } 22 | 23 | return acc 24 | }, {}) 25 | 26 | steps.push({ 27 | key: 'object', 28 | type: id ? 'updateVertex' : 'createVertex', 29 | class: className, 30 | id, 31 | attributes 32 | }) 33 | 34 | Object.keys(schema.relationships) 35 | .filter(key => resourceRelationships[key]) 36 | .forEach(key => { 37 | const relationshipSchema = schema.relationships[key] 38 | const data = resourceRelationships[key].data 39 | 40 | if (relationshipSchema.throughEdge && data.length > 0) { 41 | data 42 | .forEach(link => { 43 | if (link.type !== relationshipSchema.class) { 44 | throw new Error(`Unexpected relationships type "${link.type}", expected "${relationshipSchema.class}"`) 45 | } 46 | 47 | const key1 = 'relationshipClean' + steps.length 48 | const key2 = 'relationshipFind' + steps.length 49 | const key3 = 'relationshipEdge' + steps.length 50 | 51 | if (id) { 52 | steps.push({ 53 | key: key1, 54 | type: 'removeEdges', 55 | class: relationshipSchema.throughEdge, 56 | from: id 57 | }) 58 | } 59 | 60 | steps.push({ 61 | key: key2, 62 | type: 'findOne', 63 | class: relationshipSchema.class, 64 | id: `#${link.id}` 65 | }) 66 | 67 | steps.push({ 68 | key: key3, 69 | type: 'createEdge', 70 | class: relationshipSchema.throughEdge, 71 | from: id ? id : '$object', 72 | to: `\$${key2}` 73 | }) 74 | }) 75 | } 76 | }) 77 | 78 | let returnKey = '$object' 79 | 80 | if (id) { 81 | returnKey = '$result' 82 | steps.push({ 83 | key: 'result', 84 | type: 'findOne', 85 | class: schema.type, 86 | id 87 | }) 88 | } 89 | 90 | return {steps, returnKey, returnType: 'one'} 91 | } 92 | -------------------------------------------------------------------------------- /src/webserver/di/app.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import morgan from 'morgan' 3 | import bodyParser from 'body-parser' 4 | import cookieParser from 'cookie-parser' 5 | import orientoDbMiddleware from './orientoDbMiddleware' 6 | import wrapHandler from '../utilities/wrapHandler' 7 | import renderPage from '../utilities/renderPage' 8 | import config from './config' 9 | import handlers from '../handlers' 10 | import wrapPassportMiddleware from './wrapPassportMiddleware' 11 | import cookieAuthMiddleware from './cookieAuthMiddleware' 12 | 13 | // Setup 14 | 15 | const app = express() 16 | 17 | app.use(morgan('combined')) 18 | app.disable('x-powered-by') 19 | 20 | // API 21 | 22 | app.use('/api/*', wrapPassportMiddleware('basic')) 23 | app.use('/api/*', wrapPassportMiddleware('bearer')) 24 | app.use('/api/*', bodyParser.json()) 25 | app.use('/api/*', orientoDbMiddleware) 26 | 27 | app.use('/xhr/*', cookieParser()) 28 | 29 | handlers 30 | .forEach(({method, route, handler}) => { 31 | app[method](route, wrapHandler(handler)) 32 | }) 33 | 34 | app.all('/api/*', wrapHandler(() => ({status: 404, body: {data: null}}))) // null-reasonable 35 | 36 | // Front end 37 | 38 | if (config.webserver.publicDir) { 39 | app.use(express.static(config.webserver.publicDir, { 40 | index: false 41 | })) 42 | } 43 | 44 | app.use(cookieParser()) 45 | app.use(cookieAuthMiddleware) 46 | 47 | app.get('*', wrapHandler(renderPage)) 48 | 49 | export default app 50 | -------------------------------------------------------------------------------- /src/webserver/di/config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | export default from '../../config' 4 | -------------------------------------------------------------------------------- /src/webserver/di/cookieAuthMiddleware.js: -------------------------------------------------------------------------------- 1 | import orientoDb from './orientoDb' 2 | import mapObjectToResource from '../db/mapObjectToResource' 3 | 4 | export default async function cookieAuthMiddleware(request, response, next) { 5 | if (!request.cookies.accessToken) { 6 | return next() 7 | } 8 | 9 | try { 10 | const token = await orientoDb 11 | .select() 12 | .from('AccessToken') 13 | .where({hash: request.cookies.accessToken}) 14 | .one() 15 | 16 | if (!token) { 17 | return 18 | } 19 | 20 | const tokenResource = await mapObjectToResource(orientoDb, token) 21 | 22 | if (tokenResource.relationships.user.data.length < 1) { 23 | return 24 | } 25 | 26 | const user = await orientoDb 27 | .select() 28 | .from('User') 29 | .where({ 30 | '@rid': tokenResource.relationships.user.data[0].id 31 | }) 32 | .one() 33 | 34 | if (!user) { 35 | return 36 | } 37 | 38 | const userResource = await mapObjectToResource(orientoDb, user) 39 | 40 | request.tokenResource = tokenResource 41 | request.userResource = userResource 42 | } catch (error) { 43 | return next(error) 44 | } 45 | 46 | next() 47 | } 48 | -------------------------------------------------------------------------------- /src/webserver/di/orientoDb.js: -------------------------------------------------------------------------------- 1 | import orientoServer from './orientoServer' 2 | import config from './config' 3 | 4 | export default orientoServer.use({ 5 | name: config.database.dbname, 6 | username: config.database.dbuser, 7 | password: config.database.dbpassword 8 | }) 9 | -------------------------------------------------------------------------------- /src/webserver/di/orientoDbMiddleware.js: -------------------------------------------------------------------------------- 1 | import orientoDb from './orientoDb' 2 | 3 | export default function orientoDbMiddleware(request, response, next) { 4 | request.db = orientoDb 5 | next() 6 | } 7 | -------------------------------------------------------------------------------- /src/webserver/di/orientoServer.js: -------------------------------------------------------------------------------- 1 | import oriento from 'oriento' 2 | import config from './config' 3 | 4 | export default oriento({ 5 | host: config.database.host, 6 | port: config.database.port, 7 | username: config.database.user, 8 | password: config.database.password 9 | }) 10 | -------------------------------------------------------------------------------- /src/webserver/di/passport.js: -------------------------------------------------------------------------------- 1 | import passport from 'passport' 2 | import {BasicStrategy} from 'passport-http' 3 | import BearerStrategy from 'passport-http-bearer' 4 | import db from './orientoDb' 5 | import bcrypt from 'bcryptjs' 6 | 7 | passport.use( 8 | new BasicStrategy(async (email, password, callback) => { 9 | try { 10 | const user = await db 11 | .select() 12 | .from('User') 13 | .where({email}) 14 | .one() 15 | 16 | if (!user) { 17 | return callback(undefined, false) 18 | } 19 | 20 | const validPassword = await new Promise((resolve, reject) => { 21 | bcrypt.compare(password, user.password, (error, result) => { 22 | if (error) { 23 | reject(error) 24 | } else { 25 | resolve(result) 26 | } 27 | }) 28 | }) 29 | 30 | if (!validPassword) { 31 | return callback(undefined, false) 32 | } 33 | 34 | callback(undefined, user) 35 | } catch (error) { 36 | callback(error) 37 | } 38 | }) 39 | ) 40 | 41 | passport.use( 42 | new BearerStrategy(async (token, callback) => { 43 | try { 44 | const user = await db 45 | .select('EXPAND(IN("UserToken"))') 46 | .from('AccessToken') 47 | .where({hash: token}) 48 | .one() 49 | 50 | if (!user) { 51 | return callback(undefined, false) 52 | } 53 | 54 | callback(undefined, user) 55 | 56 | } catch (error) { 57 | callback(error) 58 | } 59 | }) 60 | ) 61 | 62 | export default passport 63 | -------------------------------------------------------------------------------- /src/webserver/di/wrapPassportMiddleware.js: -------------------------------------------------------------------------------- 1 | import passport from './passport' 2 | 3 | export default function wrapPassportMiddleware(name) { 4 | return (request, response, next) => { 5 | const middleware = passport.authenticate(name, {session: false}, (error, user) => { 6 | if (error) { 7 | return next(error) 8 | } 9 | 10 | if (user) { 11 | request.user = user 12 | } 13 | 14 | next() 15 | }) 16 | 17 | middleware(request, response, next) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/webserver/handlers/index.js: -------------------------------------------------------------------------------- 1 | import * as users from './users' 2 | import * as tokens from './tokens' 3 | import * as xhr from './xhr' 4 | 5 | export default [ 6 | {method: 'post', route: '/xhr/login', handler: xhr.loginHandler}, 7 | {method: 'post', route: '/xhr/logout', handler: xhr.logoutHandler}, 8 | {method: 'post', route: '/api/v1/tokens/', handler: tokens.postHandler}, 9 | {method: 'delete', route: '/api/v1/tokens/', handler: tokens.deleteHandler}, 10 | {method: 'get', route: '/api/v1/users/', handler: users.indexHandler}, 11 | {method: 'post', route: '/api/v1/users/', handler: users.postHandler}, 12 | {method: 'get', route: '/api/v1/users/:id', handler: users.getHandler}, 13 | {method: 'patch', route: '/api/v1/users/:id', handler: users.patchHandler}, 14 | {method: 'delete', route: '/api/v1/users/:id', handler: users.deleteHandler} 15 | ] 16 | -------------------------------------------------------------------------------- /src/webserver/handlers/tokens.js: -------------------------------------------------------------------------------- 1 | import applyTransaction from '../db/applyTransaction' 2 | import fulfillQuery from '../db/fulfillQuery' 3 | import getParamsFromRequest from '../utilities/getParamsFromRequest' 4 | import {randomBytes} from 'crypto' 5 | 6 | export async function postHandler(request) { 7 | const {db, user} = request 8 | 9 | if (!user) { 10 | return { 11 | status: 401, 12 | body: {data: null} // null-reasonable 13 | } 14 | } 15 | 16 | const hash = randomBytes(128).toString('hex') 17 | 18 | const token = await applyTransaction(db, { 19 | steps: [ 20 | { 21 | key: 'token', 22 | type: 'createVertex', 23 | class: 'AccessToken', 24 | attributes: { 25 | hash 26 | } 27 | }, 28 | { 29 | key: 'usertoken', 30 | type: 'createEdge', 31 | class: 'UserToken', 32 | from: user['@rid'], 33 | to: '$token' 34 | } 35 | ], 36 | returnKey: '$token', 37 | returnType: 'one' 38 | }) 39 | 40 | const body = await fulfillQuery( 41 | db, 42 | token, 43 | getParamsFromRequest(request) 44 | ) 45 | 46 | return { 47 | status: 201, 48 | body 49 | } 50 | } 51 | 52 | 53 | export async function deleteHandler(request) { 54 | const {db, query: {filter: {hash}}} = request 55 | 56 | const deletedCount = await db 57 | .delete('VERTEX', 'AccessToken') 58 | .where({hash}) 59 | .limit(1) 60 | .scalar() 61 | 62 | return { 63 | status: deletedCount > 0 ? 200 : 404, 64 | body: {data: null} // null-reasonable 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/webserver/handlers/users.js: -------------------------------------------------------------------------------- 1 | import UserSchema from '../../schemas/User' 2 | import mapResourceToTransaction from '../db/mapResourceToTransaction' 3 | import applyTransaction from '../db/applyTransaction' 4 | import fulfillQuery from '../db/fulfillQuery' 5 | import validateResourceBySchema from '../utilities/validateResourceBySchema' 6 | import getParamsFromRequest from '../utilities/getParamsFromRequest' 7 | import {OP} from 'access-rule' 8 | 9 | export async function indexHandler(request) { 10 | const {db} = request 11 | const currentUser = request.user 12 | 13 | if (!UserSchema.schemaAccessRule(UserSchema, currentUser, OP.GET)) { 14 | return { 15 | status: currentUser ? 403 : 401, 16 | body: {data: null} // null-reasonable 17 | } 18 | } 19 | 20 | const users = (await db 21 | .select() 22 | .from(UserSchema.type) 23 | .all()) 24 | .filter(user => UserSchema.resourceAccessRule(user, currentUser, OP.GET)) 25 | 26 | const body = await fulfillQuery( 27 | db, 28 | users, 29 | getParamsFromRequest(request) 30 | ) 31 | 32 | return { 33 | body 34 | } 35 | } 36 | 37 | export async function getHandler(request) { 38 | const {db, params: {id}} = request 39 | const currentUser = request.user 40 | 41 | if (!UserSchema.schemaAccessRule(UserSchema, currentUser, OP.GET)) { 42 | return { 43 | status: currentUser ? 403 : 401, 44 | body: {data: null} // null-reasonable 45 | } 46 | } 47 | 48 | const user = await db 49 | .select() 50 | .from(UserSchema.type) 51 | .where({'@rid': id}) 52 | .one() 53 | 54 | if (!UserSchema.resourceAccessRule(user, currentUser, OP.GET)) { 55 | return { 56 | status: 403, 57 | body: {data: null} // null-reasonable 58 | } 59 | } 60 | 61 | const body = await fulfillQuery( 62 | db, 63 | user, 64 | getParamsFromRequest(request) 65 | ) 66 | 67 | return { 68 | status: body.data ? 200 : 404, 69 | body 70 | } 71 | } 72 | 73 | export async function postHandler(request) { 74 | const {db, body: {data}} = request 75 | const currentUser = request.user 76 | 77 | if (!UserSchema.schemaAccessRule(UserSchema, currentUser, OP.POST)) { 78 | return { 79 | status: currentUser ? 403 : 401, 80 | body: {data: null} // null-reasonable 81 | } 82 | } 83 | 84 | if (!data) { 85 | return { 86 | status: 400, 87 | body: {errors: [{ 88 | title: 'Request must include a single resource object as primary data' 89 | }]} 90 | } 91 | } 92 | 93 | if (data.id) { 94 | return { 95 | status: 400, 96 | body: {errors: [{ 97 | title: 'Client-generated id doesn\'t supported' 98 | }]} 99 | } 100 | } 101 | 102 | const errors = validateResourceBySchema(data, UserSchema, true) 103 | 104 | if (errors.length > 0) { 105 | return { 106 | status: 400, 107 | body: {errors} 108 | } 109 | } 110 | 111 | const normalizedData = UserSchema.normalize 112 | ? UserSchema.normalize(data) 113 | : data 114 | const transaction = mapResourceToTransaction(normalizedData, UserSchema) 115 | const user = await applyTransaction(db, transaction) 116 | 117 | const body = await fulfillQuery( 118 | db, 119 | user, 120 | getParamsFromRequest(request) 121 | ) 122 | 123 | return { 124 | status: 201, 125 | body 126 | } 127 | } 128 | 129 | export async function deleteHandler(request) { 130 | const {db, params: {id}} = request 131 | const currentUser = request.user 132 | 133 | if (!UserSchema.schemaAccessRule(UserSchema, currentUser, OP.DELETE)) { 134 | return { 135 | status: currentUser ? 403 : 401, 136 | body: {data: null} // null-reasonable 137 | } 138 | } 139 | 140 | const user = await db 141 | .select() 142 | .from(UserSchema.type) 143 | .where({'@rid': id}) 144 | .one() 145 | 146 | if (!user) { 147 | return { 148 | status: 404, 149 | body: {data: null} // null-reasonable 150 | } 151 | } 152 | 153 | if (!UserSchema.resourceAccessRule(user, currentUser, OP.DELETE)) { 154 | return { 155 | status: 403, 156 | body: {data: null} // null-reasonable 157 | } 158 | } 159 | 160 | await db 161 | .delete('VERTEX', UserSchema.type) 162 | .where({'@rid': id}) 163 | .limit(1) 164 | .scalar() 165 | 166 | return { 167 | status: 200, 168 | body: {data: null} // null-reasonable 169 | } 170 | } 171 | 172 | export async function patchHandler(request) { 173 | const {db, params: {id}, body: {data}} = request 174 | const currentUser = request.user 175 | 176 | if (!UserSchema.schemaAccessRule(UserSchema, currentUser, OP.PATCH)) { 177 | return { 178 | status: currentUser ? 403 : 401, 179 | body: {data: null} // null-reasonable 180 | } 181 | } 182 | 183 | const user = await db 184 | .select() 185 | .from(UserSchema.type) 186 | .where({'@rid': id}) 187 | .one() 188 | 189 | if (!user) { 190 | return { 191 | status: 404, 192 | body: {data: null} // null-reasonable 193 | } 194 | } 195 | 196 | if (!UserSchema.resourceAccessRule(user, currentUser, OP.PATCH)) { 197 | return { 198 | status: 403, 199 | body: {data: null} // null-reasonable 200 | } 201 | } 202 | 203 | if (!data) { 204 | return { 205 | status: 400, 206 | body: {errors: [{ 207 | title: 'Request must include a single resource object as primary data' 208 | }]} 209 | } 210 | } 211 | 212 | if (data.id !== id) { 213 | return { 214 | status: 400, 215 | body: {errors: [{ 216 | title: 'Resource id can not be changed' 217 | }]} 218 | } 219 | } 220 | 221 | const errors = validateResourceBySchema(data, UserSchema) 222 | 223 | if (errors.length > 0) { 224 | return { 225 | status: 400, 226 | body: {errors} 227 | } 228 | } 229 | 230 | const normalizedData = UserSchema.normalize 231 | ? UserSchema.normalize(data) 232 | : data 233 | const transaction = mapResourceToTransaction(normalizedData, UserSchema) 234 | const updatedUser = await applyTransaction(db, transaction) 235 | 236 | const body = await fulfillQuery( 237 | db, 238 | updatedUser, 239 | getParamsFromRequest(request) 240 | ) 241 | 242 | return { 243 | status: 200, 244 | body 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /src/webserver/handlers/xhr.js: -------------------------------------------------------------------------------- 1 | import apiRequest from '../../utilities/apiRequest' 2 | import getParamsFromRequest from '../utilities/getParamsFromRequest' 3 | 4 | export async function loginHandler(request) { 5 | const {fields, include} = getParamsFromRequest(request) 6 | 7 | const response = await apiRequest('/api/v1/tokens/', { 8 | method: 'POST', 9 | fields, 10 | include, 11 | headers: { 12 | 'Authorization': request.headers.authorization 13 | } 14 | }) 15 | 16 | return { 17 | cookies: [ 18 | { 19 | name: 'accessToken', 20 | value: response.data.attributes.hash, 21 | httpOnly: true 22 | } 23 | ], 24 | body: response 25 | } 26 | } 27 | 28 | export async function logoutHandler(request) { 29 | const token = request.cookies.accessToken 30 | 31 | const response = await apiRequest(`/api/v1/tokens/?filter[hash]=${token}`, { 32 | method: 'DELETE', 33 | headers: { 34 | 'Authorization': `Bearer ${token}` 35 | } 36 | }) 37 | 38 | return { 39 | body: response 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/webserver/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | import './polyfills' 4 | export default from './di/app' 5 | -------------------------------------------------------------------------------- /src/webserver/polyfills.js: -------------------------------------------------------------------------------- 1 | import '../utilities/patchReact' 2 | import fetch from 'node-fetch' 3 | 4 | global.fetch = fetch 5 | -------------------------------------------------------------------------------- /src/webserver/utilities/appTemplate.js: -------------------------------------------------------------------------------- 1 | export default function appTemplate(title, content, state) { 2 | return ` 3 | 4 | 5 | 6 | ${title} 7 | 8 | 9 |
${content}
10 | 11 | 21 | 22 | 23 | ` 24 | } 25 | -------------------------------------------------------------------------------- /src/webserver/utilities/getParamsFromRequest.js: -------------------------------------------------------------------------------- 1 | import {mapObj} from 'ramda' 2 | 3 | export default function getParamsFromRequest(request) { 4 | const fields = typeof request.query.fields === 'object' 5 | ? mapObj(fs => fs.split(',').filter(f => f.trim().length > 0), request.query.fields) 6 | : {} 7 | 8 | const include = request.query.include 9 | ? request.query.include.split(',') 10 | : [] 11 | 12 | return {fields, include} 13 | } 14 | -------------------------------------------------------------------------------- /src/webserver/utilities/renderPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import createDispatcher from '../../utilities/createDispatcher' 3 | import appTemplate from './appTemplate' 4 | import {setToken} from '../../flux/token/tokenActions' 5 | import {runTransition} from '../../flux/router/routerActions' 6 | import {mergeResources} from '../../flux/resources/resourcesActions' 7 | import Application from '../../components/Application' 8 | 9 | export default async function renderPage(request) { 10 | const {tokenResource, userResource} = request 11 | 12 | const dispatcher = createDispatcher() 13 | 14 | if (tokenResource && userResource) { 15 | await dispatcher.dispatch(setToken(tokenResource.attributes.hash)) 16 | await dispatcher.dispatch(mergeResources([tokenResource, userResource])) 17 | } 18 | 19 | const {title, status} = await dispatcher.dispatch(runTransition(request.originalUrl)) 20 | 21 | if (status === 401) { 22 | return { 23 | status: 302, 24 | location: '/' 25 | } 26 | } 27 | 28 | const state = dispatcher.getState() 29 | 30 | const pageContent = React.renderToString( 31 | 32 | ) 33 | 34 | return { 35 | headers: { 36 | 'Cache-Control': 'no-cache, private, max-age=86400', 37 | 'Vary': 'Cookie' 38 | }, 39 | status, 40 | body: appTemplate(title, pageContent, state) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/webserver/utilities/validateResourceBySchema.js: -------------------------------------------------------------------------------- 1 | export default function validateResourceBySchema(resource, schema, force = false) { 2 | const resourceAttributes = resource && resource.attributes || {} 3 | const resourceRelationships = resource && resource.relationships || {} 4 | const schemaAttributes = schema.attributes || {} 5 | const schemaRelationships = schema.relationships || {} 6 | 7 | const attributeErrors = Object.keys(schemaAttributes) 8 | .reduce((acc, key) => { 9 | const constraint = schemaAttributes[key] 10 | const value = resourceAttributes[key] 11 | const validationNeeded = key in resourceAttributes || force 12 | 13 | if (validationNeeded) { 14 | const constraintResult = constraint(value) 15 | 16 | if (!constraintResult.valid) { 17 | acc.push({ 18 | status: 400, 19 | title: constraintResult.message, 20 | source: { 21 | pointer: `/data/attributes/${key}` 22 | }, 23 | meta: { 24 | constraintResult 25 | } 26 | }) 27 | } 28 | } 29 | 30 | return acc 31 | }, []) 32 | 33 | const relationshipErrors = Object.keys(schemaRelationships) 34 | .reduce((acc, key) => { 35 | const relationshipSchema = schemaRelationships[key] 36 | const relationship = resourceRelationships[key] 37 | 38 | const validateRelationship = ({type, id}, index) => { 39 | if (type !== relationshipSchema.class) { 40 | acc.push({ 41 | status: 400, 42 | title: `Relationship type doesn't match schema`, 43 | source: { 44 | pointer: `/data/relationships/${key}` 45 | }, 46 | meta: { 47 | actual: type, 48 | expected: relationshipSchema.class, 49 | index 50 | } 51 | }) 52 | } 53 | } 54 | 55 | if (relationship) { 56 | const {data} = relationship 57 | 58 | if (Array.isArray(data)) { 59 | data.map(validateRelationship) 60 | } else if (data) { 61 | validateRelationship(data) 62 | } 63 | } 64 | 65 | return acc 66 | }, []) 67 | 68 | return [...attributeErrors, ...relationshipErrors] 69 | } 70 | -------------------------------------------------------------------------------- /src/webserver/utilities/wrapHandler.js: -------------------------------------------------------------------------------- 1 | import {ParametersError} from '../db/errors' 2 | 3 | function getErrorMetaInformation(error) { 4 | const errorMeta = { 5 | status: 500, 6 | title: error.message, 7 | detail: error.stack 8 | } 9 | 10 | if (error.responseStatus) { 11 | errorMeta.status = error.responseStatus 12 | 13 | if (error.responseStatus < 500) { 14 | delete errorMeta.detail 15 | } 16 | } 17 | 18 | if (error instanceof ParametersError) { 19 | errorMeta.status = 400 20 | } 21 | 22 | if (error.type === 'com.orientechnologies.orient.core.storage.ORecordDuplicatedException') { 23 | const matches = /key \'(.*)\' in index \'(.*)\' previously/.exec(error.message) 24 | 25 | if (matches) { 26 | const value = matches[1] 27 | const dbIndex = matches[2].split('.') 28 | const className = dbIndex[0] 29 | const fields = dbIndex[1].split('_') 30 | 31 | delete errorMeta.detail 32 | 33 | errorMeta.status = 409 34 | errorMeta.title = `Found duplicated key "${value}" in index "${matches[2]}"` 35 | errorMeta.meta = {value, dbIndex, className, fields} 36 | 37 | if (fields.length === 1) { 38 | errorMeta.source = { 39 | pointer: `/data/attributes/${fields[0]}` 40 | } 41 | } 42 | } 43 | } 44 | 45 | return errorMeta 46 | } 47 | 48 | export default function wrapHandler(handler) { 49 | return async (request, response) => { 50 | const isApiRequest = /^\/api\//.test(request.originalUrl) 51 | const isAjaxRequest = /^\/xhr\//.test(request.originalUrl) 52 | 53 | let contentType 54 | let status = 200 55 | let body 56 | 57 | try { 58 | const responseMeta = await handler(request) 59 | 60 | if (responseMeta.location) { 61 | response.location(responseMeta.location) 62 | } 63 | 64 | if (responseMeta.headers) { 65 | Object.keys(responseMeta.headers) 66 | .forEach(key => { 67 | response.set(key, responseMeta.headers[key]) 68 | }) 69 | } 70 | 71 | if (responseMeta.cookies) { 72 | responseMeta.cookies 73 | .forEach(({name, value, ...options}) => { 74 | response.cookie(name, value, options) 75 | }) 76 | } 77 | 78 | if (responseMeta.contentType) { 79 | contentType = responseMeta.contentType 80 | } 81 | 82 | if (responseMeta.status) { 83 | status = responseMeta.status 84 | } 85 | 86 | if (responseMeta.body) { 87 | body = responseMeta.body 88 | } 89 | } catch (error) { 90 | const errorMeta = getErrorMetaInformation(error) 91 | 92 | status = errorMeta.status 93 | 94 | if (isApiRequest || isAjaxRequest) { 95 | body = {errors: [errorMeta]} 96 | } else { 97 | contentType = 'text/plain' 98 | body = errorMeta.detail || errorMeta.title 99 | } 100 | } 101 | 102 | if (!contentType && isAjaxRequest) { 103 | contentType = 'application/json' 104 | } 105 | 106 | if (!contentType && isApiRequest) { 107 | contentType = 'application/vnd.api+json' 108 | } 109 | 110 | if (contentType) { 111 | response.header('Content-Type', contentType) 112 | } 113 | 114 | if (typeof body === 'number') { 115 | body = String(body) 116 | } 117 | 118 | response.status(status) 119 | response.send(body) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "chai": true, 7 | "assert": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/api/indexTest.js: -------------------------------------------------------------------------------- 1 | import index from '../../src/api/index' // eslint-disable-line 2 | 3 | describe('api/index', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/api/usersPageApiRequestTest.js: -------------------------------------------------------------------------------- 1 | import usersPageApiRequest from '../../src/api/usersPageApiRequest' // eslint-disable-line 2 | 3 | describe('api/usersPageApiRequest', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/components/AboutPage/AboutPageTest.js: -------------------------------------------------------------------------------- 1 | import AboutPage from '../../../src/components/AboutPage/AboutPage' // eslint-disable-line 2 | 3 | describe('components/AboutPage/AboutPage', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/components/AboutPage/indexTest.js: -------------------------------------------------------------------------------- 1 | import index from '../../../src/components/AboutPage/index' // eslint-disable-line 2 | 3 | describe('components/AboutPage/index', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/components/Application/ApplicationTest.js: -------------------------------------------------------------------------------- 1 | import Application from '../../../src/components/Application/Application' // eslint-disable-line 2 | 3 | describe('components/Application/Application', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/components/Application/indexTest.js: -------------------------------------------------------------------------------- 1 | import index from '../../../src/components/Application/index' // eslint-disable-line 2 | 3 | describe('components/Application/index', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/components/ErrorPage/ErrorPageTest.js: -------------------------------------------------------------------------------- 1 | import ErrorPage from '../../../src/components/ErrorPage/ErrorPage' // eslint-disable-line 2 | 3 | describe('components/ErrorPage/ErrorPage', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/components/ErrorPage/indexTest.js: -------------------------------------------------------------------------------- 1 | import index from '../../../src/components/ErrorPage/index' // eslint-disable-line 2 | 3 | describe('components/ErrorPage/index', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/components/HomePage/HomePageTest.js: -------------------------------------------------------------------------------- 1 | import HomePage from '../../../src/components/HomePage/HomePage' // eslint-disable-line 2 | 3 | describe('components/HomePage/HomePage', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/components/HomePage/indexTest.js: -------------------------------------------------------------------------------- 1 | import index from '../../../src/components/HomePage/index' // eslint-disable-line 2 | 3 | describe('components/HomePage/index', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/components/Link/LinkTest.js: -------------------------------------------------------------------------------- 1 | import Link from '../../../src/components/Link/Link' // eslint-disable-line 2 | 3 | describe('components/Link/Link', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/components/Link/indexTest.js: -------------------------------------------------------------------------------- 1 | import index from '../../../src/components/Link/index' // eslint-disable-line 2 | 3 | describe('components/Link/index', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/components/NotFoundPage/NotFoundPageTest.js: -------------------------------------------------------------------------------- 1 | import NotFoundPage from '../../../src/components/NotFoundPage/NotFoundPage' // eslint-disable-line 2 | 3 | describe('components/NotFoundPage/NotFoundPage', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/components/NotFoundPage/indexTest.js: -------------------------------------------------------------------------------- 1 | import index from '../../../src/components/NotFoundPage/index' // eslint-disable-line 2 | 3 | describe('components/NotFoundPage/index', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/components/SignIn/SignInTest.js: -------------------------------------------------------------------------------- 1 | import SignIn from '../../../src/components/SignIn/SignIn' // eslint-disable-line 2 | 3 | describe('components/SignIn/SignIn', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/components/SignIn/indexTest.js: -------------------------------------------------------------------------------- 1 | import index from '../../../src/components/SignIn/index' // eslint-disable-line 2 | 3 | describe('components/SignIn/index', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/components/UsersPage/ItemTest.js: -------------------------------------------------------------------------------- 1 | import Item from '../../../src/components/UsersPage/Item' // eslint-disable-line 2 | 3 | describe('components/UsersPage/Item', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/components/UsersPage/OrderTest.js: -------------------------------------------------------------------------------- 1 | import Order from '../../../src/components/UsersPage/Order' // eslint-disable-line 2 | 3 | describe('components/UsersPage/Order', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/components/UsersPage/PositionTest.js: -------------------------------------------------------------------------------- 1 | import Position from '../../../src/components/UsersPage/Position' // eslint-disable-line 2 | 3 | describe('components/UsersPage/Position', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/components/UsersPage/UserTest.js: -------------------------------------------------------------------------------- 1 | import User from '../../../src/components/UsersPage/User' // eslint-disable-line 2 | 3 | describe('components/UsersPage/User', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/components/UsersPage/UsersPageTest.js: -------------------------------------------------------------------------------- 1 | import UsersPage from '../../../src/components/UsersPage/UsersPage' // eslint-disable-line 2 | 3 | describe('components/UsersPage/UsersPage', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/components/UsersPage/indexTest.js: -------------------------------------------------------------------------------- 1 | import index from '../../../src/components/UsersPage/index' // eslint-disable-line 2 | 3 | describe('components/UsersPage/index', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/components/pagesTest.js: -------------------------------------------------------------------------------- 1 | import * as pages from '../../src/components/pages' 2 | 3 | describe('components/pages', () => { 4 | it('should contain only pages', () => { 5 | Object.keys(pages).forEach(key => { 6 | assert.match(key, /Page$/) 7 | }) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /test/config/routingTest.js: -------------------------------------------------------------------------------- 1 | import routing from '../../src/config/routing' // eslint-disable-line 2 | 3 | describe('config/routing', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/flux/resources/resourcesActionsTest.js: -------------------------------------------------------------------------------- 1 | import resourcesActions from '../../../src/flux/resources/resourcesActions' // eslint-disable-line 2 | 3 | describe('flux/resources/resourcesActions', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/flux/resources/resourcesConstantsTest.js: -------------------------------------------------------------------------------- 1 | import resourcesConstants from '../../../src/flux/resources/resourcesConstants' // eslint-disable-line 2 | 3 | describe('flux/resources/resourcesConstants', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/flux/resources/resourcesStoreTest.js: -------------------------------------------------------------------------------- 1 | import resourcesStore from '../../../src/flux/resources/resourcesStore' // eslint-disable-line 2 | 3 | describe('flux/resources/resourcesStore', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/flux/router/routerActionsTest.js: -------------------------------------------------------------------------------- 1 | import routerActions from '../../../src/flux/router/routerActions' // eslint-disable-line 2 | 3 | describe('flux/router/routerActions', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/flux/router/routerConstantsTest.js: -------------------------------------------------------------------------------- 1 | import routerConstants from '../../../src/flux/router/routerConstants' // eslint-disable-line 2 | 3 | describe('flux/router/routerConstants', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/flux/router/routerStoreTest.js: -------------------------------------------------------------------------------- 1 | import routerStore from '../../../src/flux/router/routerStore' // eslint-disable-line 2 | 3 | describe('flux/router/routerStore', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/flux/signIn/signInActionsTest.js: -------------------------------------------------------------------------------- 1 | import signInActions from '../../../src/flux/signIn/signInActions' // eslint-disable-line 2 | 3 | describe('flux/signIn/signInActions', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/flux/signIn/signInConstantsTest.js: -------------------------------------------------------------------------------- 1 | import signInConstants from '../../../src/flux/signIn/signInConstants' // eslint-disable-line 2 | 3 | describe('flux/signIn/signInConstants', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/flux/signIn/signInStoreTest.js: -------------------------------------------------------------------------------- 1 | import signInStore from '../../../src/flux/signIn/signInStore' // eslint-disable-line 2 | 3 | describe('flux/signIn/signInStore', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/flux/signIn/signInValidatorTest.js: -------------------------------------------------------------------------------- 1 | import signInValidator from '../../../src/flux/signIn/signInValidator' // eslint-disable-line 2 | 3 | describe('flux/signIn/signInValidator', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/flux/storesTest.js: -------------------------------------------------------------------------------- 1 | import stores from '../../src/flux/stores' // eslint-disable-line 2 | 3 | describe('flux/stores', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/flux/token/tokenActionsTest.js: -------------------------------------------------------------------------------- 1 | import tokenActions from '../../../src/flux/token/tokenActions' // eslint-disable-line 2 | 3 | describe('flux/token/tokenActions', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/flux/token/tokenConstantsTest.js: -------------------------------------------------------------------------------- 1 | import tokenConstants from '../../../src/flux/token/tokenConstants' // eslint-disable-line 2 | 3 | describe('flux/token/tokenConstants', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/flux/token/tokenStoreTest.js: -------------------------------------------------------------------------------- 1 | import tokenStore from '../../../src/flux/token/tokenStore' // eslint-disable-line 2 | 3 | describe('flux/token/tokenStore', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/flux/transition/createFetchTransitionTest.js: -------------------------------------------------------------------------------- 1 | import createFetchTransition from '../../../src/flux/transition/createFetchTransition' // eslint-disable-line 2 | 3 | describe('flux/transition/createFetchTransition', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/flux/transition/createStaticTransitionTest.js: -------------------------------------------------------------------------------- 1 | import createStaticTransition from '../../../src/flux/transition/createStaticTransition' // eslint-disable-line 2 | 3 | describe('flux/transition/createStaticTransition', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/flux/transition/transitionActionsTest.js: -------------------------------------------------------------------------------- 1 | import transitionActions from '../../../src/flux/transition/transitionActions' // eslint-disable-line 2 | 3 | describe('flux/transition/transitionActions', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/schemas/AccessTokenTest.js: -------------------------------------------------------------------------------- 1 | import AccessToken from '../../src/schemas/AccessToken' // eslint-disable-line 2 | 3 | describe('schemas/AccessToken', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/schemas/ItemTest.js: -------------------------------------------------------------------------------- 1 | import Item from '../../src/schemas/Item' // eslint-disable-line 2 | 3 | describe('schemas/Item', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/schemas/OrderPositionTest.js: -------------------------------------------------------------------------------- 1 | import OrderPosition from '../../src/schemas/OrderPosition' // eslint-disable-line 2 | 3 | describe('schemas/OrderPosition', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/schemas/OrderTest.js: -------------------------------------------------------------------------------- 1 | import Order from '../../src/schemas/Order' // eslint-disable-line 2 | 3 | describe('schemas/Order', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/schemas/UserTest.js: -------------------------------------------------------------------------------- 1 | import User from '../../src/schemas/User' // eslint-disable-line 2 | 3 | describe('schemas/User', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/schemas/indexTest.js: -------------------------------------------------------------------------------- 1 | import index from '../../src/schemas/index' // eslint-disable-line 2 | 3 | describe('schemas/index', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/utilities/apiRequestTest.js: -------------------------------------------------------------------------------- 1 | import apiRequest from '../../src/utilities/apiRequest' // eslint-disable-line 2 | 3 | describe('utilities/apiRequest', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/utilities/bindActionTest.js: -------------------------------------------------------------------------------- 1 | import bindAction from '../../src/utilities/bindAction' // eslint-disable-line 2 | 3 | describe('utilities/bindAction', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/utilities/collectResourcesTest.js: -------------------------------------------------------------------------------- 1 | import collectResources from '../../src/utilities/collectResources' // eslint-disable-line 2 | 3 | describe('utilities/collectResources', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/utilities/createDispatcherTest.js: -------------------------------------------------------------------------------- 1 | import createDispatcher from '../../src/utilities/createDispatcher' // eslint-disable-line 2 | 3 | describe('utilities/createDispatcher', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/utilities/createStoreTest.js: -------------------------------------------------------------------------------- 1 | import createStore from '../../src/utilities/createStore' // eslint-disable-line 2 | 3 | describe('utilities/createStore', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/utilities/getCurrentUserTest.js: -------------------------------------------------------------------------------- 1 | import getCurrentUser from '../../src/utilities/getCurrentUser' // eslint-disable-line 2 | 3 | describe('utilities/getCurrentUser', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/utilities/patchReactTest.js: -------------------------------------------------------------------------------- 1 | import patchReact from '../../src/utilities/patchReact' // eslint-disable-line 2 | 3 | describe('utilities/patchReact', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/utilities/preventEventTest.js: -------------------------------------------------------------------------------- 1 | import preventEvent from '../../src/utilities/preventEvent' // eslint-disable-line 2 | 3 | describe('utilities/preventEvent', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/utilities/progressIndicatorTest.js: -------------------------------------------------------------------------------- 1 | import progressIndicator from '../../src/utilities/progressIndicator' // eslint-disable-line 2 | 3 | describe('utilities/progressIndicator', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/webserver/db/applyTransactionTest.js: -------------------------------------------------------------------------------- 1 | import applyTransaction from '../../../src/webserver/db/applyTransaction' // eslint-disable-line 2 | 3 | describe('webserver/db/applyTransaction', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/webserver/db/errorsTest.js: -------------------------------------------------------------------------------- 1 | import errors from '../../../src/webserver/db/errors' // eslint-disable-line 2 | 3 | describe('webserver/db/errors', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/webserver/db/fulfillQueryTest.js: -------------------------------------------------------------------------------- 1 | import fulfillQuery from '../../../src/webserver/db/fulfillQuery' // eslint-disable-line 2 | 3 | describe('webserver/db/fulfillQuery', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/webserver/db/mapObjectToResourceTest.js: -------------------------------------------------------------------------------- 1 | import mapObjectToResource from '../../../src/webserver/db/mapObjectToResource' // eslint-disable-line 2 | 3 | describe('webserver/db/mapObjectToResource', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/webserver/db/mapResourceToTransactionTest.js: -------------------------------------------------------------------------------- 1 | import mapResourceToTransaction from '../../../src/webserver/db/mapResourceToTransaction' // eslint-disable-line 2 | 3 | describe('webserver/db/mapResourceToTransaction', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/webserver/handlers/indexTest.js: -------------------------------------------------------------------------------- 1 | import index from '../../../src/webserver/handlers/index' // eslint-disable-line 2 | 3 | describe('webserver/handlers/index', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/webserver/handlers/tokensTest.js: -------------------------------------------------------------------------------- 1 | import tokens from '../../../src/webserver/handlers/tokens' // eslint-disable-line 2 | 3 | describe('webserver/handlers/tokens', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/webserver/handlers/usersTest.js: -------------------------------------------------------------------------------- 1 | import users from '../../../src/webserver/handlers/users' // eslint-disable-line 2 | 3 | describe('webserver/handlers/users', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/webserver/handlers/xhrTest.js: -------------------------------------------------------------------------------- 1 | import xhr from '../../../src/webserver/handlers/xhr' // eslint-disable-line 2 | 3 | describe('webserver/handlers/xhr', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/webserver/utilities/appTemplateTest.js: -------------------------------------------------------------------------------- 1 | import appTemplate from '../../../src/webserver/utilities/appTemplate' // eslint-disable-line 2 | 3 | describe('webserver/utilities/appTemplate', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/webserver/utilities/getParamsFromRequestTest.js: -------------------------------------------------------------------------------- 1 | import getParamsFromRequest from '../../../src/webserver/utilities/getParamsFromRequest' // eslint-disable-line 2 | 3 | describe('webserver/utilities/getParamsFromRequest', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/webserver/utilities/renderPageTest.js: -------------------------------------------------------------------------------- 1 | import renderPage from '../../../src/webserver/utilities/renderPage' // eslint-disable-line 2 | 3 | describe('webserver/utilities/renderPage', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/webserver/utilities/validateResourceBySchemaTest.js: -------------------------------------------------------------------------------- 1 | import validateResourceBySchema from '../../../src/webserver/utilities/validateResourceBySchema' // eslint-disable-line 2 | 3 | describe('webserver/utilities/validateResourceBySchema', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /test/webserver/utilities/wrapHandlerTest.js: -------------------------------------------------------------------------------- 1 | import wrapHandler from '../../../src/webserver/utilities/wrapHandler' // eslint-disable-line 2 | 3 | describe('webserver/utilities/wrapHandler', () => { 4 | it('should work') 5 | }) 6 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | require('babel-core/register') 2 | 3 | module.exports = [ 4 | require('./build/webpack/frontend'), 5 | require('./build/webpack/webserver'), 6 | require('./build/webpack/migrate'), 7 | require('./build/webpack/fixtures') 8 | ] 9 | --------------------------------------------------------------------------------