diff --git a/packages/http-client/src/lib/options/base-options.js b/packages/http-client/src/lib/options/base-options.js index 129ec1745279eac541c181e7552165e2006aa664..15d471ddde2e42a57df084528eca868f645a3e17 100644 --- a/packages/http-client/src/lib/options/base-options.js +++ b/packages/http-client/src/lib/options/base-options.js @@ -16,7 +16,7 @@ import { import { createConfig } from 'jsonql-params-validator' export const constProps = { contract: false, - MUTATION_ARGS: ['name', 'payload', 'conditions'], + MUTATION_ARGS: ['name', 'payload', 'conditions'], // this seems wrong? CONTENT_TYPE, BEARER, AUTH_HEADER diff --git a/packages/jwt/README.md b/packages/jwt/README.md index 3d79444b733046dfcb5ddd43cac716e085c87893..90feea87c030d01ad83e19c0169765cb814548fe 100644 --- a/packages/jwt/README.md +++ b/packages/jwt/README.md @@ -1,9 +1,12 @@ # jsonql-jwt -> A jwt based authentication system for jsonql +> A jwt based authentication system for jsonql, including http and socket (socket.io and ws) This library provide several methods that will be use in different jsonql javascript frameworks. +**We have taken out the `socket.io-client` and `ws` out of the dependencies since v1.2.4; +if you need them then you have to install this modules separately** + ## Installation ```sh @@ -12,7 +15,7 @@ $ npm i jsonql-jwt ### Node command line utility -If you install this globally +If you install this globally, you can use the command line utility ```sh $ jsonql-jwt rsa-pem diff --git a/packages/jwt/cmd.js b/packages/jwt/cmd.js index fce80853a8196475112a3e2056c1ef281c028fad..785d2885dbac7c990a7b6a8949c2a91976696181 100644 --- a/packages/jwt/cmd.js +++ b/packages/jwt/cmd.js @@ -1,66 +1,66 @@ #!/usr/bin/env node // https://nodejs.org/api/crypto.html // https://stackoverflow.com/questions/8520973/how-to-create-a-pair-private-public-keys-using-node-js-crypto -const { argv } = require('yargs'); -const colors = require('colors/safe'); -const fsx = require('fs-extra'); -const debug = require('debug')('jsonql-jwt:cmd'); +const { argv } = require('yargs') +const colors = require('colors/safe') +const fsx = require('fs-extra') +const debug = require('debug')('jsonql-jwt:cmd') -const { version } = require('./package.json'); -const { rsaKeys, jwtToken, rsaPemKeys } = require('./main'); +const { version } = require('./package.json') +const { rsaKeys, jwtToken, rsaPemKeys } = require('./main') const saveMsg = () => { - console.info('-----------------------------------------'); - console.info(colors.bgRed.white(`Please make a copy of these keys NOW!`)); -}; + console.info('-----------------------------------------') + console.info(colors.bgRed.white(`Please make a copy of these keys NOW!`)) +} // main method const run = argv => { let args = []; - console.info(colors.white(`Running jsonql-jwt cli version ${version}`)); + console.info(colors.white(`Running jsonql-jwt cli version ${version}`)) switch (argv._[0]) { case 'rsa-keys': args[0] = argv.format || undefined; args[1] = argv.len || undefined; - const { publicKey, privateKey } = Reflect.apply(rsaKeys, null, args); - console.log(colors.yellow('[RSA key pairs result]')); - console.log('PUBLIC KEY: ', colors.bgCyan.black(publicKey)); - console.log('PRIVATE KEY: ', colors.bgYellow.black(privateKey)); + const { publicKey, privateKey } = Reflect.apply(rsaKeys, null, args) + console.log(colors.yellow('[RSA key pairs result]')) + console.log('PUBLIC KEY: ', colors.bgCyan.black(publicKey)) + console.log('PRIVATE KEY: ', colors.bgYellow.black(privateKey)) saveMsg(); break; case 'rsa-pem': args[0] = argv.len || undefined; args[1] = argv.outputDir || ''; - const p = Reflect.apply(rsaPemKeys, null, args); + const p = Reflect.apply(rsaPemKeys, null, args) p.then(result => { if (!argv.outputDir) { - console.log('PUBLIC PEM KEY: \r\n', colors.bgCyan.black(result.publicKey)); - console.log('PRIVATE PEM KEY: \r\n', colors.bgYellow.black(result.privateKey)); + console.log('PUBLIC PEM KEY: \r\n', colors.bgCyan.black(result.publicKey)) + console.log('PRIVATE PEM KEY: \r\n', colors.bgYellow.black(result.privateKey)) saveMsg(); } }) .catch(err => { - console.error(colors.red(err.message || err)); + console.error(colors.red(err.message || err)) }); break; case 'token': if (argv.secret && argv.payload) { - console.log(argv.payload); + console.log(argv.payload) try { - const payload = JSON.parse(JSON.stringify(argv.payload)); - const token = jwtToken(payload, argv.secret); + const payload = JSON.parse(JSON.stringify(argv.payload)) + const token = jwtToken(payload, argv.secret) - console.log('JWT TOKEN: ', colors.bgCyan.black(token)); + console.log('JWT TOKEN: ', colors.bgCyan.black(token)) } catch(e) { - console.error('error!', colors.red(e)); + console.error('error!', colors.red(e)) } } break; default: - console.log(colors.red('You need to tell me what you want!')); + console.log(colors.red('You need to tell me what you want!')) } -}; +} // now run it -run(argv); +run(argv) diff --git a/packages/jwt/package.json b/packages/jwt/package.json index 10874469d1685a69cb745282c9acf2c305c12b66..38a7a28eca66cef31db0b755a77edcbc187b537b 100644 --- a/packages/jwt/package.json +++ b/packages/jwt/package.json @@ -1,7 +1,7 @@ { "name": "jsonql-jwt", - "version": "1.2.3", - "description": "jwt authentication and helpers library for jsonql", + "version": "1.2.4", + "description": "jwt authentication helpers library for jsonql", "main": "main.js", "module": "index.js", "browser": "dist/jsonql-jwt.js", @@ -40,7 +40,9 @@ "jsonql", "jwt", "crypto", - "rsa256" + "rsa256", + "socket.io", + "WebSocket" ], "author": "Joel Chu ", "license": "ISC", @@ -53,14 +55,14 @@ "jsonwebtoken": "^8.5.1", "jwt-decode": "^2.2.0", "socketio-jwt": "^4.5.0", - "yargs": "^13.3.0", - "socket.io-client": "^2.2.0", - "ws": "^7.1.2" + "yargs": "^13.3.0" }, "bin": { "jsonql-jwt": "./cmd.js" }, "devDependencies": { + "socket.io-client": "^2.2.0", + "ws": "^7.1.2", "ava": "^2.2.0", "debug": "^4.1.1", "esm": "^3.2.25", diff --git a/packages/jwt/src/server/socketio/node-clients.js b/packages/jwt/src/server/socketio/node-clients.js index 03ea700204b9601f0fb0e32bcea78e6a0817f8c4..b18b9ded443d3e25c19ef41ee7fa926d1fc53d0c 100644 --- a/packages/jwt/src/server/socketio/node-clients.js +++ b/packages/jwt/src/server/socketio/node-clients.js @@ -1,5 +1,11 @@ // take the export from the browser client then we wrap the code in // a node version implmentation that save some of the parameters +let socketIoClientModule; +try { + socketIoClientModule = require('socket.io-client') +} catch(e) { + console.error(`You need to install socket.io-client manually!`) +} const { socketIoHandshakeLogin, socketIoRoundtripLogin, @@ -7,7 +13,6 @@ const { socketIoClientAsync, socketIoChainConnect } = require('./clients') -const socketIoClientModule = require('socket.io-client') const { IO_ROUNDTRIP_LOGIN, IO_HANDSHAKE_LOGIN } = require('jsonql-constants') /** diff --git a/packages/jwt/src/server/ws/ws-client.js b/packages/jwt/src/server/ws/ws-client.js index aebc17682be464c53cb6002f5b95585a269ee2cb..3e845342db723a2d98defb13fd3a180924b46a93 100644 --- a/packages/jwt/src/server/ws/ws-client.js +++ b/packages/jwt/src/server/ws/ws-client.js @@ -1,9 +1,16 @@ // ws node clients -const WebSocket = require('ws') +let WebSocket; +try { + WebSocket = require('ws') +} catch(e) { + console.error(`You need to install ws module manually as a peer dependencies!`) +} + const { TOKEN_PARAM_NAME } = require('jsonql-constants') /** * normal client + * @param {object} WebSocket the ws object * @param {string} url end point * @param {object} [options={}] configuration * @return {object} ws instance diff --git a/packages/koa/README.md b/packages/koa/README.md index 31c2abcfd4dbe48bbdd1f2ce3f257a2773140140..9ae1048b8a21b672913adc414af1b8952503add5 100755 --- a/packages/koa/README.md +++ b/packages/koa/README.md @@ -1,8 +1,11 @@ [![NPM](https://nodei.co/npm/jsonql-koa.png?compact=true)](https://npmjs.org/package/jsonql-koa) -# Koa middleware using json:ql +# jsonql Koa middleware -An API construction tool for super fast development. +> An API construction tool for super fast development. + +**Many of this README is already outdated, we are preparing the complete documentation at this moment. +And it will publish to our website [jsonql.org](http://jsonql.org) shortly** ## Installation @@ -18,8 +21,6 @@ $ yarn add jsonql-koa ## Configuration - - ## Required Middlewares Also you need to install middleware to parse the JSON content. diff --git a/packages/koa/client.js b/packages/koa/client.js index defbabe3a2a63b21852f468fec0ac5cfdccf3734..93b4925e5469b477241db1db1c3ebab08eab60bb 100755 --- a/packages/koa/client.js +++ b/packages/koa/client.js @@ -1,20 +1,24 @@ -const { join } = require('path'); -const fs = require('fs'); -const fsx = require('fs-extra'); -const merge = require('lodash.merge'); -const debug = require('debug')('jsonql-koa:client'); -const jsonqlNodeClient = require('jsonql-node-client'); -// This is for the resolvers to include and get their node-client -module.exports = function(name, config) { +const { join } = require('path') +const fs = require('fs') +const fsx = require('fs-extra') +const merge = require('lodash.merge') +const debug = require('debug')('jsonql-koa:client') +const jsonqlNodeClient = require('jsonql-node-client') + +/** + * This is for the resolvers to include and get their node-client + * @param {string} name this client - it get automatically generate if the user didn't provide one + * @param {object} config configuration + * @return {promise} to resolve the node client instance + */ +module.exports = function createNodeClient(name, config) { if (!name) { - throw new Error('Name is required!'); + throw new Error('Name is required!') } if (!config.contractDir) { - throw new Error('contractDir is required'); + throw new Error('contractDir is required') } - config.contractDir = join(config.contractDir, name); - if (config) { - return jsonqlNodeClient(config); - } - throw new Error(`${name} client not found!`); + config.contractDir = join(config.contractDir, name) + + return jsonqlNodeClient(config) } diff --git a/packages/koa/package.json b/packages/koa/package.json index 5b1a7d28c16fce65c9ebb64ae6dd370ffa03919c..13ca8a20d3439ccd0a1e02a81258c725b5d97c4f 100755 --- a/packages/koa/package.json +++ b/packages/koa/package.json @@ -25,6 +25,7 @@ "test:throw": "DEBUG=jsonql-* ava ./tests/throw.test.js", "test:gen": "DEBUG=jsonql* ava ./tests/contract.test.js", "test:jsonp": "DEBUG=jsonql* ava --verbose ./tests/jsonp.test.js", + "test:chain": "DEBUG=jsonql* ava --verbose ./tests/chain-fn.test.js", "web-console": "DEBUG=jsonql-koa*,jsonql-web-console* node ./tests/helpers/browser.js", "contract": "node ./node_modules/jsonql-contract/cmd.js ./tests/fixtures/resolvers ./tests/fixtures/contracts" }, diff --git a/packages/koa/src/lib/config-check/index.js b/packages/koa/src/lib/config-check/index.js index bf6674dd64fcdf6ede45bcd04229c1928346bf15..9570e25d1910f028455bea211f499edeff3c5823 100644 --- a/packages/koa/src/lib/config-check/index.js +++ b/packages/koa/src/lib/config-check/index.js @@ -5,24 +5,49 @@ const { checkConfig, isString } = require('jsonql-params-validator') const { rsaPemKeys } = require('jsonql-jwt') const { appProps, constProps, jwtProcessKey } = require('./options') -const { getContract, isContractJson, chainFns, getDebug } = require('../index') +const { getContract, isContractJson, chainFns, getDebug, inArray } = require('../index') const debug = getDebug('config-check') /** - * we need an extra step to cache some of the auth related configuration data - * ASYNC AWAIT IS A FUCKING JOKE + * Double check if the client config is correct or not also we could inject some of the properties here * @param {object} config configuration - * @return {object} config with extra property + * @return {object} with additional properties */ -const applyAuthOptions = function(config) { - - debug('call applyAuthOptions') +const validateClientConfig = function(config) { + let ctn = config.clientConfig.length + if (ctn) { + let names = [] + let clients = [] + for (let i = 0; i < ctn; ++i) { + let client = config.clientConfig[i] + if (!client.hostname) { + throw new Error(`Missing hostname in client config ${i}`) + } + if (!client.name) { + client.name = `nodeClient${i}` + } + if (inArray(client.name, names)) { + throw new Error(`[${i}] ${client.name} already existed, can not have duplicated!`) + } + names.push(client.name) + clients.push(client) + } + config.clientConfig = clients; + } + return config; +} +/** + * break out from the applyAuthOptions because it's not suppose to be there + * @param {object} config configuration + * @return {object} with additional properties + */ +const applyGetContract = function(config) { const { contract } = config; if (isContractJson(contract)) { config.contract = contract; - // @ 1.3.4 + // @ 1.3.4 - also generate the public contract if (config.withPublicContract) { getContract(config, true) } @@ -37,8 +62,16 @@ const applyAuthOptions = function(config) { return contract; }) } + return config; +} - +/** + * we need an extra step to cache some of the auth related configuration data + * ASYNC AWAIT IS A FUCKING JOKE + * @param {object} config configuration + * @return {object} config with extra property + */ +const applyAuthOptions = function(config) { if (config.enableAuth && config.useJwt && !isString(config.useJwt)) { const { keysDir, publicKeyFileName, privateKeyFileName } = config; const publicKeyPath = join(keysDir, publicKeyFileName) @@ -60,6 +93,6 @@ const applyAuthOptions = function(config) { * @api public */ module.exports = function configCheck(config) { - const fn = chainFns(checkConfig, applyAuthOptions) + const fn = chainFns(checkConfig, validateClientConfig, applyGetContract, applyAuthOptions) return fn(config, appProps, constProps) } diff --git a/packages/koa/src/lib/config-check/options.js b/packages/koa/src/lib/config-check/options.js index 33f43d33372a009c97fbc0b0d95e489ce48f5c66..d666dda4f066271d3cb07d83029178e2573d1023 100755 --- a/packages/koa/src/lib/config-check/options.js +++ b/packages/koa/src/lib/config-check/options.js @@ -90,7 +90,7 @@ const appProps = { jsType: {[ARGS_KEY]: CJS_TYPE, [TYPE_KEY]: STRING_TYPE, [ENUM_KEY]: ACCEPTED_JS_TYPES}, // undecided properties - nodeClient: {[ARGS_KEY]: false, [TYPE_KEY]: BOOLEAN_TYPE}, // this will require a config item + clientConfig: {[ARGS_KEY]: [], [TYPE_KEY]: ARRAY_TYPE}, // need to develop a new tool to validate and inject this exposeError: {[ARGS_KEY]: false, [TYPE_KEY]: BOOLEAN_TYPE}, // this will allow you to control if you want to throw your error back to your client // Perhaps I should build the same create options style like server-io-core diff --git a/packages/koa/src/lib/resolve-method.js b/packages/koa/src/lib/resolve-method.js index f664b2e1a98289d86cac1badbd17740f068f2822..857511692ae8312d1ef0737fdd4e3f9a2865c5ae 100644 --- a/packages/koa/src/lib/resolve-method.js +++ b/packages/koa/src/lib/resolve-method.js @@ -91,6 +91,6 @@ const resolveMethod = async (ctx, type, opts, contract) => { } return ctxErrorHandler(ctx, errorClassName, e); } -}; +} // export module.exports = resolveMethod; diff --git a/packages/koa/src/lib/utils.js b/packages/koa/src/lib/utils.js index 910b95f5e3702813b3238985f6abc0b8ba34b11d..8b913d5b7246e07049ac45e42d8127b773b6eed7 100755 --- a/packages/koa/src/lib/utils.js +++ b/packages/koa/src/lib/utils.js @@ -37,13 +37,19 @@ const getDebug = (name, cond = true) => ( /** * using lodash to chain two functions * @param {function} mainFn function - * @param {function} moreFns functions (could be but not now) + * @param {array} ...moreFns functions spread + * @return {function} to accept the parameter for the first function */ -const chainFns = (mainFn, moreFns) => ( - (...args) => _( Reflect.apply(mainFn, null, args) ) - .chain() - .thru(moreFns) - .value() +const chainFns = (mainFn, ...moreFns) => ( + (...args) => { + let chain = _( Reflect.apply(mainFn, null, args) ).chain() + let ctn = moreFns.length; + for (let i = 0; i < ctn; ++i) { + chain = chain.thru(moreFns[i]) + } + + return chain.value() + } ) /** diff --git a/packages/koa/src/lib/validate-and-call.js b/packages/koa/src/lib/validate-and-call.js index 660e643bce1f9edaca155607e4d11f0cc642a29a..3e7fc25467fefb6abb7073a2815c54473d375c62 100644 --- a/packages/koa/src/lib/validate-and-call.js +++ b/packages/koa/src/lib/validate-and-call.js @@ -51,7 +51,7 @@ const applyJwtMethod = (type, name, opts, contract) => { * @param {object} opts configuration option to use in the future * @return {object} now return a promise that resolve whatever the resolver is going to return and packed */ -module.exports = function(fn, args, contract, type, name, opts) { +module.exports = function validateAndCall(fn, args, contract, type, name, opts) { const { params } = extractParamsFromContract(contract, type, name); let errors = validateSync(args, params); if (errors.length) { diff --git a/packages/koa/tests/chain-fn.test.js b/packages/koa/tests/chain-fn.test.js new file mode 100644 index 0000000000000000000000000000000000000000..7c8962e07b9d2fa6bdad3b07e526bc4108c53361 --- /dev/null +++ b/packages/koa/tests/chain-fn.test.js @@ -0,0 +1,18 @@ +// testing just one function chainFns +const test = require('ava') +const { chainFns } = require('../src/lib') + + +test('It should able to accept more than one functions after the first one', t => { + + const baseFn = (num) => num * 10; + const add1 = (num) => num + 1; + const add2 = (num) => num + 2; + + const fn = chainFns(baseFn, add1, add2) + + const result = fn(10) + + t.is(103, result) + +}) diff --git a/packages/node-client/src/check-options.js b/packages/node-client/src/check-options.js index fd4e8eb2f5844e31911e9fe43c6da066e17b9b8f..ac6246cfbb7cdacbd969293f82d5314dea93569a 100755 --- a/packages/node-client/src/check-options.js +++ b/packages/node-client/src/check-options.js @@ -38,17 +38,19 @@ const constProps = { useDoc: true, returnAs: 'json' } + const appProps = { - useJwt: createConfig(false, [BOOLEAN_TYPE]), + hostname: constructConfig('', STRING_TYPE), // required the hostname + jsonqlPath: constructConfig(JSONQL_PATH, STRING_TYPE), // The path on the server + + useJwt: createConfig(true, [BOOLEAN_TYPE]), loginHandlerName: createConfig(ISSUER_NAME, [STRING_TYPE]), logoutHandlerName: createConfig(LOGOUT_NAME, [STRING_TYPE]), validatorHandlerName: createConfig(VALIDATOR_NAME, [STRING_TYPE]), enableAuth: createConfig(false, [BOOLEAN_TYPE]), - hostname: constructConfig('', STRING_TYPE), // required the hostname - jsonqlPath: constructConfig(JSONQL_PATH, STRING_TYPE), // The path on the server // useLocalstorage: constructConfig(true, BOOLEAN_TYPE), // should we store the contract into localStorage storageKey: constructConfig(CLIENT_STORAGE_KEY, STRING_TYPE),// the key to use when store into localStorage diff --git a/packages/resolver/README.md b/packages/resolver/README.md new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/packages/resolver/index.js b/packages/resolver/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/packages/resolver/package.json b/packages/resolver/package.json new file mode 100644 index 0000000000000000000000000000000000000000..8dfff8598c0fbd72af019019b62f5b42425a014b --- /dev/null +++ b/packages/resolver/package.json @@ -0,0 +1,48 @@ +{ + "name": "jsonql-resolver", + "version": "0.1.0", + "description": "This is NOT for general use, please do not install it directly. This module is part of the jsonql tools supporting modules.", + "main": "index.js", + "scripts": { + "test": "ava" + }, + "keywords": [ + "jsonql" + ], + "author": "Joel Chu ", + "license": "ISC", + "dependencies": { + "debug": "^4.1.1", + "jsonql-constants": "^1.7.9", + "jsonql-errors": "^1.0.9", + "jsonql-jwt": "^1.2.3", + "jsonql-params-validator": "^1.4.3" + }, + "devDependencies": { + "ava": "^2.2.0" + }, + "ava": { + "files": [ + "tests/**/*.test.js", + "!tests/fixtures/**/*.*" + ], + "sources": [ + "**/*.{js,jsx}", + "!dist/**/*" + ], + "cache": true, + "concurrency": 5, + "failFast": true, + "failWithoutAssertions": false, + "tap": false, + "verbose": true, + "compileEnhancements": false + }, + "engine": { + "node": ">=8" + }, + "repository": { + "type": "git", + "url": "git+ssh://git@gitee.com:to1source/jsonql.git" + } +} diff --git a/packages/resolver/src/resolve-method.js b/packages/resolver/src/resolve-method.js new file mode 100644 index 0000000000000000000000000000000000000000..857511692ae8312d1ef0737fdd4e3f9a2865c5ae --- /dev/null +++ b/packages/resolver/src/resolve-method.js @@ -0,0 +1,96 @@ +// this was in the core-middleware now make this standalone for use in +// two middlewares +const { join } = require('path') +const { + JsonqlResolverNotFoundError, + JsonqlResolverAppError, + JsonqlValidationError, + JsonqlAuthorisationError +} = require('jsonql-errors') +const searchResolvers = require('./search-resolvers') +const validateAndCall = require('./validate-and-call') +const { + getDebug, + printError, + handleOutput, + extractArgsFromPayload, + ctxErrorHandler, + packResult +} = require('./utils') +const { provideUserdata } = require('jsonql-jwt') +const { + DEFAULT_RESOLVER_IMPORT_FILE_NAME, + MODULE_TYPE +} = require('jsonql-constants') +const debug = getDebug('resolve-method') + +/** + * New for ES6 module features + * @param {string} resolverDir resolver directory + * @param {string} type of resolver + * @param {string} resolverName name of resolver + * @return {function} the imported resolver + */ +function importFromModule(resolverDir, type, resolverName) { + debug(resolverDir, type, resolverName) + const resolvers = require( join(resolverDir, DEFAULT_RESOLVER_IMPORT_FILE_NAME) ) + return resolvers[type + resolverName] +} + +/** + * The method call has this signature + * @param {object} ctx Koa context + * @param {string} type of calls + * @param {object} opts configuration + * @param {object} contract to search via the file name info + * @return {mixed} depends on the contract + */ +const resolveMethod = async (ctx, type, opts, contract) => { + const { payload, resolverName, userdata } = ctx.state.jsonql; + debug('resolveMethod', resolverName, payload, type) + // There must be only one method call + const renderHandler = handleOutput(opts) + // first try to catch the resolve error + try { + let fn; + const { sourceType } = contract; + if (sourceType === MODULE_TYPE) { + const { resolverDir } = opts; + fn = importFromModule(resolverDir, type, resolverName) + } else { + fn = require(searchResolvers(resolverName, type, opts, contract)) + } + const args = extractArgsFromPayload(payload, type) + // here we could apply the userdata to the method + const result = await validateAndCall( + provideUserdata(fn, userdata), // always call it + args, + contract, + type, + resolverName, + opts) + // @TODO if we need to check returns in the future + debug('called and now serve up', result) + return renderHandler(ctx, packResult(result)) + } catch (e) { + debug('resolveMethod error', e) + let errorClassName = 'JsonqlError'; + switch (true) { + case (e instanceof JsonqlResolverNotFoundError): + errorClassName = 'JsonqlResolverNotFoundError'; + break; + case (e instanceof JsonqlAuthorisationError): + errorClassName = 'JsonqlAuthorisationError'; + break; + case (e instanceof JsonqlValidationError): + errorClassName = 'JsonqlValidationError'; + break; + case (e instanceof JsonqlResolverAppError): + errorClassName = 'JsonqlResolverAppError'; + break; + } + return ctxErrorHandler(ctx, errorClassName, e); + } +} +// export +module.exports = resolveMethod; diff --git a/packages/resolver/src/search-resolvers.js b/packages/resolver/src/search-resolvers.js new file mode 100644 index 0000000000000000000000000000000000000000..b07f9dc795cc6ab02caab6242f20b2954e68c638 --- /dev/null +++ b/packages/resolver/src/search-resolvers.js @@ -0,0 +1,60 @@ +// search for the resolver location +const fs = require('fs') +const { join } = require('path') +const debug = require('debug')('jsonql-koa:lib:search') + +const { JsonqlResolverNotFoundError } = require('jsonql-errors') +const { getPathToFn } = require('./utils') +const prod = process.env.NODE_ENV === 'production'; + +/** + * Using the contract to find the function to call + * @param {string} type of resolver + * @param {string} name of resolver + * @param {object} contract to search from + * @return {string} file path to function + */ +function findFromContract(type, name, contract) { + if (contract[type] && contract[type][name] && contract[type][name].file) { + if (fs.existsSync(contract[type][name].file)) { + return contract[type][name].file; + } + } + return false; +} + +/** + * search for the file starting with + * 1. Is the path in the contract (or do we have a contract file) + * 2. if not then resolvers/query/name-of-call/index.js (query swap with mutation) + * 3. then resolvers/query/name-of-call.js + * @param {string} name of the resolver + * @param {string} type of the resolver + * @param {object} opts options + * @param {object} contract full version + * @return {string} the path to function + */ +module.exports = function searchResolvers(name, type, opts, contract) { + try { + const json = typeof contract === 'string' ? JSON.parse(contract) : contract; + const search = findFromContract(type, name, json) + if (search !== false) { + return search; + } + // search by running + const filePath = getPathToFn(name, type, opts) + if (filePath) { + return filePath; + } + const debugMsg = `${name} not found!`; + debug(debugMsg); + const msg = prod ? 'NOT FOUND!' : debugMsg; + throw new JsonqlResolverNotFoundError(msg) + } catch(e) { + if (e instanceof JsonqlResolverNotFoundError) { + throw new JsonqlResolverNotFoundError(e) + } else { + throw new JsonqError(e) + } + } +} diff --git a/packages/resolver/src/utils.js b/packages/resolver/src/utils.js new file mode 100644 index 0000000000000000000000000000000000000000..6b83ca9840f6b3feb0c949d20ff6c1595bf6f54e --- /dev/null +++ b/packages/resolver/src/utils.js @@ -0,0 +1,392 @@ +// util methods +const { join } = require('path') +const fs = require('fs') +const { inspect } = require('util') +const { isObject } = require('jsonql-params-validator') +const jsonqlErrors = require('jsonql-errors') +const { + JsonqlResolverNotFoundError, + getErrorByStatus, + JsonqlError +} = jsonqlErrors; +const { + BASE64_FORMAT, + CONTENT_TYPE, + QUERY_NAME, + MUTATION_NAME, + API_REQUEST_METHODS, + PAYLOAD_PARAM_NAME, + CONDITION_PARAM_NAME, + RESOLVER_PARAM_NAME , + QUERY_ARG_NAME +} = require('jsonql-constants') + +// export a create debug method +const debug = require('debug') +/** + * @param {string} name for id + * @param {boolean} cond i.e. NODE_ENV==='development' + * @return {void} nothing + */ +const getDebug = (name, cond = true) => ( + cond ? debug('jsonql-resolver').extend(name) : () => {} +) + +/** + * using lodash to chain two functions + * @param {function} mainFn function + * @param {array} ...moreFns functions spread + * @return {function} to accept the parameter for the first function + */ +const chainFns = (mainFn, ...moreFns) => ( + (...args) => { + let chain = _( Reflect.apply(mainFn, null, args) ).chain() + let ctn = moreFns.length; + for (let i = 0; i < ctn; ++i) { + chain = chain.thru(moreFns[i]) + } + + return chain.value() + } +) + +/** + * DIY in Array + * @param {array} arr to check from + * @param {*} value to check against + * @return {boolean} true on found + */ +const inArray = (arr, value) => !!arr.filter(a => a === value).length; + +/** + * From underscore.string library + * @BUG there is a bug here with the non-standard name start with _ + * @param {string} str string + * @return {string} dasherize string + */ +const dasherize = str => ( + str + .trim() + .replace(/([A-Z])/g, '-$1') + .replace(/[-_\s]+/g, '-') + .toLowerCase() +) + +/** + * Get document (string) byte length for use in header + * @param {string} doc to calculate + * @return {number} length + */ +const getDocLen = doc => Buffer.byteLength(doc, 'utf8') + +/** + * The koa ctx object is not returning what it said on the documentation + * So I need to write a custom parser to check the request content-type + * @param {object} req the ctx.request + * @param {string} type (optional) to check against + * @return {mixed} Array or Boolean + */ +const headerParser = (req, type) => { + try { + const headers = req.headers.accept.split(',') + if (type) { + return headers.filter(h => h === type) + } + return headers; + } catch (e) { + // When Chrome dev tool activate the headers become empty + return []; + } +} + +/** + * wrapper of above method to make it easier to use + * @param {object} req ctx.request + * @param {string} type of header + * @return {boolean} + */ +const isHeaderPresent = (req, type) => { + const headers = headerParser(req, type) + return !!headers.length; +} + +/** + * @TODO need to be more flexible + * @param {object} ctx koa + * @param {object} opts configuration + * @return {boolean} if it match + */ +const isJsonqlPath = (ctx, opts) => ctx.path === opts.jsonqlPath; + +/** + * combine two check in one and save time + * @param {object} ctx koa + * @param {object} opts config + * @return {boolean} check result + */ +const isJsonqlRequest = (ctx, opts) => { + const header = isHeaderPresent(ctx.request, opts.contentType) + if (header) { + return isJsonqlPath(ctx, opts) + } + return false; +} + +/** + * check if this is point to the jsonql console + * @param {object} ctx koa context + * @param {object} opts config + * @return {boolean} + */ +const isJsonqlConsoleUrl = (ctx, opts) => ( + ctx.method === 'GET' && isJsonqlPath(ctx, opts) +) + +/** + * getting what is calling after the above check + * @param {string} method of call + * @return {mixed} false on failed + */ +const getCallMethod = method => { + const [ POST, PUT ] = API_REQUEST_METHODS; + switch (true) { + case method === POST: + return QUERY_NAME; + case method === PUT: + return MUTATION_NAME; + default: + return false; + } +}; + +/** + * @param {string} name + * @param {string} type + * @param {object} opts + * @return {function} + */ +const getPathToFn = function(name, type, opts) { + const dir = opts.resolverDir; + const fileName = dasherize(name); + let paths = []; + if (opts.contract && opts.contract[type] && opts.contract[type].path) { + paths.push(opts.contract[type].path) + } + paths.push( join(dir, type, fileName, 'index.js') ) + paths.push( join(dir, type, fileName + '.js') ) + + const ctn = paths.length; + for (let i=0; i { + return JSON.stringify({ data: result }) +} + +/** + * Handle the output + * @param {object} opts configuration + * @return {function} with ctx and body as params + */ +const handleOutput = function(opts) { + return function(ctx, body) { + ctx.size = getDocLen(body) + ctx.type = opts.contentType; + ctx.status = 200; + ctx.body = body; + } +} + +/** + * handle HTML output for the web console + * @param {object} ctx koa context + * @param {string} body output content + * @return {void} + */ +const handleHtmlOutput = function(ctx, body) { + ctx.size = getDocLen(body) + ctx.type = 'text/html'; + ctx.status = 200; + ctx.body = body + ''; // just make sure its string output +} + +/** + * Port this from the CIS App + * @param {string} key of object + * @param {mixed} value of object + * @return {string} of things we after + */ +const replaceErrors = function(key, value) { + if (value instanceof Error) { + var error = {}; + Object.getOwnPropertyNames(value).forEach(function (key) { + error[key] = value[key]; + }) + return error; + } + return value; +} + +/** + * create readible string version of the error object + * @param {object} error obj + * @return {string} printable result + */ +const printError = function(error) { + //return 'MASKED'; //error.toString(); + // return JSON.stringify(error, replaceErrors); + return inspect(error, false, null, true) +} + +/** + * wrapper method - the output is trying to match up the structure of the Error sub class + * @param {mixed} detail of fn error + * @param {string} [className=JsonqlError] the errorName + * @param {number} [statusCode=500] the original error code + * @return {string} stringify error + */ +const packError = function(detail, className = 'JsonqlError', statusCode = 500, message = '') { + return JSON.stringify({ + error: { detail, className, statusCode, message } + }) +} + +/** + * use the ctx to generate error output + * V1.1.0 we render this as a normal output with status 200 + * then on the client side will check against the result object for error + * @param {object} ctx context + * @param {number} code 404 / 500 etc + * @param {object} e actual error + * @param {string} message if there is one + * @param {string} name custom error class name + */ +const ctxErrorHandler = function(ctx, code, e, message = '') { + const render = handleOutput({contentType: CONTENT_TYPE}) + let name; + if (typeof code === 'string') { + name = code; + code = jsonqlErrors[name] ? jsonqlErrors[name].statusCode : -1; + } else { + name = jsonqlErrors.getErrorByStatus(code) + } + // preserve the message + if (!message && e && e.message) { + message = e.message; + } + return render(ctx, packError(e, name, code, message)) +} + +/** + * Just a wrapper to be clearer what error is it + * @param {object} ctx koa + * @param {object} e error + * @return {undefined} nothing + */ +const forbiddenHandler = (ctx, e) => ( + ctxErrorHandler(ctx, 403, e, 'JsonqlAuthorisationError') +) + +/** + * Like what the name said + * @param {object} contract the contract json + * @param {string} type query|mutation + * @param {string} name of the function + * @return {object} the params part of the contract + */ +const extractParamsFromContract = function(contract, type, name) { + try { + const result = contract[type][name]; + debug('extractParamsFromContract', result) + if (!result) { + debug(name, type, contract) + throw new JsonqlResolverNotFoundError(name, type) + } + return result; + } catch(e) { + throw new JsonqlResolverNotFoundError(name, e) + } +} + +/** + * Check several parameter that there is something in the param + * @param {*} param input + * @return {boolean} + */ +const isNotEmpty = param => ( + param !== undefined && param !== false && param !== null && (typeof param === 'string' && param.trim() !== '') +) + +/** + * Check if a json file is a contract or not + * @param {*} contract input + * @return {*} false on failed + */ +const isContractJson = (contract) => ( + isObject(contract) && (contract[QUERY_NAME] || contract[MUTATION_NAME]) ? contract : false +) + +/** + * Extract the args from the payload + * @param {object} payload to work with + * @param {string} type of call + * @return {array} args + */ +const extractArgsFromPayload = function(payload, type) { + switch (type) { + case QUERY_NAME: + return payload[QUERY_ARG_NAME]; + case MUTATION_NAME: + return [ + payload[PAYLOAD_PARAM_NAME], + payload[CONDITION_PARAM_NAME] + ]; + default: + throw new JsonqlError(`Unknown ${type} to extract argument from!`); + } +} + +// export +module.exports = { + + chainFns, + + inArray, + getDebug, + + dasherize, + headerParser, + getPathToFn, + getDocLen, + packResult, + packError, + printError, + ctxErrorHandler, + forbiddenHandler, + + isJsonqlPath, + isJsonqlRequest, + isJsonqlConsoleUrl, + + getCallMethod, + isHeaderPresent, + extractParamsFromContract, + + isObject, + isNotEmpty, + isContractJson, + + handleOutput, + handleHtmlOutput, + extractArgsFromPayload +}; diff --git a/packages/resolver/src/validate-and-call.js b/packages/resolver/src/validate-and-call.js new file mode 100644 index 0000000000000000000000000000000000000000..3e7fc25467fefb6abb7073a2815c54473d375c62 --- /dev/null +++ b/packages/resolver/src/validate-and-call.js @@ -0,0 +1,62 @@ +// validation wrapper +const { AUTH_TYPE, HSA_ALGO, RSA_ALGO } = require('jsonql-constants') +const { validateSync, isString } = require('jsonql-params-validator') +const { JsonqlValidationError } = require('jsonql-errors') +const { loginResultToJwt } = require('jsonql-jwt') + +const { extractParamsFromContract, getDebug } = require('./utils') + +const debug = getDebug('validate-and-call') +// for caching +var resultMethod; + +/** + * get the encode method also cache it + * @param {object} opts configuration + * @return {function} encode method + */ +const getEncodeJwtMethod = opts => { + if (resultMethod && typeof resultMethod === 'function') { + return resultMethod; + } + let key = isString(opts.useJwt) ? opts.useJwt : opts.privateKey; + let alg = isString(opts.useJwt) ? HSA_ALGO : RSA_ALGO; + // add jwtTokenOption for the extra configuration for generate token + return loginResultToJwt(key, opts.jwtTokenOption, alg) +} + +/** + * This will hijack some of the function for the auth type + * @param {string} type of resolver we only after the auth type + * @param {string} name of the resolver function + * @param {object} opts configuration + * @param {object} contract the contract.json + */ +const applyJwtMethod = (type, name, opts, contract) => { + return result => { + if (type === AUTH_TYPE && name === opts.loginHandlerName && opts.enableAuth && opts.useJwt) { + return getEncodeJwtMethod(opts)(result) + } + return result; + } +} + +/** + * Main method to replace the fn.apply call inside the core method + * @param {function} fn the resolver to get execute + * @param {array} args the argument list + * @param {object} contract the full contract.json + * @param {string} type query | mutation + * @param {string} name of the function + * @param {object} opts configuration option to use in the future + * @return {object} now return a promise that resolve whatever the resolver is going to return and packed + */ +module.exports = function validateAndCall(fn, args, contract, type, name, opts) { + const { params } = extractParamsFromContract(contract, type, name); + let errors = validateSync(args, params); + if (errors.length) { + throw new JsonqlValidationError(name, errors); + } + return Promise.resolve(fn.apply(null, args)) + .then(applyJwtMethod(type, name, opts, contract)) +} diff --git a/packages/resolver/tests/base.test.js b/packages/resolver/tests/base.test.js new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391