From 092e72c12e1d4d75a1f2800b1b86aee0f92ecd5e Mon Sep 17 00:00:00 2001 From: Joelchu Date: Wed, 14 Aug 2019 11:20:22 +0800 Subject: [PATCH 1/9] Update the chainFns and add test to verify if its working --- packages/koa/README.md | 9 +++++---- packages/koa/package.json | 1 + packages/koa/src/lib/config-check/index.js | 2 +- packages/koa/src/lib/config-check/options.js | 2 +- packages/koa/src/lib/utils.js | 18 ++++++++++++------ packages/koa/tests/chain-fn.test.js | 18 ++++++++++++++++++ 6 files changed, 38 insertions(+), 12 deletions(-) create mode 100644 packages/koa/tests/chain-fn.test.js diff --git a/packages/koa/README.md b/packages/koa/README.md index 31c2abcf..9ae1048b 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/package.json b/packages/koa/package.json index 5b1a7d28..13ca8a20 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 bf6674dd..f0954bd2 100644 --- a/packages/koa/src/lib/config-check/index.js +++ b/packages/koa/src/lib/config-check/index.js @@ -22,7 +22,7 @@ const applyAuthOptions = 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) } diff --git a/packages/koa/src/lib/config-check/options.js b/packages/koa/src/lib/config-check/options.js index 33f43d33..d666dda4 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/utils.js b/packages/koa/src/lib/utils.js index 910b95f5..8b913d5b 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/tests/chain-fn.test.js b/packages/koa/tests/chain-fn.test.js new file mode 100644 index 00000000..7c8962e0 --- /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) + +}) -- Gitee From fe5bcb3b21652b529fd7b0cfa17d1074c1f8df09 Mon Sep 17 00:00:00 2001 From: Joelchu Date: Wed, 14 Aug 2019 12:03:53 +0800 Subject: [PATCH 2/9] change the startup config check options --- .../src/lib/options/base-options.js | 2 +- packages/koa/src/lib/config-check/index.js | 51 +++++++++++++++---- packages/node-client/src/check-options.js | 8 +-- 3 files changed, 48 insertions(+), 13 deletions(-) diff --git a/packages/http-client/src/lib/options/base-options.js b/packages/http-client/src/lib/options/base-options.js index 129ec174..15d471dd 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/koa/src/lib/config-check/index.js b/packages/koa/src/lib/config-check/index.js index f0954bd2..9570e25d 100644 --- a/packages/koa/src/lib/config-check/index.js +++ b/packages/koa/src/lib/config-check/index.js @@ -5,20 +5,45 @@ 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; @@ -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/node-client/src/check-options.js b/packages/node-client/src/check-options.js index fd4e8eb2..ac6246cf 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 -- Gitee From 620f7dc9f4eb26458fe3ae59524b90937e3f6d94 Mon Sep 17 00:00:00 2001 From: Joelchu Date: Wed, 14 Aug 2019 12:11:27 +0800 Subject: [PATCH 3/9] update the client create method with async --- packages/koa/client.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/koa/client.js b/packages/koa/client.js index defbabe3..25325ca4 100755 --- a/packages/koa/client.js +++ b/packages/koa/client.js @@ -5,7 +5,7 @@ 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) { +module.exports = async function(name, config) { if (!name) { throw new Error('Name is required!'); } @@ -13,8 +13,6 @@ module.exports = function(name, config) { throw new Error('contractDir is required'); } config.contractDir = join(config.contractDir, name); - if (config) { - return jsonqlNodeClient(config); - } - throw new Error(`${name} client not found!`); + + return await jsonqlNodeClient(config); } -- Gitee From 0f45c0e4ccc0853daa249b056d9b3a5da0d79aab Mon Sep 17 00:00:00 2001 From: Joelchu Date: Wed, 14 Aug 2019 12:14:48 +0800 Subject: [PATCH 4/9] update the styling --- packages/koa/client.js | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/packages/koa/client.js b/packages/koa/client.js index 25325ca4..93b4925e 100755 --- a/packages/koa/client.js +++ b/packages/koa/client.js @@ -1,18 +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 = async 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); - - return await jsonqlNodeClient(config); + config.contractDir = join(config.contractDir, name) + + return jsonqlNodeClient(config) } -- Gitee From a48a4999ea45033b40eaa83d4d12c1efa9f1ef63 Mon Sep 17 00:00:00 2001 From: Joelchu Date: Wed, 14 Aug 2019 12:30:18 +0800 Subject: [PATCH 5/9] init the jsonql-resolver project --- packages/resolver/README.md | 0 packages/resolver/index.js | 0 packages/resolver/package.json | 14 ++++++++++++++ packages/resolver/src/index.js | 0 packages/resolver/tests/base.test.js | 0 5 files changed, 14 insertions(+) create mode 100644 packages/resolver/README.md create mode 100644 packages/resolver/index.js create mode 100644 packages/resolver/package.json create mode 100644 packages/resolver/src/index.js create mode 100644 packages/resolver/tests/base.test.js diff --git a/packages/resolver/README.md b/packages/resolver/README.md new file mode 100644 index 00000000..e69de29b diff --git a/packages/resolver/index.js b/packages/resolver/index.js new file mode 100644 index 00000000..e69de29b diff --git a/packages/resolver/package.json b/packages/resolver/package.json new file mode 100644 index 00000000..490bb4df --- /dev/null +++ b/packages/resolver/package.json @@ -0,0 +1,14 @@ +{ + "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" +} diff --git a/packages/resolver/src/index.js b/packages/resolver/src/index.js new file mode 100644 index 00000000..e69de29b diff --git a/packages/resolver/tests/base.test.js b/packages/resolver/tests/base.test.js new file mode 100644 index 00000000..e69de29b -- Gitee From 84f191dc28e361d7c3894da49135a8ffb3a85a9e Mon Sep 17 00:00:00 2001 From: Joelchu Date: Wed, 14 Aug 2019 12:37:21 +0800 Subject: [PATCH 6/9] ported the resolvers method from jsonql-koa --- packages/koa/src/lib/resolve-method.js | 2 +- packages/koa/src/lib/validate-and-call.js | 2 +- packages/resolver/package.json | 12 +- packages/resolver/src/index.js | 0 packages/resolver/src/resolve-method.js | 96 +++++ packages/resolver/src/search-resolvers.js | 60 ++++ packages/resolver/src/utils.js | 393 +++++++++++++++++++++ packages/resolver/src/validate-and-call.js | 62 ++++ 8 files changed, 624 insertions(+), 3 deletions(-) delete mode 100644 packages/resolver/src/index.js create mode 100644 packages/resolver/src/resolve-method.js create mode 100644 packages/resolver/src/search-resolvers.js create mode 100644 packages/resolver/src/utils.js create mode 100644 packages/resolver/src/validate-and-call.js diff --git a/packages/koa/src/lib/resolve-method.js b/packages/koa/src/lib/resolve-method.js index f664b2e1..85751169 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/validate-and-call.js b/packages/koa/src/lib/validate-and-call.js index 660e643b..3e7fc254 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/resolver/package.json b/packages/resolver/package.json index 490bb4df..eaeea689 100644 --- a/packages/resolver/package.json +++ b/packages/resolver/package.json @@ -10,5 +10,15 @@ "jsonql" ], "author": "Joel Chu ", - "license": "ISC" + "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" + } } diff --git a/packages/resolver/src/index.js b/packages/resolver/src/index.js deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/resolver/src/resolve-method.js b/packages/resolver/src/resolve-method.js new file mode 100644 index 00000000..85751169 --- /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 00000000..b07f9dc7 --- /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 00000000..8b913d5b --- /dev/null +++ b/packages/resolver/src/utils.js @@ -0,0 +1,393 @@ +// util methods +const _ = require('lodash'); +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') +const { trim } = _; + +// 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-koa').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 => ( + trim(str) + .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') ) + // paths.push( join(dir, 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 = function(param) { + return param !== undefined && param !== false && param !== null && trim(param) !== ''; +} + +/** + * 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 00000000..3e7fc254 --- /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)) +} -- Gitee From 6bc5983a0fc5cb44b5df77d5a6398d8c1f05bd05 Mon Sep 17 00:00:00 2001 From: Joelchu Date: Wed, 14 Aug 2019 16:40:19 +0800 Subject: [PATCH 7/9] copy over the ava test setting --- packages/jwt/cmd.js | 52 +++++++++++++++++----------------- packages/jwt/package.json | 6 ++-- packages/resolver/package.json | 24 ++++++++++++++++ packages/resolver/src/utils.js | 27 +++++++++--------- 4 files changed, 66 insertions(+), 43 deletions(-) diff --git a/packages/jwt/cmd.js b/packages/jwt/cmd.js index fce80853..785d2885 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 10874469..6cf863d5 100644 --- a/packages/jwt/package.json +++ b/packages/jwt/package.json @@ -53,14 +53,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/resolver/package.json b/packages/resolver/package.json index eaeea689..8dfff859 100644 --- a/packages/resolver/package.json +++ b/packages/resolver/package.json @@ -20,5 +20,29 @@ }, "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/utils.js b/packages/resolver/src/utils.js index 8b913d5b..6b83ca98 100644 --- a/packages/resolver/src/utils.js +++ b/packages/resolver/src/utils.js @@ -1,10 +1,9 @@ // util methods -const _ = require('lodash'); -const { join } = require('path'); -const fs = require('fs'); -const { inspect } = require('util'); -const { isObject } = require('jsonql-params-validator'); -const jsonqlErrors = require('jsonql-errors'); +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, @@ -21,7 +20,6 @@ const { RESOLVER_PARAM_NAME , QUERY_ARG_NAME } = require('jsonql-constants') -const { trim } = _; // export a create debug method const debug = require('debug') @@ -31,7 +29,7 @@ const debug = require('debug') * @return {void} nothing */ const getDebug = (name, cond = true) => ( - cond ? debug('jsonql-koa').extend(name) : () => {} + cond ? debug('jsonql-resolver').extend(name) : () => {} ) /** @@ -67,7 +65,8 @@ const inArray = (arr, value) => !!arr.filter(a => a === value).length; * @return {string} dasherize string */ const dasherize = str => ( - trim(str) + str + .trim() .replace(/([A-Z])/g, '-$1') .replace(/[-_\s]+/g, '-') .toLowerCase() @@ -171,11 +170,11 @@ const getPathToFn = function(name, type, opts) { const fileName = dasherize(name); let paths = []; if (opts.contract && opts.contract[type] && opts.contract[type].path) { - paths.push(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') ) - // paths.push( join(dir, fileName + '.js') ); + const ctn = paths.length; for (let i=0; i ( + param !== undefined && param !== false && param !== null && (typeof param === 'string' && param.trim() !== '') +) /** * Check if a json file is a contract or not -- Gitee From 2225ad432dfdd0180d85bc79d8bb4689b786e372 Mon Sep 17 00:00:00 2001 From: Joelchu Date: Wed, 14 Aug 2019 16:52:32 +0800 Subject: [PATCH 8/9] tests passed and ready to publish jsonql-jwt to v1.2.4 --- packages/jwt/README.md | 7 +++++-- packages/jwt/package.json | 2 +- packages/jwt/src/server/socketio/node-clients.js | 7 ++++++- packages/jwt/src/server/ws/ws-client.js | 9 ++++++++- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/jwt/README.md b/packages/jwt/README.md index 3d79444b..90feea87 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/package.json b/packages/jwt/package.json index 6cf863d5..5d7e1d9b 100644 --- a/packages/jwt/package.json +++ b/packages/jwt/package.json @@ -1,6 +1,6 @@ { "name": "jsonql-jwt", - "version": "1.2.3", + "version": "1.2.4", "description": "jwt authentication and helpers library for jsonql", "main": "main.js", "module": "index.js", diff --git a/packages/jwt/src/server/socketio/node-clients.js b/packages/jwt/src/server/socketio/node-clients.js index 03ea7002..b18b9ded 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 aebc1768..3e845342 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 -- Gitee From 234593ba2909613dac8efb50ce25b9f9b0e4302e Mon Sep 17 00:00:00 2001 From: Joelchu Date: Wed, 14 Aug 2019 16:53:48 +0800 Subject: [PATCH 9/9] update package.json --- packages/jwt/package.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/jwt/package.json b/packages/jwt/package.json index 5d7e1d9b..38a7a28e 100644 --- a/packages/jwt/package.json +++ b/packages/jwt/package.json @@ -1,7 +1,7 @@ { "name": "jsonql-jwt", "version": "1.2.4", - "description": "jwt authentication and helpers library for jsonql", + "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", -- Gitee