diff --git a/README.md b/README.md index c97eabda228c57d8766737c7c35bbafb00403b60..31ef213f12c875616d193c0d35e29abbe5aca16b 100755 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -![](public/jsonql-logo.png) +![](docs/assets/jsonql-logo.png) -> What is JSON:QL? JSON Query Load :) +> What is JSON:QL? JSON Query Load :) **IMPORTANT NOTE: This repo is currently under heavy development and many of the existing module will get transfer to our new `@jsonql` npm namespace -Please check out website http://jsonql.org for update to date information** +Please check out website [jsonql.org](http://jsonql.js.org) for update to date information** # jsonql tools @@ -70,7 +70,7 @@ written test accordingly before PR. ## Documentation (coming soon) -Full documentation at [json:ql](c) +Full documentation at [json:ql](https://jsonql.js.org) --- diff --git a/demo/.gitignore b/demo/.gitignore deleted file mode 100644 index 3c3629e647f5ddf82548912e337bea9826b434af..0000000000000000000000000000000000000000 --- a/demo/.gitignore +++ /dev/null @@ -1 +0,0 @@ -node_modules diff --git a/demo/README.md b/demo/README.md deleted file mode 100644 index 5051461fb0eec3a92cebdc97d61613f9f19c8fef..0000000000000000000000000000000000000000 --- a/demo/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# json:ql demo - -A bunch of demos to get you started diff --git a/demo/basic/.gitignore b/demo/basic/.gitignore deleted file mode 100644 index 3c3629e647f5ddf82548912e337bea9826b434af..0000000000000000000000000000000000000000 --- a/demo/basic/.gitignore +++ /dev/null @@ -1 +0,0 @@ -node_modules diff --git a/demo/basic/README.md b/demo/basic/README.md deleted file mode 100644 index 8957da32429dc5620c0fffbaf0f9db928f168c7c..0000000000000000000000000000000000000000 --- a/demo/basic/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# json:ql basic demo with server-io-core - -This is a very basic demo that use the server-io-core to setup diff --git a/demo/basic/contracts/contract.json b/demo/basic/contracts/contract.json deleted file mode 100644 index a8dbd93368de9ffd59f0ec8b6456f8a7177ee1fb..0000000000000000000000000000000000000000 --- a/demo/basic/contracts/contract.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "query": {}, - "mutation": {}, - "auth": {}, - "timestamp": 1556372646032 -} diff --git a/demo/basic/index.js b/demo/basic/index.js deleted file mode 100644 index 209c1ee09ff5feaceaf19e1b8acab4361aded2a0..0000000000000000000000000000000000000000 --- a/demo/basic/index.js +++ /dev/null @@ -1,15 +0,0 @@ -// main - -const server = require('server-io-core'); -const jsonqlKoa = require('../../koa'); -const { join } = require('path'); - -server({ - port: 8880, - webroot: [join(__dirname, 'web')], - middlewares: [ - jsonqlKoa({ - - }) - ] -}); diff --git a/demo/basic/package.json b/demo/basic/package.json deleted file mode 100644 index 818770ff670ba54b976fce0f4fe382b3d9822836..0000000000000000000000000000000000000000 --- a/demo/basic/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "jsonql-basic-demo", - "version": "1.0.0", - "description": "jsonql basic demo with server-io-core", - "main": "index.js", - "scripts": { - "test": "nothing", - "start": "DEBUG=jsonql-koa:* node ./index.js" - }, - "keywords": [ - "demo" - ], - "author": "Joel Chu ", - "license": "MIT", - "dependencies": { - "server-io-core": "^1.0.0-beta.6" - } -} diff --git a/demo/index.js b/demo/index.js deleted file mode 100644 index e6ec26799e6a8290275d8e0a87a31718dc6136a7..0000000000000000000000000000000000000000 --- a/demo/index.js +++ /dev/null @@ -1,23 +0,0 @@ -const { jsonqlKoa } = require('./src/imports'); -const serverIoCore = require('server-io-core'); -const { resolve, join } = require('path'); - -const pkgDir = resolve(join(__dirname, '..', 'packages')); - - -const server = serverIoCore({ - port: 3388, - webroot: [ - join(__dirname, 'html'), - join(__dirname, '..', 'packages', 'client', 'dist') - ], - open: true, - debugger: true, - reload: false, - middlewares: [ - jsonqlKoa({ - resolverDir: join(__dirname, 'resolvers'), - contractDir: join(__dirname, 'contracts') - }) - ] -}); diff --git a/demo/package.json b/demo/package.json deleted file mode 100644 index ce1177d608048a3df6a2a7ff7b801b50bfced07b..0000000000000000000000000000000000000000 --- a/demo/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "jsonql-demo", - "version": "1.0.0", - "description": "Put this in the top level for testing purpose and use all the local packages instead of npm version", - "main": "index.js", - "scripts": { - "test": "ava", - "start": "DEBUG=jsonql* node ./index.js" - }, - "keywords": [ - "jsonql" - ], - "author": "Joel Chu ", - "license": "ISC", - "dependencies": { - "server-io-core": "^1.0.2" - }, - "devDependencies": { - "debug": "^4.1.1" - } -} diff --git a/demo/src/imports.js b/demo/src/imports.js deleted file mode 100644 index a80c4da3114048af851a058e87cd929a0e72dc52..0000000000000000000000000000000000000000 --- a/demo/src/imports.js +++ /dev/null @@ -1,10 +0,0 @@ -// import all the module locally - -const jsonqlKoa = require('../../packages/koa/index'); -const debug = require('debug'); -const getDebug = name => debug('jsonql-demo').extend(name); - -module.exports = { - jsonqlKoa, - getDebug -}; diff --git a/docs/.nojekyll b/docs/.nojekyll deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/docs/README.md b/docs/README.md index c97eabda228c57d8766737c7c35bbafb00403b60..3950d89502b82a539f921419902698f6f5b9b1d6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,79 +1,3 @@ -![](public/jsonql-logo.png) +The documentation has moved to https://github.com/joel-chu/jsonql-org -> What is JSON:QL? JSON Query Load :) - -**IMPORTANT NOTE: This repo is currently under heavy development and many of the existing module will get transfer to our new `@jsonql` npm namespace -Please check out website http://jsonql.org for update to date information** - - -# jsonql tools - -This is a collection of JSONql tools starting with jsonql-client and jsonql-koa - -## What is jsonql - -This is not a replacement of REST or GraphQL, instead we are focus on the `last one mile`. - -The communication protocol is based on [JSON API](https://jsonapi.org/). - -It's intend for communication between different devices or services. - -And the main goal is to write the resolver (on server side) once, and the -client doesn't need any code written and it should work. - -It's like this - -``` - client <--> contract <--> server -``` - -## Query, Mutation, Auth - -There are only three types of calls. `query`, `mutation` and `auth` - -For more information please check one of our following available modules - - -- [jsonql-koa](https://www.npmjs.com/package/jsonql-koa) json:ql Koa middleware
[![NPM](https://nodei.co/npm/jsonql-koa.png?compact=true)](https://npmjs.org/package/jsonql-koa) -- [jsonql-client](https://www.npmjs.com/package/jsonql-client) json:ql Javascript client using Superagent
[![NPM](https://nodei.co/npm/jsonql-client.png?compact=true)](https://npmjs.org/package/jsonql-client) -- [jsonql-node-client](https://www.npmjs.com/package/jsonql-node-client) json:ql Node.js client
[![NPM](https://nodei.co/npm/jsonql-node-client.png?compact=true)](https://npmjs.org/package/jsonql-node-client) -- [jsonql-contract](https://www.npmjs.com/package/jsonql-contract) json:ql command line util that creates the contract.json
[![NPM](https://nodei.co/npm/jsonql-contract.png?compact=true)](https://npmjs.org/package/jsonql-contract) -- [jsonql-constants](https://www.npmjs.com/package/jsonql-constants) json:ql constants for development purpose
[![NPM](https://nodei.co/npm/jsonql-constants.png?compact=true)](https://npmjs.org/package/jsonql-constants) -- [jsonql-ws-server](https://www.npmjs.com/package/jsonql-ws-server) json:ql Web Socket Server support `ws` and `socket.io`
[![NPM](https://nodei.co/npm/jsonql-ws-server.png?compact=true)](https://npmjs.org/package/jsonql-ws-server) -- [jsonql-params-validator](https://www.npmjs.com/package/jsonql-params-validator) json:ql resolvers validate interface for both client and server
[![NPM](https://nodei.co/npm/jsonql-params-validator.png?compact=true)](https://npmjs.org/package/jsonql-params-validator) -- [jsonql-errors](https://www.npmjs.com/package/jsonql-errors) json:ql errors classes for use in all the jsonql js projects
[![NPM](https://nodei.co/npm/jsonql-errors.png?compact=true)](https://npmjs.org/package/jsonql-errors) - -There are several more in the planning stage - -- json:ql ws-client the websocket client for Browser -- json:ql rx-client - this is the new client that replace the fetch / ws client using stream -- ~~json:ql ts-koa - this is the Typescript port for the server side~~ This will replace by json:ql-deno -- json:ql ts-rx-client - the rx client using Typescript -- json:ql Go -- json:ql Dart -- json:ql ASP.NET - -If you are interested in creating your implementation please do not hesitate to contact us. - -## Work on this project - -Please first fork this repo. Then setup your git as follow: - -```sh -$ git remote add upstream git@gitee.com:to1source/jsonql.git -$ git remote -v -$ git fetch upstream -``` - -Once you are done coding, please PR your code for checking. Please make sure you have -written test accordingly before PR. - -## Documentation (coming soon) - -Full documentation at [json:ql](c) - ---- - -MIT - -Co-Develop by [to1source LTD China](https://to1source.com) and [NEWBRAN LTD UK](https://newbran.ch) +And its now published in [jsonql.js.org](https://jsonql.js.org) diff --git a/public/jsonql-logo.png b/docs/assets/jsonql-logo.png similarity index 100% rename from public/jsonql-logo.png rename to docs/assets/jsonql-logo.png diff --git a/docs/index.html b/docs/index.html deleted file mode 100644 index f48a9142598404fa0d3c9d4e97a8ffc2b70bf3d5..0000000000000000000000000000000000000000 --- a/docs/index.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - Document - - - - - - -
- - - - diff --git a/packages/@jsonql/cli/package.json b/packages/@jsonql/cli/package.json index 1247faa55274799cd910a1703d235a18499193ee..f0f1f26170745900ac52ab3cc71bb0cd892dbdac 100644 --- a/packages/@jsonql/cli/package.json +++ b/packages/@jsonql/cli/package.json @@ -12,6 +12,7 @@ "interactive", "shell" ], + "homepage": "jsonql.org", "author": "Joel Chu ", "license": "ISC" } diff --git a/packages/@jsonql/client/package.json b/packages/@jsonql/client/package.json index 1fcf1cd6a6fc9d461f3859cab9175c9f475e9c2e..5181901a1a68a95b58059d8aab014a7b2bcbe8b2 100644 --- a/packages/@jsonql/client/package.json +++ b/packages/@jsonql/client/package.json @@ -37,6 +37,7 @@ "index.js" ], "author": "Joel Chu ", + "homepage": "jsonql.org", "repository": { "type": "git", "url": "git+ssh://git@gitee.com:to1source/jsonql.git" diff --git a/packages/@jsonql/event/package.json b/packages/@jsonql/event/package.json index 3dc114ea337d29ec283cf15b15fa3861b085d074..ce410afc5a8280f7067fe691929392c8b9f0fcb8 100644 --- a/packages/@jsonql/event/package.json +++ b/packages/@jsonql/event/package.json @@ -33,7 +33,7 @@ "bugs": { "url": "https://gitee.com/to1source/jsonql/issues" }, - "homepage": "https://jsonql.js.org", + "homepage": "jsonql.org", "license": "ISC", "devDependencies": { "ava": "^2.2.0", diff --git a/packages/@jsonql/express/package.json b/packages/@jsonql/express/package.json index 6c5947be8f914cf41390ad8cb543f3d087e261a8..954ab7aeade01f90f7ffd3b19b5b33d2d1170ecc 100644 --- a/packages/@jsonql/express/package.json +++ b/packages/@jsonql/express/package.json @@ -11,6 +11,7 @@ "express", "node" ], + "homepage": "jsonql.org", "author": "Joel Chu ", "license": "ISC" } diff --git a/packages/@jsonql/koa/tests/fixtures/contract/contract.json b/packages/@jsonql/koa/_tests/fixtures/contract/contract.json similarity index 100% rename from packages/@jsonql/koa/tests/fixtures/contract/contract.json rename to packages/@jsonql/koa/_tests/fixtures/contract/contract.json diff --git a/packages/@jsonql/koa/tests/fixtures/contract/public-contract.json b/packages/@jsonql/koa/_tests/fixtures/contract/public-contract.json similarity index 100% rename from packages/@jsonql/koa/tests/fixtures/contract/public-contract.json rename to packages/@jsonql/koa/_tests/fixtures/contract/public-contract.json diff --git a/packages/@jsonql/koa/tests/fixtures/resolvers/mutation/change-something.js b/packages/@jsonql/koa/_tests/fixtures/resolvers/mutation/change-something.js similarity index 100% rename from packages/@jsonql/koa/tests/fixtures/resolvers/mutation/change-something.js rename to packages/@jsonql/koa/_tests/fixtures/resolvers/mutation/change-something.js diff --git a/packages/@jsonql/koa/tests/fixtures/resolvers/query/get-something.js b/packages/@jsonql/koa/_tests/fixtures/resolvers/query/get-something.js similarity index 100% rename from packages/@jsonql/koa/tests/fixtures/resolvers/query/get-something.js rename to packages/@jsonql/koa/_tests/fixtures/resolvers/query/get-something.js diff --git a/packages/@jsonql/koa/tests/fixtures/setup.js b/packages/@jsonql/koa/_tests/fixtures/setup.js similarity index 100% rename from packages/@jsonql/koa/tests/fixtures/setup.js rename to packages/@jsonql/koa/_tests/fixtures/setup.js diff --git a/packages/@jsonql/koa/tests/fixtures/start.js b/packages/@jsonql/koa/_tests/fixtures/start.js similarity index 100% rename from packages/@jsonql/koa/tests/fixtures/start.js rename to packages/@jsonql/koa/_tests/fixtures/start.js diff --git a/packages/@jsonql/koa/tests/main.test.js b/packages/@jsonql/koa/_tests/main.test.js similarity index 100% rename from packages/@jsonql/koa/tests/main.test.js rename to packages/@jsonql/koa/_tests/main.test.js diff --git a/packages/@jsonql/koa/index.js b/packages/@jsonql/koa/index.js index 21db2e946c2771675e39fa1bbec1d4bd0fb43f6e..5ff0e9ca5cae94e3bbcb9afac78379fbd27447a7 100644 --- a/packages/@jsonql/koa/index.js +++ b/packages/@jsonql/koa/index.js @@ -5,15 +5,7 @@ const cors = require('koa-cors') const bodyparser = require('koa-bodyparser') const jsonql = require('jsonql-koa') const DEFAULT_PORT = 8001; -/** - * simple util method to get the value - * @param {string} name of the key - * @param {object} obj to take value from - * @return {*} the object value id by name or undefined - */ -const getConfigValue = (name, obj) => ( - (name in obj) ? obj[name] : undefined -) + /** * @param {object} [config={}] configuration diff --git a/packages/@jsonql/koa/package.json b/packages/@jsonql/koa/package.json index 8c8295d7e1a007b76e7a29c1df95b3b6e7800ebf..2cad776cfd73344c130a22d42f906c5140c7f0b2 100644 --- a/packages/@jsonql/koa/package.json +++ b/packages/@jsonql/koa/package.json @@ -1,11 +1,30 @@ { "name": "@jsonql/koa", - "version": "0.0.1", + "version": "0.1.0", "description": "Complete jsonql Koa setup + extra in later release", "main": "index.js", "scripts": { - "test": "DEBUG=@jsonql_koa* ava --verbose", - "start": "node ./test/fixtures/start.js" + "test": "ava --verbose", + "start": "node ./test/fixtures/start.js", + "test:debug": "DEBUG=jsonql* ava --verbose", + "coverage": "nyc ava --verbose", + "test:notfound": "DEBUG=jsonql-koa* ava --verbose ./tests/resolverNotFound.test.js", + "test:es6": "DEBUG=jsonql-koa* ava --verbose ./tests/es6-module.test.js", + "test:jwt": "DEBUG=jsonql-koa*,jsonql-jwt* ava ./tests/jwt.test.js", + "test:jwt-auth": "DEBUG=jsonql-koa* ava ./tests/jwt-auth.test.js", + "test:fail": "ava ./tests/fail.test.js", + "test:basic": "DEBUG=jsonql* ava ./tests/koa.test.js", + "test:contract": "DEBUG=jsonql-koa* ava ./tests/contractWithAuth.test.js", + "test:auth": "DEBUG=jsonql* ava ./tests/auth.test.js", + "test:error": "DEBUG=jsonql-koa* ava ./tests/resolverNotFound.test.js", + "test:config": "DEBUG=jsonql-* ava ./tests/config.test.js", + "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", + "test:clients": "DEBUG=jsonql* ava --verbose ./tests/node-client.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" }, "keywords": [ "jsonql", @@ -28,7 +47,7 @@ "bugs": { "url": "https://gitee.com/to1source/jsonql/issues" }, - "homepage": "https://gitee.com/to1source/jsonql#readme", + "homepage": "jsonql.org", "ava": { "files": [ "tests/*.test.js", diff --git a/packages/@jsonql/koa/src/contracts/contract-generator.js b/packages/@jsonql/koa/src/contracts/contract-generator.js new file mode 100644 index 0000000000000000000000000000000000000000..364b4c60f5c69a485afb4db7839dea0b9c642f4b --- /dev/null +++ b/packages/@jsonql/koa/src/contracts/contract-generator.js @@ -0,0 +1,51 @@ +// this need to get call from the beginning +// so make this available in different places +const fsx = require('fs-extra') +const { join } = require('path') +const contractApi = require('jsonql-contract') +const { + DEFAULT_CONTRACT_FILE_NAME, + PUBLIC_CONTRACT_FILE_NAME +} = require('jsonql-constants') + +const debug = require('debug')('jsonql-koa::contract-generator') +/** + * @param {string} contractDir where the contract is + * @param {boolean} pub system of public + * @return {mixed} false on not found + */ +const readContract = function(contractDir, pub) { + const file = join(contractDir, pub ? PUBLIC_CONTRACT_FILE_NAME : DEFAULT_CONTRACT_FILE_NAME) + if (fsx.existsSync(file)) { + debug('Serving up the existing one from ', file) + return fsx.readJsonSync(file) + } + return false; +} + +/** + * contract create handler + * @param {object} opts options + * @param {boolean} pub where to serve this + * @param {boolean} start is this the first call + * @return {object} promise to resolve contract json + */ +const contractGenerator = function(opts, pub = false, start = false) { + return new Promise((resolver, rejecter) => { + if (opts.buildContractOnStart === false && start === false) { + const contract = readContract(opts.contractDir, pub) + if (contract !== false) { + return resolver(contract) + } + } + contractApi(Object.assign({}, opts, { public: pub })) + .then(resolver) + .catch(rejecter) + }) +} + +// export +module.exports = { + contractGenerator, + readContract +} diff --git a/packages/@jsonql/koa/src/contracts/index.js b/packages/@jsonql/koa/src/contracts/index.js index d0f6bcd5bfaa417ef7149a7b5e2daa07738d207c..2e334287e8df84e3233d9d993fe40b43901fa2e3 100644 --- a/packages/@jsonql/koa/src/contracts/index.js +++ b/packages/@jsonql/koa/src/contracts/index.js @@ -1 +1,40 @@ -// contract related methods export +// move all the code into it's folder +// we are going to fork the process to lighten the load when it start +const { fork } = require('child_process') +const { join } = require('path') +const debug = require('debug')('jsonql-koa:contract-generator') +// try to cache it +let contractCache = {}; +/** + * getContract main + * @param {object} config options + * @param {boolean} pub public contract or not + * @return {object} Promise to resolve the contract json + */ +module.exports = function(config, pub = false) { + const ps = fork(join(__dirname, 'run.js')) + const key = pub ? 'public' : 'private'; + if (contractCache[key]) { + debug(`return ${key} contract from cache`, contractCache[key]) + // this is the problem - if we want to keep it consistent then + // we should always provide the contract key --> cache is optional! + // { contract: contractCache[key], cache: true } + return Promise.resolve(contractCache[key]) + } + ps.send({ config, pub }) + // return + return new Promise((resolver, rejecter) => { + ps.on('message', msg => { + if (msg.contract) { + // contractCache[key] = msg.contract; // <-- disable cache + // return + return resolver(msg.contract) + } + rejecter(msg) + }) + ps.on('error', err => { + debug('ps error', err) + rejecter(err) + }) + }) +} diff --git a/packages/@jsonql/koa/src/contracts/process-contract.js b/packages/@jsonql/koa/src/contracts/process-contract.js new file mode 100644 index 0000000000000000000000000000000000000000..9a6a4a4eba0279510c8b5426e8c65a3cd6bed59c --- /dev/null +++ b/packages/@jsonql/koa/src/contracts/process-contract.js @@ -0,0 +1,41 @@ +// wrap the method in the init-middleware here +const { JsonqlError } = require('jsonql-errors') +const { isContractJson, getDebug } = require('../utils') +const debug = getDebug('process-contract') + +/** + * Create the start of chain + * @param {object} opts configuration + * @return {object} promise resolve the contract if found + */ +const getFromOpts = opts => { + return Promise.resolve( + isContractJson(opts.contract) + ) +} + +/** + * @param {object} ctx koa context + * @param {object} opts configuration + * @return {object} promise to resolve the contract + */ +module.exports = async function processContract(ctx, opts) { + // const { setter, getter } = ctx.state.jsonql; + return getFromOpts(opts) + .then(c => c || false) + // .then(c => !c ? getter('contract') : c) + .then(c => { + if (!c) { + if (!ctx.state.jsonql.contract && opts.initContract && opts.initContract.then) { + debug('calling the initContract to get the contract') + return opts.initContract.then(c => { + // disable the cache for the time being + // setter('contract', c) + return c; + }) + } + throw new JsonqlError('Unable to get the contract!') + } + return c; + }) +} diff --git a/packages/@jsonql/koa/src/contracts/run.js b/packages/@jsonql/koa/src/contracts/run.js new file mode 100644 index 0000000000000000000000000000000000000000..6dcbb6947bf79fc07999b52f20d15cf48cee82e2 --- /dev/null +++ b/packages/@jsonql/koa/src/contracts/run.js @@ -0,0 +1,19 @@ +// /lib/contract-generator/run.js +const { contractGenerator, readContract } = require('./contract-generator') +// listening +process.on('message', m => { + const { config, pub } = m; + if (config) { + let contract = readContract(config.contractDir, pub) + if (contract !== false) { + return process.send({ contract }) + } + contractGenerator(config, pub) + .then(contract => { + process.send({ contract }) + }) + .catch(error => { + process.send({ error }) + }) + } +}) diff --git a/packages/@jsonql/koa/src/index.js b/packages/@jsonql/koa/src/index.js index 4894a8771e557662919193ad73b9b00460704bd2..3098a81d5e469273a75ea9c98188d37a17ad0860 100644 --- a/packages/@jsonql/koa/src/index.js +++ b/packages/@jsonql/koa/src/index.js @@ -1 +1 @@ -// completely reorganize the folder structure etc +// completely reorganize the folder structure etc diff --git a/packages/@jsonql/koa/src/middlewares/auth-middleware.js b/packages/@jsonql/koa/src/middlewares/auth-middleware.js new file mode 100644 index 0000000000000000000000000000000000000000..377f82eff3e4ecfefe8231cde8edda8df8d3911b --- /dev/null +++ b/packages/@jsonql/koa/src/middlewares/auth-middleware.js @@ -0,0 +1,153 @@ +// jsonql-auth middleware +const { + AUTH_TYPE, + ISSUER_NAME, + VALIDATOR_NAME, + AUTH_CHECK_HEADER, + BEARER +} = require('jsonql-constants') +const { + chainFns, + getDebug, + packResult, + headerParser, + printError, + isObject, + isNotEmpty, + handleOutput, + forbiddenHandler, + ctxErrorHandler, + createTokenValidator +} = require('./lib') +const { + JsonqlResolverNotFoundError, + JsonqlAuthorisationError, + JsonqlValidationError, + finalCatch +} = require('jsonql-errors') +const { getLocalValidator } = require('jsonql-resolver') +const { trim } = require('lodash') + +const debug = getDebug('auth-middleware') + +// this will create a cache version without keep calling the getter +var validatorFn; + +// declare a global variable to store the userdata with null value +// this way we don't mess up with the resolver have to check +// the last param is the user data + +/** + * @param {object} ctx Koa context + * @param {string} type to look for + * @return {mixed} the bearer token on success + */ +const authHeaderParser = ctx => { + // const header = headers[AUTH_CHECK_HEADER]; + let header = ctx.request.get(AUTH_CHECK_HEADER) + // debug(_header, AUTH_CHECK_HEADER); + // debug('Did we get the token?', header); + return header ? getToken(header) : false; +} + +/** + * just return the token string + * @param {string} header + * @return {string} token + */ +const getToken = header => { + return trim(header.replace(BEARER, '')) +} + +/** + * when using useJwt we allow the user to provide their own validator + * and we pass the result to this validator for them to do further processing + * This is useful because the user can control if they want to invalidate the client side + * from here based on their need + * @param {object} config configuration + * @param {function|boolean} validator false if there is none + * @return {function} the combine validator + */ +const createJwtValidatorChain = (config, validator = false) => { + const jwtFn = createTokenValidator(config) + if (!validator || typeof validator !== 'function') { + return jwtFn; + } + return chainFns(jwtFn, validator) +} + +/** + * if useJwt = true then use the jsonql-jwt version + * @param {object} config configuration + * @param {string} type type of call + * @param {object} contract contract.json + * @return {function} the correct handler + */ +const getValidator = (config, type, contract) => { + if (validatorFn && typeof validatorFn === 'function') { + return validatorFn; + } + let localValidator; + try { + localValidator = getLocalValidator(config, type, contract) + } catch(e) { + // we ignore this error becasue they might not have one? + if (!(e instanceof JsonqlResolverNotFoundError)) { + return finalCatch(e) + } + } + if (config.useJwt) { + return createJwtValidatorChain(config, localValidator) + } + return localValidator; +} + +/** + * Auth middleware, we support + * 1) OAuth 2 + * 2) JWT token + * This is just front we don't do any real auth here, + * instead we expect you to supply a function and pass you data and let you + * handle the actual authentication + * @TODO need to break this down further at the moment its very hard to debug what is going on here + */ +module.exports = function(config) { + // return middleware + return async function(ctx, next) { + // we should only care if there is api call involved + const { isReq, contract } = ctx.state.jsonql; + if (isReq && config.enableAuth) { + try { + const token = authHeaderParser(ctx) + if (token) { + debug('got a token', token) + validatorFn = getValidator(config , AUTH_TYPE, contract) + let userdata = await validatorFn(token) + debug('validatorFn result', userdata) + if (isNotEmpty(userdata) && isObject(userdata)) { + // here we add the userData to the global + // @TODO need more testing to see if this is going to work or not + ctx.state.jsonql.userdata = userdata; + // debug('get user data result', userdata) + await next() + } else { + debug('throw at wrong result', userdata) + return forbiddenHandler(ctx, {message: 'userdata is empty?'}) + } + } else { + debug('throw at headers not found', ctx.request.headers) + return forbiddenHandler(ctx, {message: 'header is not found!'}) + } + } catch(e) { + if (e instanceof JsonqlResolverNotFoundError) { + return ctxErrorHandler(ctx, 404, e) + } else { + debug('throw at some where throw error', e) + return forbiddenHandler(ctx, e) + } + } + } else { + await next() + } + } +} diff --git a/packages/@jsonql/koa/src/middlewares/console-middleware.js b/packages/@jsonql/koa/src/middlewares/console-middleware.js new file mode 100644 index 0000000000000000000000000000000000000000..637c4d3d1c531ac61a467422584e2beb23085749 --- /dev/null +++ b/packages/@jsonql/koa/src/middlewares/console-middleware.js @@ -0,0 +1,8 @@ +// This will be handling the json:ql console +const { isJsonqlConsoleUrl } = require('./lib') +const webConsole = require('jsonql-web-console') + +// export +module.exports = function(opts) { + return webConsole(opts, isJsonqlConsoleUrl) +} diff --git a/packages/@jsonql/koa/src/middlewares/contract-middleware.js b/packages/@jsonql/koa/src/middlewares/contract-middleware.js new file mode 100644 index 0000000000000000000000000000000000000000..6bdade1088d40e73e7289f791121c2e3dcbc451e --- /dev/null +++ b/packages/@jsonql/koa/src/middlewares/contract-middleware.js @@ -0,0 +1,142 @@ +// This is the first part of the middlewares that try to work out the contract +// the reason is, it will get re-use by subsequences middlewares +// so we might as well do this here +// Also we could hijack it off and serve the html files up for documentation purpose + +const fsx = require('fs-extra') +const { join } = require('path') +const { trim } = require('lodash') +const { CONTRACT_NAME, SHOW_CONTRACT_DESC_PARAM } = require('jsonql-constants') +const { JsonqlContractAuthError } = require('jsonql-errors') +const { isKeyInObject } = require('jsonql-params-validator') +const { + getDebug, + getContract, + handleOutput, + packResult, + isContractJson, + ctxErrorHandler +} = require('./lib') + +const debug = getDebug('contract-middleware') + +/** + * remove the description field + * @param {boolean} showDesc true to keep + * @param {object} contract json + * @return {object} clean contract + */ +const removeDesc = (showDesc, contract) => { + debug('showDesc', showDesc) + if (showDesc) { + return contract; + } + let c = contract; + for (let type in c) { + for (let fn in c[type]) { + if (isKeyInObject(c[type][fn], 'description')) { + delete c[type][fn].description; + if (c[type][fn].returns && isKeyInObject(c[type][fn].returns, 'description')) { + delete c[type][fn].returns.description; + } + } + } + } + return c; +} + +/** + * get the contract data @TODO might require some sort of security here + * @param {object} opts options + * @param {object} ctx koa + * @return {undefined} + */ +const handleContract = (opts, ctx, contract) => { + // @1.3.2 add a filter here to exclude the description field + // just to reduce the size, but we serve it up if this is request with + // desc=1 param - useful for the jsonql-web-console + // debug('handleContract', ctx.query.desc) + const key = Object.keys(SHOW_CONTRACT_DESC_PARAM)[0] + let desc = !!(ctx.query[key] && ctx.query[key] === SHOW_CONTRACT_DESC_PARAM[key]) + handleOutput(opts)(ctx, packResult( + removeDesc(desc, contract) + )) +} + +/** + * Search for the value from the CONTRACT_KEY_NAME + * @param {object} ctx koa context + * @param {string} contractKeyName the key to search + * @return {string} the value from header + */ +const searchContractAuth = function(ctx, contractKeyName) { + // debug(`Try to get ${contractKeyName} from header`, ctx.request.header); + return ctx.request.get(contractKeyName) +} + +/** + * Handle contract authorisation using a key + * @param {object} ctx koa + * @param {object} opts options + * @return {boolean} true ok + */ +const contractAuth = function(ctx, opts) { + if (opts.contractKey !== false && opts.contractKeyName !== false) { + const { contractKey, contractKeyName } = opts; + // debug('Received this for auth', contractKeyName, contractKey); + // @2019-05-08 we change from url query to header + // const keyValueFromClient = trim(ctx.query[contractKeyName]); + // debug('the query value', ctx.query); + const keyValueFromClient = searchContractAuth(ctx, contractKeyName) + + switch (true) { + case typeof contractKey === 'string': + // debug('compare this two', keyValueFromClient, contractKey); + return keyValueFromClient === contractKey; + break; + default: + // @TODO what if we want to read the header? + debug('Unsupported contract auth type method', typeof contractKey) + return false; + } + } + return true; +} + +/** + * @TODO is there a bug in here somewhere that I am not aware of + * it seems to me that everytime the middleware get call, it keep trying to + * generate contract, or reading from the contract, which is not ideal + * it should able to cache it some how? + */ +module.exports = function(opts) { + // export + return async function(ctx, next) { + // this will only handle certain methods + const { isReq, resolverType } = ctx.state.jsonql; + // @2019-05-24 We need to make sure the call is actually a jsonql call + // because when http access happen it could make multiple call to the + // server and contract generator just run multiple times on a very short time + // and cause the file read failure + if (isReq) { + // now is this request asking for the public contract + if (resolverType === CONTRACT_NAME) { + if (contractAuth(ctx, opts)) { + let publicContractJson = ctx.state.jsonql.publicContract; + if (!publicContractJson) { + debug(`call the get public contract here`, opts.name) + // This should be a one off event + publicContractJson = await getContract(opts, true) + ctx.state.jsonql.publicContract = publicContractJson; + } + // debug('call handle public contract method here'); + // this is a public contract + return handleContract(opts, ctx, publicContractJson) + } else { + return ctxErrorHandler(ctx, 'JsonqlContractAuthError') + } + } + } + await next() + } +} diff --git a/packages/@jsonql/koa/src/middlewares/core-middleware.js b/packages/@jsonql/koa/src/middlewares/core-middleware.js new file mode 100644 index 0000000000000000000000000000000000000000..234e748867287030013b54a6b429cbf8fe322e89 --- /dev/null +++ b/packages/@jsonql/koa/src/middlewares/core-middleware.js @@ -0,0 +1,27 @@ +// The core of the jsonql middleware +const { QUERY_NAME, MUTATION_NAME } = require('jsonql-constants') +const { getDebug, resolveMethod } = require('./lib') +const debug = getDebug('core') + +/** + * Top level entry point for jsonql Koa middleware + * @param {object} config options + * @return {mixed} depends whether if we catch the call + */ +module.exports = function(opts) { + // ouput the middleware + return async function(ctx, next) { + if (ctx.state.jsonql.isReq) { + const { contract, resolverType } = ctx.state.jsonql; + debug('isJsonqlRequest', resolverType) + if (resolverType === QUERY_NAME || resolverType === MUTATION_NAME) { + // debug(`Is jsonql query`, contract, opts) + // The problem is - the config is correct + // but the contract return the previous one + return resolveMethod(ctx, resolverType, opts, contract) + } + } else { + await next() + } + } +} diff --git a/packages/@jsonql/koa/src/middlewares/errors-handler-middleware.js b/packages/@jsonql/koa/src/middlewares/errors-handler-middleware.js new file mode 100644 index 0000000000000000000000000000000000000000..2a6e7f8c5597cfdf5ee7fa2526e04e75dae47a39 --- /dev/null +++ b/packages/@jsonql/koa/src/middlewares/errors-handler-middleware.js @@ -0,0 +1,25 @@ +// we will get rip of all the try catch inside each of the middleware +// and collect all the throw here +const { isContractJson, getDebug } = require('./lib') +const debug = getDebug('errors-handler-middleware') +// @TODO NOT IN USE AT THE MOMEMENT + +module.exports = function(config) { + // return middleware + return async function(ctx, next) { + return next().catch(err => { + // ctx.assert(err instanceof JsonqlServerError, 404 , 'Checking of this is what throw earlier'); + debug('Catch an error here', err) + const { statusCode, message } = err; + ctx.type = 'json'; + ctx.status = statusCode || 500; + ctx.body = JSON.stringify({ + error: { + className: 'JsonqlServerError', + message + } + }); + ctx.app.emit('error', err, ctx) + }) + } +} diff --git a/packages/@jsonql/koa/src/middlewares/hello-middleware.js b/packages/@jsonql/koa/src/middlewares/hello-middleware.js new file mode 100644 index 0000000000000000000000000000000000000000..4b151e78e26220294dde7eba2fcf1c0255dcdc71 --- /dev/null +++ b/packages/@jsonql/koa/src/middlewares/hello-middleware.js @@ -0,0 +1,19 @@ +// The helloWorld should always be available. Because this can serve as a ping even when its auth:true +const { getDebug, handleOutput, packResult } = require('./lib') +const { HELLO, HELLO_FN, QUERY_NAME } = require('jsonql-constants') +const debug = getDebug('hello-middlware') +// export +module.exports = function(opts) { + return async function(ctx, next) { + const { isReq, resolverType, resolverName } = ctx.state.jsonql; + // so here we check two things, the header and the url if they match or not + if (isReq && resolverName === HELLO_FN && resolverType === QUERY_NAME) { + debug('********* ITS CALLING THE HELLO WORLD *********') + return handleOutput(opts)(ctx, packResult(HELLO)) + } else { + // @NOTE For some reason if I don't wrap this in an else statment + // I got an error said next get call multiple times + await next() + } + } +} diff --git a/packages/@jsonql/koa/src/middlewares/index.js b/packages/@jsonql/koa/src/middlewares/index.js index 17ee5c44eded2377ea5684db48e628f9c90bdc72..fc69da0f45caa0401aeec7f0e764060fc934f895 100644 --- a/packages/@jsonql/koa/src/middlewares/index.js +++ b/packages/@jsonql/koa/src/middlewares/index.js @@ -1 +1,26 @@ -// all middleware export +// main export interface +const authMiddleware = require('./auth-middleware') +const coreMiddleware = require('./core-middleware') +const contractMiddleware = require('./contract-middleware') +const helloMiddleware = require('./hello-middleware') + + +const consoleMiddleware = require('./console-middleware') +const publicMethodMiddleware = require('./public-method-middleware') +const errorsHandlerMiddleware = require('./errors-handler-middleware') +const initMiddleware = require('./init-middleware') + +// export +module.exports = { + configCheck, + + consoleMiddleware, + authMiddleware, + coreMiddleware, + contractMiddleware, + helloMiddleware, + + publicMethodMiddleware, + errorsHandlerMiddleware, + initMiddleware +} diff --git a/packages/@jsonql/koa/src/middlewares/init-middleware.js b/packages/@jsonql/koa/src/middlewares/init-middleware.js new file mode 100644 index 0000000000000000000000000000000000000000..afa9e887b50f098b953b92bacae135d006f186b9 --- /dev/null +++ b/packages/@jsonql/koa/src/middlewares/init-middleware.js @@ -0,0 +1,143 @@ +// this will be the first part of the middleware that checkout the +// headers and request and extract the parts that we need for the operation +const { + inArray, + getDebug, + isJsonqlRequest, + isContractJson, + isObject, + ctxErrorHandler, + processJwtKeys +} = require('./lib') +const { + AUTH_TYPE, + QUERY_NAME, + MUTATION_NAME, + CONTRACT_NAME, + CONTRACT_REQUEST_METHODS, + API_REQUEST_METHODS, + PAYLOAD_PARAM_NAME, + CONDITION_PARAM_NAME, + RESOLVER_PARAM_NAME, + JSONP_CALLBACK_NAME +} = require('jsonql-constants') +const { Jsonql406Error } = require('jsonql-errors') +const processContract = require('./lib/contract-generator/process-contract') +const { getQueryFromPayload, getMutationFromPayload } = require('jsonql-params-validator') + +const debug = getDebug('init-middleware') + +/** + * Just figure out what is the calling methods here + * @param {object} ctx koa context + * @return {*} false on unknown + */ +const getBaseResolverType = function(ctx) { + const [ POST, PUT ] = API_REQUEST_METHODS; + const { method } = ctx; + switch (true) { + case inArray(CONTRACT_REQUEST_METHODS, method): + return CONTRACT_NAME; + case method === POST: + return QUERY_NAME; + case method === PUT: + return MUTATION_NAME; + } +} + +/** + * check if it is auth type + * @param {string} type we need this to be the QUERY_TYPE + * @param {string} name from ctx.request.body to find the issuer name present or not + * @param {object} opts config we need that to find the custom names + */ +const isAuthType = function(type, name, opts) { + const { logoutHandlerName, loginHandlerName } = opts; + const AUTH_TYPE_METHODS = [loginHandlerName, logoutHandlerName] + if (type === QUERY_NAME) { + return inArray(AUTH_TYPE_METHODS, name) ? AUTH_TYPE : type; + } + return type; +} + +/** + * new in v1.3.0 jsonp handler + * @param {object} ctx koa context + * @return {boolean|string} return resolverName or false when its not + */ +const isJsonpCall = function(ctx) { + if (ctx.query && ctx.query[JSONP_CALLBACK_NAME]) { + return ctx.query[JSONP_CALLBACK_NAME] + } + return false; +} + +/** + * v1.2.0 add setter and getter and store in the ctx for use later + * @param {object} opts configuration + * @param {function} setter nodeCache set + * @param {function} getter nodeCache get + * @return {function} middleware + */ +module.exports = function(opts) { + // export + return async function(ctx, next) { + ctx.state.jsonql = {}; + // ctx.state.jsonql.setter = setter; + // ctx.state.jsonql.getter = getter; + // first check if its calling the jsonql api + const isJsonql = isJsonqlRequest(ctx, opts) + // init our own context + ctx.state.jsonql.isReq = isJsonql; + if (isJsonql) { + // its only call once + ctx.state.jsonql.contract = await processContract(ctx, opts) + // v1.2.0 grabbing the public / private keys + opts = await processJwtKeys(ctx, opts) + // debug('processJwtKeys', opts) + // get what is calling + let payload = ctx.request.body; + // new in v1.3.0 - test for jsonp type + if (opts.enableJsonp === true) { + const jsonp = isJsonpCall(ctx) + if (jsonp !== false) { + ctx.state.jsonql.jsonp = jsonp; + // mutate the payload to let the rest to work + payload = {[jsonp]: payload} + debug('jsonp', jsonp, payload) + } + } + // start + let type = getBaseResolverType(ctx) + if (type) { + let params; + switch(type) { + case CONTRACT_NAME: + ctx.state.jsonql.resolverType = CONTRACT_NAME; + break; + case QUERY_NAME: + params = getQueryFromPayload(payload) + break; + case MUTATION_NAME: + params = getMutationFromPayload(payload) + break; + default: // should never happen! + throw new JsonqlError(`[init-middleware] ${type} is unknown!`) + } + if (type !== CONTRACT_NAME) { + let name = params[RESOLVER_PARAM_NAME]; + ctx.state.jsonql.resolverName = name; + ctx.state.jsonql.payload = params; + ctx.state.jsonql.resolverType = isAuthType(type, name, opts) + } + } else { + return ctxErrorHandler(ctx, 406, { + message: 'Payload is not the expected object type', + payload + } + ); + } + } + await next() + } +} diff --git a/packages/@jsonql/koa/src/middlewares/public-method-middleware.js b/packages/@jsonql/koa/src/middlewares/public-method-middleware.js new file mode 100644 index 0000000000000000000000000000000000000000..162a208b6ede156753045fe5d4f0370467eee0e4 --- /dev/null +++ b/packages/@jsonql/koa/src/middlewares/public-method-middleware.js @@ -0,0 +1,29 @@ +// we need to let this middleware take the public method first before +// running pass to the auth-middleware otherwise, it will not able to +// get to the next one +const { QUERY_NAME, MUTATION_NAME, AUTH_TYPE } = require('jsonql-constants') +const { resolveMethod, handleAuthMethods } = require('jsonql-resolver') + +const { getDebug, extractParamsFromContract } = require('./lib') + +const debug = getDebug('public-method-middleware') + +// main export +module.exports = function(opts) { + return async function(ctx, next) { + const { isReq, resolverType, resolverName, payload, contract } = ctx.state.jsonql; + // we pre-check if this is auth enable, if it's not then let the other middleware to deal with it + if (isReq && opts.enableAuth) { + if (resolverType === QUERY_NAME || resolverType === MUTATION_NAME) { + let params = extractParamsFromContract(contract, resolverType, resolverName) + if (params.public === true) { + return resolveMethod(ctx, resolverType, opts, contract) + } + } else if (resolverType === AUTH_TYPE && resolverName === opts.loginHandlerName) { + debug(`This is an auth ${opts.loginHandlerName} call`); + return handleAuthMethods(ctx, resolverName, payload, opts, contract) + } + } + await next() + } +} diff --git a/packages/@jsonql/koa/src/options/index.js b/packages/@jsonql/koa/src/options/index.js index 55c1ad22e0d8e28b5b7c1aa2e78b1a07af6c4581..506ddeced7c90819ab15bdaea6eed55071ad795e 100644 --- a/packages/@jsonql/koa/src/options/index.js +++ b/packages/@jsonql/koa/src/options/index.js @@ -1 +1,79 @@ -// all the configuration options related methods export +// wrap all the options and method in one instead of all over the places +const { join, resolve } = require('path') +const fsx = require('fs-extra') +const _ = require('lodash') +const { checkConfig, isString } = require('jsonql-params-validator') +const { rsaPemKeys } = require('jsonql-jwt') + +const { appProps, constProps, jwtProcessKey } = require('./options') +const { getContract, isContractJson, chainFns, getDebug, inArray } = require('../index') + +const debug = getDebug('config-check') + +/** + * break out from the applyAuthOptions because it's not suppose to be there + * @NOTE v1.3.8 change it to a fully functional interface + * @param {object} config configuration + * @return {object} with additional properties + */ +const applyGetContract = function(config) { + return _(config) + .chain() + .thru(config => { + const { contract } = config; + if (isContractJson(contract)) { + config.contract = contract; + return [true, config] + } + return [false, config] + }) + .thru(result => { + let [processed, config] = result + if (!processed) { + debug(`call initContract`) + // get resolve later inside the middleware + config.initContract = getContract(config) + } + return config; + }) + .thru(config => { + if (config.withPublicContract) { + debug(`call generate public contract`) + getContract(config, true) + } + return config; + }) + .value() +} + +/** + * 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) + const privateKeyPath = join(keysDir, privateKeyFileName) + if (fsx.existsSync(publicKeyPath) && fsx.existsSync(privateKeyPath)) { + config.publicKey = fsx.readFileSync(publicKeyPath) + config.privateKey = fsx.readFileSync(privateKeyPath) + } else { + // we only call here then resolve inside the init-middleware + config[jwtProcessKey] = rsaPemKeys(config.rsaModulusLength, config.keysDir) + } + } + return config; +} + +/** + * @param {object} config configuration supply by developer + * @return {object} configuration been checked + * @api public + */ +module.exports = function configCheck(config) { + const fn = chainFns(checkConfig, applyGetContract, applyAuthOptions) + return fn(config, appProps, constProps) +} diff --git a/packages/@jsonql/koa/src/options/options.js b/packages/@jsonql/koa/src/options/options.js new file mode 100644 index 0000000000000000000000000000000000000000..e423dd7435b5c66a1d90d91f78d7bacf3eece440 --- /dev/null +++ b/packages/@jsonql/koa/src/options/options.js @@ -0,0 +1,113 @@ +const { join } = require('path'); +const fs = require('fs'); +const { + PUBLIC_KEY, + PRIVATE_KEY, + JSONQL_PATH, + CONTENT_TYPE, + DEFAULT_RESOLVER_DIR, + DEFAULT_CONTRACT_DIR, + CONTRACT_KEY_NAME, + ARRAY_TYPE, + BOOLEAN_TYPE, + STRING_TYPE, + NUMBER_TYPE, + OBJECT_TYPE, + ARGS_KEY, + TYPE_KEY, + ENUM_KEY, + CHECKER_KEY, + ACCEPTED_JS_TYPES, + CJS_TYPE, + ISSUER_NAME, + LOGOUT_NAME, + VALIDATOR_NAME, + RETURN_AS_JSON, + DEFAULT_KEYS_DIR, + DEFAULT_PUBLIC_KEY_FILE, + DEFAULT_PRIVATE_KEY_FILE, + RSA_MIN_MODULE_LEN +} = require('jsonql-constants') +const { + createConfig, + constructConfig +} = require('jsonql-params-validator') +// const NodeCache = require('node-cache'); +// const mcache = new NodeCache; +// @BUG when we deploy it in docker, or using systemd the workingDirectory affect the +// execute path, it might be better to allow a config option of workingDirectory and +// use that as base +const dirname = process.cwd() +// @TODO we need to create the same fn to clear out the options like I did in server-io-core +const constProps = { + __checked__: true, + contentType: CONTENT_TYPE, + contract: false, + initContract: false, + useDoc: true, + returnAs: RETURN_AS_JSON, + privateKey: false, + publicKey: false, + initJwtKeys: false +} + +const appProps = { + name: createConfig('jsonql-koa', [STRING_TYPE]), // this is for ID which one is which when use as ms + expired: createConfig(0, [NUMBER_TYPE]), + // allow user to change their auth type methods name + loginHandlerName: createConfig(ISSUER_NAME, [STRING_TYPE]), + logoutHandlerName: createConfig(LOGOUT_NAME, [STRING_TYPE]), + validatorHandlerName: createConfig(VALIDATOR_NAME, [STRING_TYPE, BOOLEAN_TYPE]), + // this flag will change many things + enableAuth: {[ARGS_KEY]: false, [TYPE_KEY]: BOOLEAN_TYPE}, + // from now on always turn this to on + useJwt: createConfig(true, [BOOLEAN_TYPE, STRING_TYPE]), + jwtTokenOption: createConfig(false, [BOOLEAN_TYPE, OBJECT_TYPE]), + // add in v1.3.0 + enableJsonp: createConfig(false, [BOOLEAN_TYPE]), + // show or hide the description field in the public contract + contractWithDesc: createConfig(false, [BOOLEAN_TYPE]), + // @1.3.4 whenever generate a contract will generate the public contract as well + withPublicContract: createConfig(true, [BOOLEAN_TYPE]), + keysDir: createConfig(join(dirname, DEFAULT_KEYS_DIR), [STRING_TYPE]), + publicKeyFileName: createConfig(DEFAULT_PUBLIC_KEY_FILE, [STRING_TYPE]), + privateKeyFileName: createConfig(DEFAULT_PRIVATE_KEY_FILE, [STRING_TYPE]), + rsaModulusLength: createConfig(RSA_MIN_MODULE_LEN, [NUMBER_TYPE]), + + jsonqlPath: {[ARGS_KEY]: ['/', JSONQL_PATH].join(''), [TYPE_KEY]: STRING_TYPE}, + resolverDir: {[ARGS_KEY]: join(dirname, DEFAULT_RESOLVER_DIR), [TYPE_KEY]: STRING_TYPE, [CHECKER_KEY]: fs.existsSync}, + // we don't really need to check if the contract directory exist or not, it will get created + contractDir: {[ARGS_KEY]: join(dirname, DEFAULT_CONTRACT_DIR), [TYPE_KEY]: STRING_TYPE}, + + contractKey: {[ARGS_KEY]: false, [TYPE_KEY]: [BOOLEAN_TYPE, STRING_TYPE]}, + contractKeyName: {[ARGS_KEY]: CONTRACT_KEY_NAME, [TYPE_KEY]: STRING_TYPE}, + + publicMethodDir: createConfig(PUBLIC_KEY, [STRING_TYPE]), + // just try this with string type first + privateMethodDir: constructConfig(PRIVATE_KEY, [STRING_TYPE], true), + + // new feature for v.1.1 release + // if the developer pass the nodeClient config then we will pre-generate the calling method + // for them. We expect them to named the client so it will be key:value pair + + enableWebConsole: {[ARGS_KEY]: false, [TYPE_KEY]: [BOOLEAN_TYPE, OBJECT_TYPE]}, // you need to actively enable this option to have the web console enable + jsType: {[ARGS_KEY]: CJS_TYPE, [TYPE_KEY]: STRING_TYPE, [ENUM_KEY]: ACCEPTED_JS_TYPES}, + + // undecided properties + // 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 + autoCreateContract: {[ARGS_KEY]: true, [TYPE_KEY]: BOOLEAN_TYPE}, + buildContractOnStart: {[ARGS_KEY]: false, [TYPE_KEY]: BOOLEAN_TYPE}, // process.env.NODE_ENV === 'development', + keepLastContract: {[ARGS_KEY]: false, [TYPE_KEY]: BOOLEAN_TYPE}, // true keep last one, integer > 0 keep that number of files + validateReturns: {[ARGS_KEY]: false, [TYPE_KEY]: BOOLEAN_TYPE}, // reserved for use in the future + // For v1.5.0 to integrate the node-client + clientConfig: createConfig([], [ARRAY_TYPE]) +}; + +module.exports = { + constProps, + appProps, + jwtProcessKey: 'INIT_JWT_KEYS' // just for id the promise call +}; diff --git a/packages/@jsonql/koa/src/options/process-jwt-keys.js b/packages/@jsonql/koa/src/options/process-jwt-keys.js new file mode 100644 index 0000000000000000000000000000000000000000..d960c56d29052eefcc39946234a7e3b7ad699a86 --- /dev/null +++ b/packages/@jsonql/koa/src/options/process-jwt-keys.js @@ -0,0 +1,70 @@ +// Async Await is a fucking joke +// We can't use aynsc await during the start up because you can't wrap that output call +// as a async await ... on and on and on +// so we need to do this in two steps +// if we find a keys file, great, read it and store it +// if not then wait until inside the init-middleware and call the rsaPemKeys there +const _ = require('lodash') +const fsx = require('fs-extra') +const { jwtProcessKey } = require('./options') +const { isKeyInObject, isString } = require('jsonql-params-validator') +const debug = require('debug')('jsonql-koa:process-jwt-keys') + +/** + * Get the keys from cache call + * @param {object} ctx koa context + * @param {object} config configuration + * @return {mixed} boolean on failed or object on success + */ +const getKeysFromCache = (ctx, config) => { + const { setter, getter } = ctx.state.jsonql; + if (config.enableAuth && + config.useJwt && + !isString(config.useJwt) && + (!config.publicKey || !config.privateKey)) { + let privateKey = getter('privateKey') + let publicKey = getter('publicKey') + if (privateKey && publicKey) { + return _.extend(config, { publicKey, privateKey }) + } + } + return false; +} + +/** + * Get the keys from the init promise call + * @param {object} ctx koa context + * @param {object} config configuration + * @return {mixed} boolean on failed or object on success + */ +const getCreatedKeys = (ctx, config) => { + if (isKeyInObject(config, jwtProcessKey) && config[jwtProcessKey].then) { + const { setter } = ctx.state.jsonql; + return config[jwtProcessKey] + .then( result => _.extend( config, _.mapValues(result, value => fsx.readFileSync(value) ) ) ) + .then(keys => { + _.forEach(keys, (value, key) => { + setter(key, value) + }) + return keys; + }) + } + return false; +} + +/** + * we only call this here to init it + * @param {object} ctx koa context + * @param {object} config configuration + * @return {object} config with the privateKey and publicKey stored + */ +module.exports = function processJwtKeys(ctx, config) { + let result; + if ((result = getKeysFromCache(ctx, config)) !== false) { + return result; + } + if ((result = getCreatedKeys(ctx, config)) !== false) { + return result; + } + return config; +} diff --git a/packages/@jsonql/koa/src/utils/cache.js b/packages/@jsonql/koa/src/utils/cache.js new file mode 100644 index 0000000000000000000000000000000000000000..f1b19b00cd242ec431719e1b1d3e9a803f233c35 --- /dev/null +++ b/packages/@jsonql/koa/src/utils/cache.js @@ -0,0 +1,22 @@ +// hope this will solve the missing contract of what not problem once and for all +const nodeCache = require('node-cache') +const cache = new nodeCache() +const debug = require('debug')('jsonql-koa:node-cache') +/** + * @param {string} key to id + * @param {*} value value to store + * @return {*} value on success + */ +const setter = (key, value) => cache.set(key, value) ? value : false; + +/** + * throw error if the data is missing + * @param {string} key to id + * @return {*} value or throw error if missing + */ +const getter = key => { + debug(key, ' read from cache') + return cache.get(key) +} + +module.exports = { setter, getter } diff --git a/packages/@jsonql/koa/src/utils/utils.js b/packages/@jsonql/koa/src/utils/utils.js new file mode 100644 index 0000000000000000000000000000000000000000..37b1331877e1c1b9eca6071a532b51df6257c95c --- /dev/null +++ b/packages/@jsonql/koa/src/utils/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/@jsonql/koa/tests/auth.test.js b/packages/@jsonql/koa/tests/auth.test.js new file mode 100644 index 0000000000000000000000000000000000000000..afa6eef33683cc44458277d27a71cd58c7eace15 --- /dev/null +++ b/packages/@jsonql/koa/tests/auth.test.js @@ -0,0 +1,118 @@ +const test = require('ava') + +const superkoa = require('superkoa') +const { join } = require('path') +const debug = require('debug')('jsonql-koa:test:auth') +const { createQuery } = require('jsonql-params-validator') +const fsx = require('fs-extra') +const { merge } = require('lodash') +const { HELLO_FN } = require('jsonql-constants') +const jsonqlMiddleware = require(join(__dirname, '..', 'index')) +const { type, headers, dirs, bearer, contractKeyName } = require('./fixtures/options') +const createServer = require('./helpers/server') +const myKey = '4670994sdfkl'; +const dir = 'auth'; +const thisHeader = merge({}, {[contractKeyName]: myKey}, headers) + +test.before((t) => { + t.context.app = createServer({ + useJwt: false, + enableAuth: true, + contractKey: myKey + }, dir) +}) + +test.after( () => { + // remove the files after + fsx.removeSync(join(dirs.contractDir, dir)) +}) + +// Start running test(s) + +test("Should NOT fail this Hello world test even I am not login", async t => { + let res = await superkoa(t.context.app) + .post('/jsonql') + .query({_cb: Date.now()}) + .set(thisHeader) + .send( + createQuery(HELLO_FN) + ); + t.is(200, res.status) +}) + +test("The public-contract.json file should contain issuer information", async t => { + let res = await superkoa(t.context.app) + .get('/jsonql') + .query({_cb: Date.now()}) + .set(thisHeader); + + t.truthy(res.body.data.auth.login) +}) + + +test('Should able to login with this credential', async t => { + let res = await superkoa(t.context.app) + .post('/jsonql') + .query({_cb: Date.now()}) + .set(thisHeader) + .send( + createQuery('login', ['nobody', myKey]) + ); + t.is(200, res.status) + t.is(bearer, res.body.data) +}) + +test('Should cause a JsonqlAuthorisationError if I pass the wrong username or password', async t => { + let res = await superkoa(t.context.app) + .post('/jsonql') + .query({_cb: Date.now()}) + .set(thisHeader) + .send( + createQuery('login', ['body-x', myKey]) + ); + + t.is(200, res.status) + t.truthy(res.body.error) +}) + + +test("It should able to call a method that is mark as public without login", async t => { + // alwaysAvailable + let res = await superkoa(t.context.app) + .post('/jsonql') + .query({_cb: Date.now()}) + .set(headers) + .send( + createQuery('alwaysAvailable') + ); + t.is(200, res.status) + t.is('Hello there', res.body.data) +}) + +test('Now I should able to call the api with credential', async t => { + let res = await superkoa(t.context.app) + .post('/jsonql') + .query({_cb: Date.now()}) + .set(merge({} , thisHeader, {Authorization: `Bearer ${bearer}`})) + .send( + createQuery('getUser', ['testing', 'userId']) + ); + t.is(200, res.status) + // debug(res.body) + t.is(1, res.body.data.userId) +}) + +test("Should NOT able to call the logout method without the Bearer", async t => { + let res = await superkoa(t.context.app) + .post('/jsonql') + .query({_cb: Date.now()}) + .set(merge({} , thisHeader)) + .send( + createQuery('logout') + ); + + t.is(200, res.status) + // should get an error object + t.truthy(res.body.error) + +}) diff --git a/packages/@jsonql/koa/tests/chain-fn.test.js b/packages/@jsonql/koa/tests/chain-fn.test.js new file mode 100644 index 0000000000000000000000000000000000000000..7c8962e07b9d2fa6bdad3b07e526bc4108c53361 --- /dev/null +++ b/packages/@jsonql/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/@jsonql/koa/tests/config.test.js b/packages/@jsonql/koa/tests/config.test.js new file mode 100644 index 0000000000000000000000000000000000000000..f9ea2ac14d125704f74e9ac176cf56668157ed71 --- /dev/null +++ b/packages/@jsonql/koa/tests/config.test.js @@ -0,0 +1,50 @@ +// testing the config +const test = require('ava') +const fsx = require('fs-extra') +const { join, resolve } = require('path') +const configCheck = require('../src/lib/config-check') +const { processJwtKeys } = require('../src/lib') +const { jwtProcessKey } = require('../src/lib/config-check/options') +const debug = require('debug')('jsonql-koa:test:config') +const resolverDir = join(__dirname, 'fixtures', 'resolvers') +const contractDir = join(__dirname, 'fixtures', 'tmp', 'config-test') +const keysDir = join(__dirname, 'fixtures', 'tmp', 'keys') + +// mocking a ctx object +let ctx = { + state: { + jsonql: { + setter: () => {}, + getter: () => {} + } + } +} + +test.after( t => { + // fsx.removeSync(keysDir) +}) + +test('It should able to check the in dir', t => { + const opts = configCheck({ + resolverDir, + contractDir + }) + + const dir = resolve(resolverDir) + t.is( dir, opts.resolverDir ) +}); + +test('It should have privateKey and publicKey when set useJwt = true', async t => { + let opts = configCheck({ + enableAuth: true, + useJwt: true, + resolverDir, + contractDir, + keysDir + }) + + opts = await processJwtKeys(ctx, opts) + + t.truthy(opts.privateKey && opts.publicKey) + +}) diff --git a/packages/@jsonql/koa/tests/contract.test.js b/packages/@jsonql/koa/tests/contract.test.js new file mode 100644 index 0000000000000000000000000000000000000000..40b2466d0d30dca5a56b3635ff82f14aadb1509f --- /dev/null +++ b/packages/@jsonql/koa/tests/contract.test.js @@ -0,0 +1,28 @@ +// separate test for the contract interface +const test = require('ava') +const { join } = require('path') +const fsx = require('fs-extra') +const generator = require('../src/lib/contract-generator') +const { isContractJson } = require('../src/lib') +const debug = require('debug')('jsonql-koa:test:gen') +const contractDir = join(__dirname, 'fixtures', 'tmp', 'generator') +const resolverDir = join(__dirname, 'fixtures', 'resolvers') + +test.before(t => { + t.context.initContract = generator({ + contractDir, + resolverDir, + returnAs: 'json' + }) +}) + +test.after( t => { + fsx.removeSync(contractDir) +}) + +test('It should able to generate a contract file', async t => { + // this way can test if this setup works for the middleware as well + const contract = await t.context.initContract; + debug(contract) + t.truthy(isContractJson(contract)) +}) diff --git a/packages/@jsonql/koa/tests/contractWithAuth.test.js b/packages/@jsonql/koa/tests/contractWithAuth.test.js new file mode 100644 index 0000000000000000000000000000000000000000..beb345c8e20686b9df508d93af58485782596349 --- /dev/null +++ b/packages/@jsonql/koa/tests/contractWithAuth.test.js @@ -0,0 +1,62 @@ +// we test all the fail scenario here +const test = require('ava') + +const superkoa = require('superkoa') +const { join } = require('path') +const debug = require('debug')('jsonql-koa:test:fail') +const { merge } = require('lodash') +const { type, headers, dirs, returns, contractKeyName } = require('./fixtures/options') +const fsx = require('fs-extra') +const myKey = '4670994sdfkl'; +const expired = Date.now() + 60*24*365*1000; +const createServer = require('./helpers/server') +const dir = 'withauth'; + +test.before((t) => { + + t.context.app = createServer({ + expired, + contractKey: myKey + }, dir) +}) + +test.after( () => { + fsx.removeSync(join(dirs.contractDir, dir)) + +}) + + +test("It should Not return a contract", async (t) => { + let res = await superkoa(t.context.app) + .get('/jsonql') + .query({_cb: Date.now()}) + .set(headers) + + t.is(401, res.body.error.statusCode) +}) + + +test("It should able to get contract with a key", async (t) => { + + let res = await superkoa(t.context.app) + .get('/jsonql') + .query({_cb: Date.now()}) + .set(merge({}, headers, { + [contractKeyName]: myKey + })) + + t.is(200, res.status) + t.is(res.body.data.expired, expired) + t.deepEqual(returns, res.body.data.query.helloWorld.returns) +}) + +test("It should fail to get contract if we provide the wrong password", async t => { + let res = await superkoa(t.context.app) + .get('/jsonql') + .query({_cb: Date.now()}) + .set(merge({}, headers, { + [contractKeyName]: 'what-ever-that-key' + })) + + t.is(401, res.body.error.statusCode) +}) diff --git a/packages/@jsonql/koa/tests/es6-module.test.js b/packages/@jsonql/koa/tests/es6-module.test.js new file mode 100644 index 0000000000000000000000000000000000000000..b4255a08f9951a8f91e65bcb7db9890ec1f91807 --- /dev/null +++ b/packages/@jsonql/koa/tests/es6-module.test.js @@ -0,0 +1,72 @@ +// testing the new ES6 modules import +const test = require('ava') +const { join } = require('path') +const fsx = require('fs-extra') +const superkoa = require('superkoa') +const { + DEFAULT_RESOLVER_LIST_FILE_NAME, + DEFAULT_RESOLVER_IMPORT_FILE_NAME, + HELLO_FN +} = require('jsonql-constants') +const { createQuery } = require('jsonql-params-validator') +const debug = require('debug')('jsonql-koa:test:es6') + +const { headers } = require('./fixtures/options') +const createServer = require('./helpers/server') + +const esResolverDir = join(__dirname, 'fixtures', 'es') +const esContractDir = join(__dirname, 'fixtures', 'tmp', 'es') + +const dir = 'es6'; + +test.before( t => { + t.context.app = createServer({ + resolverDir: esResolverDir, + contractDir: esContractDir + }, dir) +}) + +test.after( t => { + fsx.removeSync( esContractDir ) + fsx.removeSync( join(esResolverDir, DEFAULT_RESOLVER_LIST_FILE_NAME) ) + fsx.removeSync( join(esResolverDir, DEFAULT_RESOLVER_IMPORT_FILE_NAME) ) +}) + +test('It should able to generate the contract with es6 modules', async t => { + let res = await superkoa(t.context.app) + .post('/jsonql') + .query({_cb: Date.now()}) + .set(headers) + .send( + createQuery(HELLO_FN) + ) + t.is(200, res.status) + t.true( fsx.existsSync( join(esContractDir, 'contract.json') ) ) +}) + +test('It should able to handle a ES6 function', async t => { + let res = await superkoa(t.context.app) + .post('/jsonql') + .query({_cb: Date.now()}) + .set(headers) + .send( + createQuery('getSomething') + ) + t.is(200, res.status) + + t.true(Array.isArray(res.body.data)) + +}) + +test('It should able to serve up the correct public contract', async t => { + + let res = await superkoa(t.context.app) + .get('/jsonql') + .query({_cb: Date.now()}) + .set(headers) + + t.is(200, res.status) + + t.truthy(res.body.data.timestamp) + +}) diff --git a/packages/@jsonql/koa/tests/fail.test.js b/packages/@jsonql/koa/tests/fail.test.js new file mode 100644 index 0000000000000000000000000000000000000000..c7a6acd6b05423220d53529bc3f2c8c348a504ef --- /dev/null +++ b/packages/@jsonql/koa/tests/fail.test.js @@ -0,0 +1,32 @@ +// we test all the fail scenario here +const test = require('ava') +const superkoa = require('superkoa') +const { join } = require('path') +const debug = require('debug')('jsonql-koa:test:fail') +const jsonqlMiddleware = require(join(__dirname, '..', 'index')) +const { type, headers, dirs } = require('./fixtures/options') +const fsx = require('fs-extra') + +const createServer = require('./helpers/server') + +test.before((t) => { + t.context.app = createServer({}, 'failed'); +}) + +test.after( () => { + // remove the files after + fsx.removeSync(join(dirs.contractDir, 'failed')) +}) + +test("Should fail this Hello world test", async t => { + let res = await superkoa(t.context.app) + .post('/jsonql/somethingelse?_cb=9082390483204830') + .set(headers) + .send({ + helloWorld: { + args: {} + } + }) + t.is(404, res.status) + +}) diff --git a/packages/@jsonql/koa/tests/fixtures/es/mutation/save-something.js b/packages/@jsonql/koa/tests/fixtures/es/mutation/save-something.js new file mode 100644 index 0000000000000000000000000000000000000000..ea30ca671bbfb46aa5caacefe000e8d0c9114a2b --- /dev/null +++ b/packages/@jsonql/koa/tests/fixtures/es/mutation/save-something.js @@ -0,0 +1,9 @@ + +/** + * @param {object} payload + * @param {object} condition + * @return {boolean} true on OK + */ +export default function saveSomething(payload, condition) { + return true; +} diff --git a/packages/@jsonql/koa/tests/fixtures/es/query/get-something.js b/packages/@jsonql/koa/tests/fixtures/es/query/get-something.js new file mode 100644 index 0000000000000000000000000000000000000000..9a5d7c8c86d5c44bac11228be7cb8436c85d60d9 --- /dev/null +++ b/packages/@jsonql/koa/tests/fixtures/es/query/get-something.js @@ -0,0 +1,8 @@ + + +/** + * @return {array} list of something + */ +export default function getSomething() { + return ['Hello there'] +} diff --git a/demo/basic/web/index.html b/packages/@jsonql/koa/tests/fixtures/html/index.html similarity index 31% rename from demo/basic/web/index.html rename to packages/@jsonql/koa/tests/fixtures/html/index.html index 74de2a9770da444c9f2e560ec46e71a35c02ae1c..d1fdcefb8fae3b9a5759efe6bdaad4f1dfabd1ba 100644 --- a/demo/basic/web/index.html +++ b/packages/@jsonql/koa/tests/fixtures/html/index.html @@ -1,12 +1,9 @@ - Jsonql demo hello world + dummy -

Hello world

-

- Open the json:ql web console -

+ dummy diff --git a/packages/@jsonql/koa/tests/fixtures/keys/privateKey.pem b/packages/@jsonql/koa/tests/fixtures/keys/privateKey.pem new file mode 100644 index 0000000000000000000000000000000000000000..52ceae92066efead75838933d8bca25eeb9666ac --- /dev/null +++ b/packages/@jsonql/koa/tests/fixtures/keys/privateKey.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQDfDqpnh8TceIRuemm8GWM6nvE6KumK/Lq+POrZqghgHpZa5zjv +wwjsJ2iK45zWIRpggMkSlQZWvnRRjj/TWfv7448qhhiTB7hmqV63XjfYXJ5OgTtN +fPW36ZQ48Ha0y4sjlU4gvSijHpnzrJ5yV/vjLLLp9WxTux4ColeZu2B/XQIDAQAB +AoGAEmLJFQOR7IJamCiq8oA9N6XGSH8lBPnUAr5OtWZYjmO3DQMmJE01PRH6gghE +8zmDTRUQfeGexiOovtg01p0CMhehwS8D8d0m01s43zQ77xVJuFAvuW1U1kER4Xze +tVkLEvvO9PcWpKUEmxYpDoCJXGIfXuHaSAVbVLYDKn2MEUECQQD4FeWlpkxSNQT9 +u6w01zR/byjXzUmibOP5zrpaEsDGIxxTlxc/7WJZlKLNybXUyZE8oHgepuefdcL0 +ybk6gvQpAkEA5ixdMtnsbUImJYNFrt5BbLzEU9eF76hovsOSjOc2eTUJHEeiXeDA +Q66WZwXNBf/CRrZdsAvBPMQcWzJLwp24FQJBAPaojtPMLEXwAS5l0ioXblLlqq4l +pfigW2qcaBv2WUSm1BsoNi2RUB/Q8K26x9bxMj4dLlELkW+yHkxT5J6QZUECQFRO +A4TQlOwfwmETB77Y4RW2viIHWqNBB7x3XYIGXclfR4r4IdxIqaMgmy34zfNYjgvg +V8hXRdu/6LLuZRlPM1ECQHUppZNG7WKP9F7ywAr33u3xD0+9MqsfqcgKPfP9VOxs +Lo8EdmjmB30lyyP/Cd1hzxb+BsJjGmxzU/DikGbWos8= +-----END RSA PRIVATE KEY----- diff --git a/packages/@jsonql/koa/tests/fixtures/keys/publicKey.pem b/packages/@jsonql/koa/tests/fixtures/keys/publicKey.pem new file mode 100644 index 0000000000000000000000000000000000000000..7bd2532afbeb5b3be8b9c9f275ffac7b32456d0d --- /dev/null +++ b/packages/@jsonql/koa/tests/fixtures/keys/publicKey.pem @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDfDqpnh8TceIRuemm8GWM6nvE6 +KumK/Lq+POrZqghgHpZa5zjvwwjsJ2iK45zWIRpggMkSlQZWvnRRjj/TWfv7448q +hhiTB7hmqV63XjfYXJ5OgTtNfPW36ZQ48Ha0y4sjlU4gvSijHpnzrJ5yV/vjLLLp +9WxTux4ColeZu2B/XQIDAQAB +-----END PUBLIC KEY----- diff --git a/packages/@jsonql/koa/tests/fixtures/options.js b/packages/@jsonql/koa/tests/fixtures/options.js new file mode 100644 index 0000000000000000000000000000000000000000..b8e5c8e0c27e7a8e2f0b5aca16a66dd865b19160 --- /dev/null +++ b/packages/@jsonql/koa/tests/fixtures/options.js @@ -0,0 +1,28 @@ +// reusable options +const { join } = require('path'); + +const { DEFAULT_HEADER, CONTRACT_KEY_NAME, CONTENT_TYPE } = require('jsonql-constants'); + +const type = CONTENT_TYPE; +const headers = DEFAULT_HEADER; +const contractKeyName = CONTRACT_KEY_NAME; + +const dirs = { + resolverDir: join(__dirname, 'resolvers'), + contractDir: join(__dirname, 'tmp') +}; + +const secret = 'A little cat is watching you'; + +const bearer = 'sofud89723904lksd98234230823jlkjklsdfds'; + +const returns = [ + { + "type": "string", + "description": "stock message" + } +]; + +const dummy = 'yes I am dummy'; + +module.exports = { type, headers, dirs, secret, bearer, returns, contractKeyName, dummy }; diff --git a/packages/@jsonql/koa/tests/fixtures/resolvers/auth/custom-login.js b/packages/@jsonql/koa/tests/fixtures/resolvers/auth/custom-login.js new file mode 100644 index 0000000000000000000000000000000000000000..ca19bb40776218c621a9651ca90230e9e345bf97 --- /dev/null +++ b/packages/@jsonql/koa/tests/fixtures/resolvers/auth/custom-login.js @@ -0,0 +1,13 @@ + +/** + * This is a custom login method instead of the stock version + * @param {string} username username + * @param {string} password password + * @return {object} userdata payload + */ +module.exports = function customLogin(username, password) { + if (password === '123456') { + return {name: username} + } + throw new Error('Login failed!') +} diff --git a/packages/@jsonql/koa/tests/fixtures/resolvers/auth/custom-validator.js b/packages/@jsonql/koa/tests/fixtures/resolvers/auth/custom-validator.js new file mode 100644 index 0000000000000000000000000000000000000000..08d617a8ddf05b9c65d2e2df9555c8507163367d --- /dev/null +++ b/packages/@jsonql/koa/tests/fixtures/resolvers/auth/custom-validator.js @@ -0,0 +1,13 @@ +// this will use for testing the chaining validator methods +const debug = require('debug')('jsonql-koa:custom-validator') +const { dummy } = require('../../options') + +/** + * @param {*} userdata pass by the jsonql-jwt method + * @return {*} just pass it back + */ +module.exports = function(userdata) { + debug('I am being call by the chain') + userdata.dummy = dummy; + return userdata; +} diff --git a/packages/@jsonql/koa/tests/fixtures/resolvers/auth/login.js b/packages/@jsonql/koa/tests/fixtures/resolvers/auth/login.js new file mode 100644 index 0000000000000000000000000000000000000000..5f8d1e79a01e4d6095bd571c1e4f105bf5e39e34 --- /dev/null +++ b/packages/@jsonql/koa/tests/fixtures/resolvers/auth/login.js @@ -0,0 +1,18 @@ +const debug = require('debug')('jsonql:koa:issuer'); +const { bearer } = require('../../options'); + +// this is the stock method for giving out author header + +/** + * Auth method + * @param {string} username user name + * @param {string} password password + * @return {string|boolean} token on success, false on fail + */ +module.exports = function(username, password) { + debug('received this', username, password); + if (username === 'nobody' && password) { + return bearer; + } + return false; +} diff --git a/packages/@jsonql/koa/tests/fixtures/resolvers/auth/logout.js b/packages/@jsonql/koa/tests/fixtures/resolvers/auth/logout.js new file mode 100644 index 0000000000000000000000000000000000000000..d9b2af68d43af2b971b0ac20f808c6cdc5d744c1 --- /dev/null +++ b/packages/@jsonql/koa/tests/fixtures/resolvers/auth/logout.js @@ -0,0 +1,8 @@ +// complete the 3 pieces + +/** + * @return {boolean} just return something + */ +module.exports = function() { + return true; +}; diff --git a/packages/@jsonql/koa/tests/fixtures/resolvers/auth/validator.js b/packages/@jsonql/koa/tests/fixtures/resolvers/auth/validator.js new file mode 100644 index 0000000000000000000000000000000000000000..b588aa81ac6568822a903dd36386e08223ae3f57 --- /dev/null +++ b/packages/@jsonql/koa/tests/fixtures/resolvers/auth/validator.js @@ -0,0 +1,14 @@ +const debug = require('debug')('jsonql:koa:validator'); +const { bearer } = require('../../options'); +// this is the core method for checking the Auth header +/** + * @param {string} token jwt + * @return {object|boolean} user data on success, false on fail + */ +module.exports = function(token) { + debug('received this token', token, bearer); + if (token === bearer) { + return {userId: 1}; + } + return false; +} diff --git a/packages/@jsonql/koa/tests/fixtures/resolvers/mutation/update-list.js b/packages/@jsonql/koa/tests/fixtures/resolvers/mutation/update-list.js new file mode 100644 index 0000000000000000000000000000000000000000..bac225811b6ba84ae297b6b6afb3b57728eebcc9 --- /dev/null +++ b/packages/@jsonql/koa/tests/fixtures/resolvers/mutation/update-list.js @@ -0,0 +1,12 @@ +const debug = require('debug')('jsonql-koa:resolver:mutation:updateList'); +/** + * @param {object} payload + * @param {number} payload.user + * @param {object} condition + * @return {object} with user as key + */ +module.exports = function(payload, condition) { + debug('calling updateList with', payload, condition); + let p = payload.user + 1; + return {user: p}; +}; diff --git a/packages/@jsonql/koa/tests/fixtures/resolvers/mutation/update-ms-service.js b/packages/@jsonql/koa/tests/fixtures/resolvers/mutation/update-ms-service.js new file mode 100644 index 0000000000000000000000000000000000000000..62b1752475351de1e838f97b3dba1b76c0eb8b1e --- /dev/null +++ b/packages/@jsonql/koa/tests/fixtures/resolvers/mutation/update-ms-service.js @@ -0,0 +1,13 @@ +const debug = require('debug')('jsonql-koa:mutation:update-ms-service') +/** + * this will be calling a microserivce setup using the nodeClient + * @param {string} payload incoming + * @return {string} msg return from nodeClient + */ +module.exports = async function updateMsService(payload) { + const client = await updateMsService.client() + + debug(client) + + return client.query.subMsService(payload) +} diff --git a/packages/@jsonql/koa/tests/fixtures/resolvers/query/cause-error.js b/packages/@jsonql/koa/tests/fixtures/resolvers/query/cause-error.js new file mode 100644 index 0000000000000000000000000000000000000000..0a5eb534437227da7fdbb3b7c264b69a6ff879b5 --- /dev/null +++ b/packages/@jsonql/koa/tests/fixtures/resolvers/query/cause-error.js @@ -0,0 +1,10 @@ + +// this method will cause an error to throw +/** + * @param {*} x param + * @return {*} unknown + */ +module.exports = function(x) { + // none of the variable exists certainly will cause an error + return x ? y : z; +} diff --git a/packages/@jsonql/koa/tests/fixtures/resolvers/query/get-user.js b/packages/@jsonql/koa/tests/fixtures/resolvers/query/get-user.js new file mode 100644 index 0000000000000000000000000000000000000000..49b836f2159a20004a78aae04ff12700b98eaaa1 --- /dev/null +++ b/packages/@jsonql/koa/tests/fixtures/resolvers/query/get-user.js @@ -0,0 +1,12 @@ +// This is a new query method to test out if we actually get the userData props at the end of call +/** + * This use a spread as parameter + * @param {...string} args passing unknown number of param + * @return {any} extract from last of the args + */ +module.exports = function getUser(...args) { + const ctn = args.length; + const lastProp = args[ctn - 1]; + // also test with the assigned property + return getUser.userdata || {userId: 'dummy bear'}; +} diff --git a/packages/@jsonql/koa/tests/fixtures/resolvers/query/private/get-secret-msg.js b/packages/@jsonql/koa/tests/fixtures/resolvers/query/private/get-secret-msg.js new file mode 100644 index 0000000000000000000000000000000000000000..b655360dbeb227fc6a50c0405f83dc5fab07161b --- /dev/null +++ b/packages/@jsonql/koa/tests/fixtures/resolvers/query/private/get-secret-msg.js @@ -0,0 +1,9 @@ +// this resolver will not be include if the privateMethodDir is not set + +/** + * a hidden private method + * @return {string} a secret message + */ +module.exports = function getSecretMsg() { + return 'Let me tell ya a secret ...'; +} diff --git a/packages/@jsonql/koa/tests/fixtures/resolvers/query/public/always-available.js b/packages/@jsonql/koa/tests/fixtures/resolvers/query/public/always-available.js new file mode 100644 index 0000000000000000000000000000000000000000..0a08679841738e3b27e20b7a9553771869704a97 --- /dev/null +++ b/packages/@jsonql/koa/tests/fixtures/resolvers/query/public/always-available.js @@ -0,0 +1,7 @@ +/** + * This is a public method that is always available + * @return {string} a message + */ +module.exports = function() { + return 'Hello there'; +}; diff --git a/packages/@jsonql/koa/tests/fixtures/resolvers/query/test-list.js b/packages/@jsonql/koa/tests/fixtures/resolvers/query/test-list.js new file mode 100644 index 0000000000000000000000000000000000000000..b063eee68501e9a127353837a4a138c607e22951 --- /dev/null +++ b/packages/@jsonql/koa/tests/fixtures/resolvers/query/test-list.js @@ -0,0 +1,13 @@ +const debug = require('debug')('jsonql-koa:resolver:query:testList'); +/** + * @param {number} num a number + * @return {object} @TODO need to figure out how to give keys to the returns + */ +module.exports = function testList(num) { + debug('Call testList with this params', num); + return { + modified: Date.now(), + text: testList.userdata && testList.userdata.dummy ? testList.userdata.dummy : 'nope', + num: num ? --num : -1 + }; +}; diff --git a/packages/@jsonql/koa/tests/fixtures/sub/resolver/mutation/sub-update-ms-service.js b/packages/@jsonql/koa/tests/fixtures/sub/resolver/mutation/sub-update-ms-service.js new file mode 100644 index 0000000000000000000000000000000000000000..9da096d76e30733a278ed335a48dc1023d72bc24 --- /dev/null +++ b/packages/@jsonql/koa/tests/fixtures/sub/resolver/mutation/sub-update-ms-service.js @@ -0,0 +1,8 @@ +/** + * create a mutation to test why the call said the payload already declared + * @param {string} payload incoming + * @return {string} output + */ +module.exports = function subUpdateMsService(payload) { + return payload + ' updated'; +} diff --git a/packages/@jsonql/koa/tests/fixtures/sub/resolver/query/sub-ms-service.js b/packages/@jsonql/koa/tests/fixtures/sub/resolver/query/sub-ms-service.js new file mode 100644 index 0000000000000000000000000000000000000000..c8c0a047fa5884ae8af8996c361c332828a82daa --- /dev/null +++ b/packages/@jsonql/koa/tests/fixtures/sub/resolver/query/sub-ms-service.js @@ -0,0 +1,11 @@ +const debug = require('debug')('jsonql-koa:query:ms-service') +/** + * @param {string} msg incoming message + * @return {string} out going message + */ +module.exports = function subMsService(msg) { + + debug('msg', msg) + + return msg + ' ms service'; +} diff --git a/packages/@jsonql/koa/tests/helpers/browser.js b/packages/@jsonql/koa/tests/helpers/browser.js new file mode 100644 index 0000000000000000000000000000000000000000..b6217f2a40020d89764ab55c5d4c1bbf327dec42 --- /dev/null +++ b/packages/@jsonql/koa/tests/helpers/browser.js @@ -0,0 +1,20 @@ +// browser test the web console + +const serverIoCore = require('server-io-core') +const { join } = require('path') +const jsonqlKoa = require('../../') + +const baseDir = join(__dirname, '..', 'fixtures') + +serverIoCore({ + webroot: [ + join(baseDir, 'tmp', 'browser') + ], + middlewares: [ + jsonqlKoa({ + resolverDir: join(baseDir, 'resolvers'), + contractDir: join(baseDir, 'tmp', 'browser'), + enableWebConsole: true + }) + ] +}) diff --git a/packages/@jsonql/koa/tests/helpers/hello.js b/packages/@jsonql/koa/tests/helpers/hello.js new file mode 100644 index 0000000000000000000000000000000000000000..462b32e6e4d8fbee2727fe6c5a6323c2cbb9623d --- /dev/null +++ b/packages/@jsonql/koa/tests/helpers/hello.js @@ -0,0 +1,14 @@ +// wrapper function to test the server is running or not +const superkoa = require('superkoa') +const { headers } = require('../fixtures/options') +// export +module.exports = function hello(app) { + return superkoa(app) + .post('/jsonql') + .set(headers) + .send({ + helloWorld: { + args: [] + } + }); +} diff --git a/packages/@jsonql/koa/tests/helpers/server.js b/packages/@jsonql/koa/tests/helpers/server.js new file mode 100644 index 0000000000000000000000000000000000000000..2c20f0697608662097b90e6280cb5dd2edeac600 --- /dev/null +++ b/packages/@jsonql/koa/tests/helpers/server.js @@ -0,0 +1,21 @@ +// this will export the server for other test to use +// const test = require('ava'); +const Koa = require('koa') +const { join } = require('path') +const bodyparser = require('koa-bodyparser') +const jsonqlMiddleware = require(join(__dirname, '..', '..','index')) +const { type, headers, dirs } = require('../fixtures/options') +const fsx = require('fs-extra') +const myKey = '4670994sdfkl'; +// add a dir to seperate the contract files +module.exports = (config={}, dir = '') => { + const app = new Koa(); + app.use(bodyparser()); + app.use(jsonqlMiddleware( + Object.assign({},{ + resolverDir: dirs.resolverDir, + contractDir: join(dirs.contractDir, dir) + }, config) + )); + return app; +} diff --git a/packages/@jsonql/koa/tests/helpers/sub-server.js b/packages/@jsonql/koa/tests/helpers/sub-server.js new file mode 100644 index 0000000000000000000000000000000000000000..aef5991064f15350a6b66c678c98c9c78fb559d7 --- /dev/null +++ b/packages/@jsonql/koa/tests/helpers/sub-server.js @@ -0,0 +1,24 @@ +const { join } = require('path') +const fixturesDir = join(__dirname, '..', 'fixtures') + +const serverIoCore = require('server-io-core') +const jsonqlKoa = require('../../index') + +function startSubServer(msPort) { + return serverIoCore({ + webroot: join(fixturesDir, 'html'), + socket:false, + open:false, + debugger: false, + port: msPort, + middlewares: [ + jsonqlKoa({ + name: `server${msPort}`, + contractDir: join(fixturesDir, 'tmp', `sub-server-${msPort}`), + resolverDir: join(fixturesDir, 'sub', 'resolver') + }) + ] + }) +} + +module.exports = startSubServer diff --git a/packages/@jsonql/koa/tests/jsonp.test.js b/packages/@jsonql/koa/tests/jsonp.test.js new file mode 100644 index 0000000000000000000000000000000000000000..0fc1ca799912e5c53c3edef51154c979598c269a --- /dev/null +++ b/packages/@jsonql/koa/tests/jsonp.test.js @@ -0,0 +1,38 @@ +// testing the new jsonp methods +const test = require('ava') +const { JSONP_CALLBACK_NAME } = require('jsonql-constants') +const superkoa = require('superkoa') +const { join } = require('path') +const debug = require('debug')('jsonql-koa:test:jsonp') +const { type, headers, dirs, returns } = require('./fixtures/options') +const fsx = require('fs-extra') + +const { resolverDir, contractDir } = dirs; + +const createServer = require('./helpers/server') +const dir = 'standard'; + +test.before((t) => { + t.context.app = createServer({enableJsonp: true}, dir) +}) + +test.after( () => { + +}) +// @TODO this is not the proper JSONP need to think about it before continue with this feature +test('It should able to tell if this is access using jsonp', async t => { + let res = await superkoa(t.context.app) + .post('/jsonql') + .set(headers) + .query({ + _cb: Date.now(), + [JSONP_CALLBACK_NAME]: 'helloWorld' + }) + .send({ + args: [] + }) + // debug(res.body) + t.is(200, res.status) + t.is('Hello world!', res.body.data) + +}) diff --git a/packages/@jsonql/koa/tests/jwt-auth.test.js b/packages/@jsonql/koa/tests/jwt-auth.test.js new file mode 100644 index 0000000000000000000000000000000000000000..e3eacc10a0148546eb1f2c8d94279bfea8b82ac6 --- /dev/null +++ b/packages/@jsonql/koa/tests/jwt-auth.test.js @@ -0,0 +1,77 @@ +// this will use the enableAuth and useJwt options to test out all the new modules +const test = require('ava') + +const superkoa = require('superkoa'); +const { join } = require('path'); +const clientJwtDecode = require('jwt-decode') +const { jwtDecode } = require('jsonql-jwt') +const { merge } = require('lodash') +const fsx = require('fs-extra') +const { RSA_ALGO } = require('jsonql-constants') + +const debug = require('debug')('jsonql-koa:test:jwt'); + +const { type, headers, dirs, dummy } = require('./fixtures/options'); +const createServer = require('./helpers/server') +const dir = 'jwt'; +const keysDir = join(__dirname, 'fixtures', 'keys') + +const { createTokenValidator, loginResultToJwt } = require('jsonql-jwt') + +const name = 'Joel'; + +// start test +test.before( t => { + t.context.app = createServer({ + enableAuth: true, + useJwt: true, + loginHandlerName: 'customLogin', + validatorHandlerName: false, //'customValidator', + privateMethodDir: 'private', + keysDir + }, dir) + + const privateKey = fsx.readFileSync(join(keysDir, 'privateKey.pem')) + const options = { exp: 60*60 }; + + t.context.encoder = loginResultToJwt(privateKey, options, RSA_ALGO) + + t.context.token = t.context.encoder({ name }) + +}) + + +test('It should able to query the private method with token', async t => { + + let res = await superkoa(t.context.app) + .post('/jsonql') + .query({_cb: Date.now()}) + .set(merge({}, headers, {Authorization: `Bearer ${t.context.token}`})) + .send({ + testList: { + args: [667] + } + }) + + t.is(200, res.status) + let payload = res.body.data; + + t.is(666, payload.num) + t.is('nope', payload.text) +}) + +test('It should able to query a private method that is inside the private folder', async t => { + let res = await superkoa(t.context.app) + .post('/jsonql') + .query({_cb: Date.now()}) + .set(merge({}, headers, {Authorization: `Bearer ${t.context.token}`})) + .send({ + getSecretMsg: { + args: [] + } + }) + + t.is(200, res.status) + t.truthy( res.body.data.indexOf('secret') ) + +}) diff --git a/packages/@jsonql/koa/tests/jwt.test.js b/packages/@jsonql/koa/tests/jwt.test.js new file mode 100644 index 0000000000000000000000000000000000000000..2a928ecb2b7318f423b203533cc67546fce42644 --- /dev/null +++ b/packages/@jsonql/koa/tests/jwt.test.js @@ -0,0 +1,66 @@ +// this will use the enableAuth and useJwt options to test out all the new modules +const test = require('ava') + +const superkoa = require('superkoa'); +const { join } = require('path'); +const clientJwtDecode = require('jwt-decode') +const { jwtDecode } = require('jsonql-jwt') +const { merge } = require('lodash') +const fsx = require('fs-extra') +const { RSA_ALGO } = require('jsonql-constants') + +const debug = require('debug')('jsonql-koa:test:jwt'); + +const { type, headers, dirs } = require('./fixtures/options'); +const createServer = require('./helpers/server') +const dir = 'jwt'; +const keysDir = join(__dirname, 'fixtures', 'tmp', 'keys') + +const { createTokenValidator, loginResultToJwt } = require('jsonql-jwt') + +// start test +test.before( t => { + t.context.app = createServer({ + enableAuth: true, + useJwt: true, + jwtTokenOption: { exp: 60*60 }, + loginHandlerName: 'customLogin', + keysDir + }, dir) + + const publicKey = fsx.readFileSync(join(keysDir, 'publicKey.pem')) + + t.context.validator = createTokenValidator({ + publicKey, + useJwt: true + }) + +}) +// we need this one run first to get the token +test('It should able to provide a token that can be decode with the client side module', async t => { + let name = 'Joel'; + let res = await superkoa(t.context.app) + .post('/jsonql') + .query({_cb: Date.now()}) + .set(headers) + .send({ + customLogin: { + args: [name, '123456'] + } + }) + + t.is(200, res.status) + let token = res.body.data; + t.context.token = token; + + + let payload = clientJwtDecode(token) + t.is(payload.name, name) + t.truthy(payload.exp) + + let payload2 = t.context.validator(token) + + t.is(payload2.name, name) + t.truthy(payload2.exp) + +}) diff --git a/packages/@jsonql/koa/tests/koa.test.js b/packages/@jsonql/koa/tests/koa.test.js new file mode 100644 index 0000000000000000000000000000000000000000..037f90b9512c8b7cf3bf295ff8e408a9fb97e3b0 --- /dev/null +++ b/packages/@jsonql/koa/tests/koa.test.js @@ -0,0 +1,124 @@ +const test = require('ava') + +const { SHOW_CONTRACT_DESC_PARAM } = require('jsonql-constants') +const superkoa = require('superkoa') +const { join } = require('path') +const debug = require('debug')('jsonql-koa:test:koa') +const { type, headers, dirs, returns } = require('./fixtures/options') +const fsx = require('fs-extra') + +const { resolverDir, contractDir } = dirs; + +const createServer = require('./helpers/server') +const dir = 'standard'; + +const makeServer = function() { + return new Promise(resolver => { + const app = createServer({}, dir) + setTimeout(() => { + resolver(app) + }, 5000) + }) +} + +test.before( async (t) => { + t.context.app = await makeServer() +}) + +test.after( () => { + // remove the files after + fsx.removeSync(join(contractDir, dir)) +}) + +// start test +// @1.3.4 test if there is a public / private contract generated +test('It should create a contract and a public contract file at the same time', t => { + + const cd = join(contractDir, dir) + + t.truthy( fsx.existsSync(join(cd, 'contract.json')) ) + t.truthy( fsx.existsSync(join(cd, 'public-contract.json')) ) + +}) + +test("Hello world test", async t => { + let res = await superkoa(t.context.app) + .post('/jsonql') + .set(headers) + .send({ + helloWorld: { + args: [] + } + }); + // debug(res.body) + t.is(200, res.status) + t.is('Hello world!', res.body.data) +}) + +test("It should return a public contract with helloWorld", async (t) => { + let res = await superkoa(t.context.app) + .get('/jsonql') + .set(headers) + let contract = res.body.data; + + t.is(200, res.status) + + t.deepEqual(returns, contract.query.helloWorld.returns) + // it should not have a description field + t.falsy(contract.query.helloWorld.description) +}) + + +// start test +test('It should return json object',async (t) => { + let res = await superkoa(t.context.app) + .post('/jsonql') + .set(headers) + .send({ + testList: { + args: [2] + } + }); + t.is(200, res.status) + t.is(1, res.body.data.num) +}) + +test('It should change the json object', async(t) => { + let res = await superkoa(t.context.app) + .put('/jsonql') + .set(headers) + .send({ + updateList: { + payload: { + user: 1 + }, + condition: { + where: 'nothing' + } + } + }); + t.is(200, res.status) + t.is(2, res.body.data.user) +}) + +// test the web console +test("It should able see a dummy web console page", async t => { + let res = await superkoa(t.context.app) + .get('/jsonql') + t.is(200, res.status) +}) + +// for some reason if I run this as standalone then it works but run with other with +// the new test with both contract created failed +test.only("It should return a contract file without the description field", async t => { + let res = await superkoa(t.context.app) + .get('/jsonql') + .query(SHOW_CONTRACT_DESC_PARAM) + .set(headers) + let contract = res.body.data; + + debug(SHOW_CONTRACT_DESC_PARAM, contract.query) + + t.truthy(contract.query.helloWorld.description) + +}) diff --git a/packages/@jsonql/koa/tests/node-client.donttest.js b/packages/@jsonql/koa/tests/node-client.donttest.js new file mode 100644 index 0000000000000000000000000000000000000000..c749becfb686ee48801ba099c8ed97a42277fd3e --- /dev/null +++ b/packages/@jsonql/koa/tests/node-client.donttest.js @@ -0,0 +1,70 @@ +// testing the ms feature +const test = require('ava') +const { join } = require('path') +const fsx = require('fs-extra') +const debug = require('debug')('jsonql-koa:test:node-client') +const nodeClient = require('jsonql-node-client') +const serverIoCore = require('server-io-core') +// setup +const jsonqlKoa = require('../') +const hello = require('./helpers/hello') +const baseDir = join(__dirname, 'fixtures') +const clientContractDir = join(__dirname, 'fixtures', 'tmp', 'client6002') +const createServer = require('./helpers/server') + +const port = 6002; +const dir = 'server6002'; +const msPort = 8001; +const startSubServer = require('./helpers/sub-server') + +// base test setup +test.before(async t => { + + t.context.baseApp = createServer({ + name: `server${port}`, + clientConfig: [{ + hostname: `http://localhost:${msPort}`, + name: 'client0' + }] + }, dir) + t.context.baseServer = t.context.baseApp.listen(port) + + const { app, stop } = startSubServer(msPort) + + t.context.app = app + t.context.stop = stop +}) + +test.after(t => { + t.context.stop() + t.context.baseServer.close() + fsx.removeSync(clientContractDir) + fsx.removeSync(join(baseDir, 'tmp', dir)) +}) + +test.skip(`First test both server is running`, async t => { + const res1 = await hello(t.context.baseApp) + t.is(res1.status, 200) + const res2 = await hello(t.context.app) + t.is(res2.status, 200) +}) + +test.skip(`First test calling the 6001 directly with the mutation call`, async t => { + const client = await nodeClient({ + hostname: `http://localhost:${msPort}`, + contractDir: join(__dirname, 'fixtures', 'tmp', `client${msPort}`) + }) + const result = await client.query.subMsService('testing') + t.truthy(result.indexOf(`ms service`)) +}) + +test(`It should able to call a resolver that access another ms`, async t => { + // the problem now is calling the 6001 but received the 8001 public contract? + const client = await nodeClient({ + hostname: `http://localhost:${port}`, + contractDir: clientContractDir + }) + + const result = await client.mutation.updateMsService('testing') + t.truthy(result.indexOf(`ms service`)) +}) diff --git a/packages/@jsonql/koa/tests/resolverNotFound.test.js b/packages/@jsonql/koa/tests/resolverNotFound.test.js new file mode 100644 index 0000000000000000000000000000000000000000..19986ac0518f5e22d0b096c5b1874599cb665c32 --- /dev/null +++ b/packages/@jsonql/koa/tests/resolverNotFound.test.js @@ -0,0 +1,49 @@ +// this one will test the resolver not found and the application error + +const test = require('ava') +const createServer = require('./helpers/server') +const superkoa = require('superkoa') +const { headers } = require('./fixtures/options') +const debug = require('debug')('jsonql-koa:test:ResolverNotFoundError') +const fsx = require('fs-extra') +const { join } = require('path') + +test.before( t => { + t.context.app = createServer({}, 'notfound') +}) + +test.after( t => { + fsx.removeSync(join(__dirname, 'fixtures', 'tmp', 'notfound')) +}) + +test('it should throw a ResolverNotFoundError', async (t) => { + let res = await superkoa(t.context.app) + .post('/jsonql') + .set(headers) + .send({ + helloWorld2: { + args: [] + } + }) + + debug(res.body) + + t.is(res.body.error.statusCode, 404) + +}) + +test("It should cause an Application error but nothing throw", async t => { + let res = await superkoa(t.context.app) + .post('/jsonql') + .set(headers) + .send({ + causeError: { + args: [1] + } + }) + + debug(res.body) + + t.is(true, res.status === 200) + +}) diff --git a/packages/@jsonql/koa/tests/throw.test.js b/packages/@jsonql/koa/tests/throw.test.js new file mode 100644 index 0000000000000000000000000000000000000000..26e5aa71db9f387dc2e5610a30b91d22e3d5bd04 --- /dev/null +++ b/packages/@jsonql/koa/tests/throw.test.js @@ -0,0 +1,24 @@ +// this will only test all kind of throw error scenarios +const test = require('ava') +const superkoa = require('superkoa') +const jsonqlErrors = require('jsonql-errors') +// @TODO some of the idea already cover in other test so this one just keep here for now +const { + clientErrorsHandler, + getErrorByStatus, + JsonqlResolverNotFoundError +} = jsonqlErrors; + +test('Should able understand the Error being throw from clientErrorsHandler', async t => { + + const error = t.throws(() => { + return clientErrorsHandler({ + error: { + statusCode: 404, + className: 'JsonqlResolverNotFoundError' + } + }) + }, JsonqlResolverNotFoundError, 'Only give it a statusCode to find the error to throw') + + +}) diff --git a/packages/@jsonql/mqtt/src/example/README.md b/packages/@jsonql/mqtt/src/example/README.md new file mode 100644 index 0000000000000000000000000000000000000000..6b018f46d3d6b374603d0be685de3ea9d3d8c5ff --- /dev/null +++ b/packages/@jsonql/mqtt/src/example/README.md @@ -0,0 +1,11 @@ +# Example + +The first one is from IBM implementation of the MQTT over WebSocket. + +Therefore it will only support client that supports WebSocket. + +@TODO + +- Break up the files +- Put constants into our own constants module +- Look at how to do Authentication diff --git a/packages/@jsonql/mqtt/src/example/ibm.js b/packages/@jsonql/mqtt/src/example/ibm.js new file mode 100644 index 0000000000000000000000000000000000000000..63d4642988993ba03acd0255f504f15dac03a4bb --- /dev/null +++ b/packages/@jsonql/mqtt/src/example/ibm.js @@ -0,0 +1,2396 @@ +/******************************************************************************* + * Copyright (c) 2013 IBM Corp. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and Eclipse Distribution License v1.0 which accompany this distribution. + * + * The Eclipse Public License is available at + * http://www.eclipse.org/legal/epl-v10.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * Contributors: + * Andrew Banks - initial API and implementation and initial documentation + *******************************************************************************/ + + +// Only expose a single object name in the global namespace. +// Everything must go through this module. Global Paho module +// only has a single public function, client, which returns +// a Paho client object given connection details. + +/** + * Send and receive messages using web browsers. + *

+ * This programming interface lets a JavaScript client application use the MQTT V3.1 or + * V3.1.1 protocol to connect to an MQTT-supporting messaging server. + * + * The function supported includes: + *

    + *
  1. Connecting to and disconnecting from a server. The server is identified by its host name and port number. + *
  2. Specifying options that relate to the communications link with the server, + * for example the frequency of keep-alive heartbeats, and whether SSL/TLS is required. + *
  3. Subscribing to and receiving messages from MQTT Topics. + *
  4. Publishing messages to MQTT Topics. + *
+ *

+ * The API consists of two main objects: + *

+ *
{@link Paho.Client}
+ *
This contains methods that provide the functionality of the API, + * including provision of callbacks that notify the application when a message + * arrives from or is delivered to the messaging server, + * or when the status of its connection to the messaging server changes.
+ *
{@link Paho.Message}
+ *
This encapsulates the payload of the message along with various attributes + * associated with its delivery, in particular the destination to which it has + * been (or is about to be) sent.
+ *
+ *

+ * The programming interface validates parameters passed to it, and will throw + * an Error containing an error message intended for developer use, if it detects + * an error with any parameter. + *

+ * Example: + * + *

+var client = new Paho.MQTT.Client(location.hostname, Number(location.port), "clientId");
+client.onConnectionLost = onConnectionLost;
+client.onMessageArrived = onMessageArrived;
+client.connect({onSuccess:onConnect});
+
+function onConnect() {
+  // Once a connection has been made, make a subscription and send a message.
+  console.log("onConnect");
+  client.subscribe("/World");
+  var message = new Paho.MQTT.Message("Hello");
+  message.destinationName = "/World";
+  client.send(message);
+};
+function onConnectionLost(responseObject) {
+  if (responseObject.errorCode !== 0)
+	console.log("onConnectionLost:"+responseObject.errorMessage);
+};
+function onMessageArrived(message) {
+  console.log("onMessageArrived:"+message.payloadString);
+  client.disconnect();
+};
+ * 
+ * @namespace Paho + */ + +/* jshint shadow:true */ +(function ExportLibrary(root, factory) { + if(typeof exports === "object" && typeof module === "object"){ + module.exports = factory(); + } else if (typeof define === "function" && define.amd){ + define(factory); + } else if (typeof exports === "object"){ + exports = factory(); + } else { + //if (typeof root.Paho === "undefined"){ + // root.Paho = {}; + //} + root.Paho = factory(); + } +})(this, function LibraryFactory(){ + + + var PahoMQTT = (function (global) { + + // Private variables below, these are only visible inside the function closure + // which is used to define the module. + var version = "@VERSION@-@BUILDLEVEL@"; + + /** + * @private + */ + var localStorage = global.localStorage || (function () { + var data = {}; + + return { + setItem: function (key, item) { data[key] = item; }, + getItem: function (key) { return data[key]; }, + removeItem: function (key) { delete data[key]; }, + }; + })(); + + /** + * Unique message type identifiers, with associated + * associated integer values. + * @private + */ + var MESSAGE_TYPE = { + CONNECT: 1, + CONNACK: 2, + PUBLISH: 3, + PUBACK: 4, + PUBREC: 5, + PUBREL: 6, + PUBCOMP: 7, + SUBSCRIBE: 8, + SUBACK: 9, + UNSUBSCRIBE: 10, + UNSUBACK: 11, + PINGREQ: 12, + PINGRESP: 13, + DISCONNECT: 14 + }; + + // Collection of utility methods used to simplify module code + // and promote the DRY pattern. + + /** + * Validate an object's parameter names to ensure they + * match a list of expected variables name for this option + * type. Used to ensure option object passed into the API don't + * contain erroneous parameters. + * @param {Object} obj - User options object + * @param {Object} keys - valid keys and types that may exist in obj. + * @throws {Error} Invalid option parameter found. + * @private + */ + var validate = function(obj, keys) { + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + if (keys.hasOwnProperty(key)) { + if (typeof obj[key] !== keys[key]) + throw new Error(format(ERROR.INVALID_TYPE, [typeof obj[key], key])); + } else { + var errorStr = "Unknown property, " + key + ". Valid properties are:"; + for (var validKey in keys) + if (keys.hasOwnProperty(validKey)) + errorStr = errorStr+" "+validKey; + throw new Error(errorStr); + } + } + } + }; + + /** + * Return a new function which runs the user function bound + * to a fixed scope. + * @param {function} User function + * @param {object} Function scope + * @return {function} User function bound to another scope + * @private + */ + var scope = function (f, scope) { + return function () { + return f.apply(scope, arguments); + }; + }; + + /** + * Unique message type identifiers, with associated + * associated integer values. + * @private + */ + var ERROR = { + OK: {code:0, text:"AMQJSC0000I OK."}, + CONNECT_TIMEOUT: {code:1, text:"AMQJSC0001E Connect timed out."}, + SUBSCRIBE_TIMEOUT: {code:2, text:"AMQJS0002E Subscribe timed out."}, + UNSUBSCRIBE_TIMEOUT: {code:3, text:"AMQJS0003E Unsubscribe timed out."}, + PING_TIMEOUT: {code:4, text:"AMQJS0004E Ping timed out."}, + INTERNAL_ERROR: {code:5, text:"AMQJS0005E Internal error. Error Message: {0}, Stack trace: {1}"}, + CONNACK_RETURNCODE: {code:6, text:"AMQJS0006E Bad Connack return code:{0} {1}."}, + SOCKET_ERROR: {code:7, text:"AMQJS0007E Socket error:{0}."}, + SOCKET_CLOSE: {code:8, text:"AMQJS0008I Socket closed."}, + MALFORMED_UTF: {code:9, text:"AMQJS0009E Malformed UTF data:{0} {1} {2}."}, + UNSUPPORTED: {code:10, text:"AMQJS0010E {0} is not supported by this browser."}, + INVALID_STATE: {code:11, text:"AMQJS0011E Invalid state {0}."}, + INVALID_TYPE: {code:12, text:"AMQJS0012E Invalid type {0} for {1}."}, + INVALID_ARGUMENT: {code:13, text:"AMQJS0013E Invalid argument {0} for {1}."}, + UNSUPPORTED_OPERATION: {code:14, text:"AMQJS0014E Unsupported operation."}, + INVALID_STORED_DATA: {code:15, text:"AMQJS0015E Invalid data in local storage key={0} value={1}."}, + INVALID_MQTT_MESSAGE_TYPE: {code:16, text:"AMQJS0016E Invalid MQTT message type {0}."}, + MALFORMED_UNICODE: {code:17, text:"AMQJS0017E Malformed Unicode string:{0} {1}."}, + BUFFER_FULL: {code:18, text:"AMQJS0018E Message buffer is full, maximum buffer size: {0}."}, + }; + + /** CONNACK RC Meaning. */ + var CONNACK_RC = { + 0:"Connection Accepted", + 1:"Connection Refused: unacceptable protocol version", + 2:"Connection Refused: identifier rejected", + 3:"Connection Refused: server unavailable", + 4:"Connection Refused: bad user name or password", + 5:"Connection Refused: not authorized" + }; + + /** + * Format an error message text. + * @private + * @param {error} ERROR value above. + * @param {substitutions} [array] substituted into the text. + * @return the text with the substitutions made. + */ + var format = function(error, substitutions) { + var text = error.text; + if (substitutions) { + var field,start; + for (var i=0; i 0) { + var part1 = text.substring(0,start); + var part2 = text.substring(start+field.length); + text = part1+substitutions[i]+part2; + } + } + } + return text; + }; + + //MQTT protocol and version 6 M Q I s d p 3 + var MqttProtoIdentifierv3 = [0x00,0x06,0x4d,0x51,0x49,0x73,0x64,0x70,0x03]; + //MQTT proto/version for 311 4 M Q T T 4 + var MqttProtoIdentifierv4 = [0x00,0x04,0x4d,0x51,0x54,0x54,0x04]; + + /** + * Construct an MQTT wire protocol message. + * @param type MQTT packet type. + * @param options optional wire message attributes. + * + * Optional properties + * + * messageIdentifier: message ID in the range [0..65535] + * payloadMessage: Application Message - PUBLISH only + * connectStrings: array of 0 or more Strings to be put into the CONNECT payload + * topics: array of strings (SUBSCRIBE, UNSUBSCRIBE) + * requestQoS: array of QoS values [0..2] + * + * "Flag" properties + * cleanSession: true if present / false if absent (CONNECT) + * willMessage: true if present / false if absent (CONNECT) + * isRetained: true if present / false if absent (CONNECT) + * userName: true if present / false if absent (CONNECT) + * password: true if present / false if absent (CONNECT) + * keepAliveInterval: integer [0..65535] (CONNECT) + * + * @private + * @ignore + */ + var WireMessage = function (type, options) { + this.type = type; + for (var name in options) { + if (options.hasOwnProperty(name)) { + this[name] = options[name]; + } + } + }; + + WireMessage.prototype.encode = function() { + // Compute the first byte of the fixed header + var first = ((this.type & 0x0f) << 4); + + /* + * Now calculate the length of the variable header + payload by adding up the lengths + * of all the component parts + */ + + var remLength = 0; + var topicStrLength = []; + var destinationNameLength = 0; + var willMessagePayloadBytes; + + // if the message contains a messageIdentifier then we need two bytes for that + if (this.messageIdentifier !== undefined) + remLength += 2; + + switch(this.type) { + // If this a Connect then we need to include 12 bytes for its header + case MESSAGE_TYPE.CONNECT: + switch(this.mqttVersion) { + case 3: + remLength += MqttProtoIdentifierv3.length + 3; + break; + case 4: + remLength += MqttProtoIdentifierv4.length + 3; + break; + } + + remLength += UTF8Length(this.clientId) + 2; + if (this.willMessage !== undefined) { + remLength += UTF8Length(this.willMessage.destinationName) + 2; + // Will message is always a string, sent as UTF-8 characters with a preceding length. + willMessagePayloadBytes = this.willMessage.payloadBytes; + if (!(willMessagePayloadBytes instanceof Uint8Array)) + willMessagePayloadBytes = new Uint8Array(payloadBytes); + remLength += willMessagePayloadBytes.byteLength +2; + } + if (this.userName !== undefined) + remLength += UTF8Length(this.userName) + 2; + if (this.password !== undefined) + remLength += UTF8Length(this.password) + 2; + break; + + // Subscribe, Unsubscribe can both contain topic strings + case MESSAGE_TYPE.SUBSCRIBE: + first |= 0x02; // Qos = 1; + for ( var i = 0; i < this.topics.length; i++) { + topicStrLength[i] = UTF8Length(this.topics[i]); + remLength += topicStrLength[i] + 2; + } + remLength += this.requestedQos.length; // 1 byte for each topic's Qos + // QoS on Subscribe only + break; + + case MESSAGE_TYPE.UNSUBSCRIBE: + first |= 0x02; // Qos = 1; + for ( var i = 0; i < this.topics.length; i++) { + topicStrLength[i] = UTF8Length(this.topics[i]); + remLength += topicStrLength[i] + 2; + } + break; + + case MESSAGE_TYPE.PUBREL: + first |= 0x02; // Qos = 1; + break; + + case MESSAGE_TYPE.PUBLISH: + if (this.payloadMessage.duplicate) first |= 0x08; + first = first |= (this.payloadMessage.qos << 1); + if (this.payloadMessage.retained) first |= 0x01; + destinationNameLength = UTF8Length(this.payloadMessage.destinationName); + remLength += destinationNameLength + 2; + var payloadBytes = this.payloadMessage.payloadBytes; + remLength += payloadBytes.byteLength; + if (payloadBytes instanceof ArrayBuffer) + payloadBytes = new Uint8Array(payloadBytes); + else if (!(payloadBytes instanceof Uint8Array)) + payloadBytes = new Uint8Array(payloadBytes.buffer); + break; + + case MESSAGE_TYPE.DISCONNECT: + break; + + default: + break; + } + + // Now we can allocate a buffer for the message + + var mbi = encodeMBI(remLength); // Convert the length to MQTT MBI format + var pos = mbi.length + 1; // Offset of start of variable header + var buffer = new ArrayBuffer(remLength + pos); + var byteStream = new Uint8Array(buffer); // view it as a sequence of bytes + + //Write the fixed header into the buffer + byteStream[0] = first; + byteStream.set(mbi,1); + + // If this is a PUBLISH then the variable header starts with a topic + if (this.type == MESSAGE_TYPE.PUBLISH) + pos = writeString(this.payloadMessage.destinationName, destinationNameLength, byteStream, pos); + // If this is a CONNECT then the variable header contains the protocol name/version, flags and keepalive time + + else if (this.type == MESSAGE_TYPE.CONNECT) { + switch (this.mqttVersion) { + case 3: + byteStream.set(MqttProtoIdentifierv3, pos); + pos += MqttProtoIdentifierv3.length; + break; + case 4: + byteStream.set(MqttProtoIdentifierv4, pos); + pos += MqttProtoIdentifierv4.length; + break; + } + var connectFlags = 0; + if (this.cleanSession) + connectFlags = 0x02; + if (this.willMessage !== undefined ) { + connectFlags |= 0x04; + connectFlags |= (this.willMessage.qos<<3); + if (this.willMessage.retained) { + connectFlags |= 0x20; + } + } + if (this.userName !== undefined) + connectFlags |= 0x80; + if (this.password !== undefined) + connectFlags |= 0x40; + byteStream[pos++] = connectFlags; + pos = writeUint16 (this.keepAliveInterval, byteStream, pos); + } + + // Output the messageIdentifier - if there is one + if (this.messageIdentifier !== undefined) + pos = writeUint16 (this.messageIdentifier, byteStream, pos); + + switch(this.type) { + case MESSAGE_TYPE.CONNECT: + pos = writeString(this.clientId, UTF8Length(this.clientId), byteStream, pos); + if (this.willMessage !== undefined) { + pos = writeString(this.willMessage.destinationName, UTF8Length(this.willMessage.destinationName), byteStream, pos); + pos = writeUint16(willMessagePayloadBytes.byteLength, byteStream, pos); + byteStream.set(willMessagePayloadBytes, pos); + pos += willMessagePayloadBytes.byteLength; + + } + if (this.userName !== undefined) + pos = writeString(this.userName, UTF8Length(this.userName), byteStream, pos); + if (this.password !== undefined) + pos = writeString(this.password, UTF8Length(this.password), byteStream, pos); + break; + + case MESSAGE_TYPE.PUBLISH: + // PUBLISH has a text or binary payload, if text do not add a 2 byte length field, just the UTF characters. + byteStream.set(payloadBytes, pos); + + break; + + // case MESSAGE_TYPE.PUBREC: + // case MESSAGE_TYPE.PUBREL: + // case MESSAGE_TYPE.PUBCOMP: + // break; + + case MESSAGE_TYPE.SUBSCRIBE: + // SUBSCRIBE has a list of topic strings and request QoS + for (var i=0; i> 4; + var messageInfo = first &= 0x0f; + pos += 1; + + + // Decode the remaining length (MBI format) + + var digit; + var remLength = 0; + var multiplier = 1; + do { + if (pos == input.length) { + return [null,startingPos]; + } + digit = input[pos++]; + remLength += ((digit & 0x7F) * multiplier); + multiplier *= 128; + } while ((digit & 0x80) !== 0); + + var endPos = pos+remLength; + if (endPos > input.length) { + return [null,startingPos]; + } + + var wireMessage = new WireMessage(type); + switch(type) { + case MESSAGE_TYPE.CONNACK: + var connectAcknowledgeFlags = input[pos++]; + if (connectAcknowledgeFlags & 0x01) + wireMessage.sessionPresent = true; + wireMessage.returnCode = input[pos++]; + break; + + case MESSAGE_TYPE.PUBLISH: + var qos = (messageInfo >> 1) & 0x03; + + var len = readUint16(input, pos); + pos += 2; + var topicName = parseUTF8(input, pos, len); + pos += len; + // If QoS 1 or 2 there will be a messageIdentifier + if (qos > 0) { + wireMessage.messageIdentifier = readUint16(input, pos); + pos += 2; + } + + var message = new Message(input.subarray(pos, endPos)); + if ((messageInfo & 0x01) == 0x01) + message.retained = true; + if ((messageInfo & 0x08) == 0x08) + message.duplicate = true; + message.qos = qos; + message.destinationName = topicName; + wireMessage.payloadMessage = message; + break; + + case MESSAGE_TYPE.PUBACK: + case MESSAGE_TYPE.PUBREC: + case MESSAGE_TYPE.PUBREL: + case MESSAGE_TYPE.PUBCOMP: + case MESSAGE_TYPE.UNSUBACK: + wireMessage.messageIdentifier = readUint16(input, pos); + break; + + case MESSAGE_TYPE.SUBACK: + wireMessage.messageIdentifier = readUint16(input, pos); + pos += 2; + wireMessage.returnCode = input.subarray(pos, endPos); + break; + + default: + break; + } + + return [wireMessage,endPos]; + } + + function writeUint16(input, buffer, offset) { + buffer[offset++] = input >> 8; //MSB + buffer[offset++] = input % 256; //LSB + return offset; + } + + function writeString(input, utf8Length, buffer, offset) { + offset = writeUint16(utf8Length, buffer, offset); + stringToUTF8(input, buffer, offset); + return offset + utf8Length; + } + + function readUint16(buffer, offset) { + return 256*buffer[offset] + buffer[offset+1]; + } + + /** + * Encodes an MQTT Multi-Byte Integer + * @private + */ + function encodeMBI(number) { + var output = new Array(1); + var numBytes = 0; + + do { + var digit = number % 128; + number = number >> 7; + if (number > 0) { + digit |= 0x80; + } + output[numBytes++] = digit; + } while ( (number > 0) && (numBytes<4) ); + + return output; + } + + /** + * Takes a String and calculates its length in bytes when encoded in UTF8. + * @private + */ + function UTF8Length(input) { + var output = 0; + for (var i = 0; i 0x7FF) + { + // Surrogate pair means its a 4 byte character + if (0xD800 <= charCode && charCode <= 0xDBFF) + { + i++; + output++; + } + output +=3; + } + else if (charCode > 0x7F) + output +=2; + else + output++; + } + return output; + } + + /** + * Takes a String and writes it into an array as UTF8 encoded bytes. + * @private + */ + function stringToUTF8(input, output, start) { + var pos = start; + for (var i = 0; i>6 & 0x1F | 0xC0; + output[pos++] = charCode & 0x3F | 0x80; + } else if (charCode <= 0xFFFF) { + output[pos++] = charCode>>12 & 0x0F | 0xE0; + output[pos++] = charCode>>6 & 0x3F | 0x80; + output[pos++] = charCode & 0x3F | 0x80; + } else { + output[pos++] = charCode>>18 & 0x07 | 0xF0; + output[pos++] = charCode>>12 & 0x3F | 0x80; + output[pos++] = charCode>>6 & 0x3F | 0x80; + output[pos++] = charCode & 0x3F | 0x80; + } + } + return output; + } + + function parseUTF8(input, offset, length) { + var output = ""; + var utf16; + var pos = offset; + + while (pos < offset+length) + { + var byte1 = input[pos++]; + if (byte1 < 128) + utf16 = byte1; + else + { + var byte2 = input[pos++]-128; + if (byte2 < 0) + throw new Error(format(ERROR.MALFORMED_UTF, [byte1.toString(16), byte2.toString(16),""])); + if (byte1 < 0xE0) // 2 byte character + utf16 = 64*(byte1-0xC0) + byte2; + else + { + var byte3 = input[pos++]-128; + if (byte3 < 0) + throw new Error(format(ERROR.MALFORMED_UTF, [byte1.toString(16), byte2.toString(16), byte3.toString(16)])); + if (byte1 < 0xF0) // 3 byte character + utf16 = 4096*(byte1-0xE0) + 64*byte2 + byte3; + else + { + var byte4 = input[pos++]-128; + if (byte4 < 0) + throw new Error(format(ERROR.MALFORMED_UTF, [byte1.toString(16), byte2.toString(16), byte3.toString(16), byte4.toString(16)])); + if (byte1 < 0xF8) // 4 byte character + utf16 = 262144*(byte1-0xF0) + 4096*byte2 + 64*byte3 + byte4; + else // longer encodings are not supported + throw new Error(format(ERROR.MALFORMED_UTF, [byte1.toString(16), byte2.toString(16), byte3.toString(16), byte4.toString(16)])); + } + } + } + + if (utf16 > 0xFFFF) // 4 byte character - express as a surrogate pair + { + utf16 -= 0x10000; + output += String.fromCharCode(0xD800 + (utf16 >> 10)); // lead character + utf16 = 0xDC00 + (utf16 & 0x3FF); // trail character + } + output += String.fromCharCode(utf16); + } + return output; + } + + /** + * Repeat keepalive requests, monitor responses. + * @ignore + */ + var Pinger = function(client, keepAliveInterval) { + this._client = client; + this._keepAliveInterval = keepAliveInterval*1000; + this.isReset = false; + + var pingReq = new WireMessage(MESSAGE_TYPE.PINGREQ).encode(); + + var doTimeout = function (pinger) { + return function () { + return doPing.apply(pinger); + }; + }; + + /** @ignore */ + var doPing = function() { + if (!this.isReset) { + this._client._trace("Pinger.doPing", "Timed out"); + this._client._disconnected( ERROR.PING_TIMEOUT.code , format(ERROR.PING_TIMEOUT)); + } else { + this.isReset = false; + this._client._trace("Pinger.doPing", "send PINGREQ"); + this._client.socket.send(pingReq); + this.timeout = setTimeout(doTimeout(this), this._keepAliveInterval); + } + }; + + this.reset = function() { + this.isReset = true; + clearTimeout(this.timeout); + if (this._keepAliveInterval > 0) + this.timeout = setTimeout(doTimeout(this), this._keepAliveInterval); + }; + + this.cancel = function() { + clearTimeout(this.timeout); + }; + }; + + /** + * Monitor request completion. + * @ignore + */ + var Timeout = function(client, timeoutSeconds, action, args) { + if (!timeoutSeconds) + timeoutSeconds = 30; + + var doTimeout = function (action, client, args) { + return function () { + return action.apply(client, args); + }; + }; + this.timeout = setTimeout(doTimeout(action, client, args), timeoutSeconds * 1000); + + this.cancel = function() { + clearTimeout(this.timeout); + }; + }; + + /** + * Internal implementation of the Websockets MQTT V3.1 client. + * + * @name Paho.ClientImpl @constructor + * @param {String} host the DNS nameof the webSocket host. + * @param {Number} port the port number for that host. + * @param {String} clientId the MQ client identifier. + */ + var ClientImpl = function (uri, host, port, path, clientId) { + // Check dependencies are satisfied in this browser. + if (!("WebSocket" in global && global.WebSocket !== null)) { + throw new Error(format(ERROR.UNSUPPORTED, ["WebSocket"])); + } + if (!("ArrayBuffer" in global && global.ArrayBuffer !== null)) { + throw new Error(format(ERROR.UNSUPPORTED, ["ArrayBuffer"])); + } + this._trace("Paho.Client", uri, host, port, path, clientId); + + this.host = host; + this.port = port; + this.path = path; + this.uri = uri; + this.clientId = clientId; + this._wsuri = null; + + // Local storagekeys are qualified with the following string. + // The conditional inclusion of path in the key is for backward + // compatibility to when the path was not configurable and assumed to + // be /mqtt + this._localKey=host+":"+port+(path!="/mqtt"?":"+path:"")+":"+clientId+":"; + + // Create private instance-only message queue + // Internal queue of messages to be sent, in sending order. + this._msg_queue = []; + this._buffered_msg_queue = []; + + // Messages we have sent and are expecting a response for, indexed by their respective message ids. + this._sentMessages = {}; + + // Messages we have received and acknowleged and are expecting a confirm message for + // indexed by their respective message ids. + this._receivedMessages = {}; + + // Internal list of callbacks to be executed when messages + // have been successfully sent over web socket, e.g. disconnect + // when it doesn't have to wait for ACK, just message is dispatched. + this._notify_msg_sent = {}; + + // Unique identifier for SEND messages, incrementing + // counter as messages are sent. + this._message_identifier = 1; + + // Used to determine the transmission sequence of stored sent messages. + this._sequence = 0; + + + // Load the local state, if any, from the saved version, only restore state relevant to this client. + for (var key in localStorage) + if ( key.indexOf("Sent:"+this._localKey) === 0 || key.indexOf("Received:"+this._localKey) === 0) + this.restore(key); + }; + + // Messaging Client public instance members. + ClientImpl.prototype.host = null; + ClientImpl.prototype.port = null; + ClientImpl.prototype.path = null; + ClientImpl.prototype.uri = null; + ClientImpl.prototype.clientId = null; + + // Messaging Client private instance members. + ClientImpl.prototype.socket = null; + /* true once we have received an acknowledgement to a CONNECT packet. */ + ClientImpl.prototype.connected = false; + /* The largest message identifier allowed, may not be larger than 2**16 but + * if set smaller reduces the maximum number of outbound messages allowed. + */ + ClientImpl.prototype.maxMessageIdentifier = 65536; + ClientImpl.prototype.connectOptions = null; + ClientImpl.prototype.hostIndex = null; + ClientImpl.prototype.onConnected = null; + ClientImpl.prototype.onConnectionLost = null; + ClientImpl.prototype.onMessageDelivered = null; + ClientImpl.prototype.onMessageArrived = null; + ClientImpl.prototype.traceFunction = null; + ClientImpl.prototype._msg_queue = null; + ClientImpl.prototype._buffered_msg_queue = null; + ClientImpl.prototype._connectTimeout = null; + /* The sendPinger monitors how long we allow before we send data to prove to the server that we are alive. */ + ClientImpl.prototype.sendPinger = null; + /* The receivePinger monitors how long we allow before we require evidence that the server is alive. */ + ClientImpl.prototype.receivePinger = null; + ClientImpl.prototype._reconnectInterval = 1; // Reconnect Delay, starts at 1 second + ClientImpl.prototype._reconnecting = false; + ClientImpl.prototype._reconnectTimeout = null; + ClientImpl.prototype.disconnectedPublishing = false; + ClientImpl.prototype.disconnectedBufferSize = 5000; + + ClientImpl.prototype.receiveBuffer = null; + + ClientImpl.prototype._traceBuffer = null; + ClientImpl.prototype._MAX_TRACE_ENTRIES = 100; + + ClientImpl.prototype.connect = function (connectOptions) { + var connectOptionsMasked = this._traceMask(connectOptions, "password"); + this._trace("Client.connect", connectOptionsMasked, this.socket, this.connected); + + if (this.connected) + throw new Error(format(ERROR.INVALID_STATE, ["already connected"])); + if (this.socket) + throw new Error(format(ERROR.INVALID_STATE, ["already connected"])); + + if (this._reconnecting) { + // connect() function is called while reconnect is in progress. + // Terminate the auto reconnect process to use new connect options. + this._reconnectTimeout.cancel(); + this._reconnectTimeout = null; + this._reconnecting = false; + } + + this.connectOptions = connectOptions; + this._reconnectInterval = 1; + this._reconnecting = false; + if (connectOptions.uris) { + this.hostIndex = 0; + this._doConnect(connectOptions.uris[0]); + } else { + this._doConnect(this.uri); + } + + }; + + ClientImpl.prototype.subscribe = function (filter, subscribeOptions) { + this._trace("Client.subscribe", filter, subscribeOptions); + + if (!this.connected) + throw new Error(format(ERROR.INVALID_STATE, ["not connected"])); + + var wireMessage = new WireMessage(MESSAGE_TYPE.SUBSCRIBE); + wireMessage.topics = filter.constructor === Array ? filter : [filter]; + if (subscribeOptions.qos === undefined) + subscribeOptions.qos = 0; + wireMessage.requestedQos = []; + for (var i = 0; i < wireMessage.topics.length; i++) + wireMessage.requestedQos[i] = subscribeOptions.qos; + + if (subscribeOptions.onSuccess) { + wireMessage.onSuccess = function(grantedQos) {subscribeOptions.onSuccess({invocationContext:subscribeOptions.invocationContext,grantedQos:grantedQos});}; + } + + if (subscribeOptions.onFailure) { + wireMessage.onFailure = function(errorCode) {subscribeOptions.onFailure({invocationContext:subscribeOptions.invocationContext,errorCode:errorCode, errorMessage:format(errorCode)});}; + } + + if (subscribeOptions.timeout) { + wireMessage.timeOut = new Timeout(this, subscribeOptions.timeout, subscribeOptions.onFailure, + [{invocationContext:subscribeOptions.invocationContext, + errorCode:ERROR.SUBSCRIBE_TIMEOUT.code, + errorMessage:format(ERROR.SUBSCRIBE_TIMEOUT)}]); + } + + // All subscriptions return a SUBACK. + this._requires_ack(wireMessage); + this._schedule_message(wireMessage); + }; + + /** @ignore */ + ClientImpl.prototype.unsubscribe = function(filter, unsubscribeOptions) { + this._trace("Client.unsubscribe", filter, unsubscribeOptions); + + if (!this.connected) + throw new Error(format(ERROR.INVALID_STATE, ["not connected"])); + + var wireMessage = new WireMessage(MESSAGE_TYPE.UNSUBSCRIBE); + wireMessage.topics = filter.constructor === Array ? filter : [filter]; + + if (unsubscribeOptions.onSuccess) { + wireMessage.callback = function() {unsubscribeOptions.onSuccess({invocationContext:unsubscribeOptions.invocationContext});}; + } + if (unsubscribeOptions.timeout) { + wireMessage.timeOut = new Timeout(this, unsubscribeOptions.timeout, unsubscribeOptions.onFailure, + [{invocationContext:unsubscribeOptions.invocationContext, + errorCode:ERROR.UNSUBSCRIBE_TIMEOUT.code, + errorMessage:format(ERROR.UNSUBSCRIBE_TIMEOUT)}]); + } + + // All unsubscribes return a SUBACK. + this._requires_ack(wireMessage); + this._schedule_message(wireMessage); + }; + + ClientImpl.prototype.send = function (message) { + this._trace("Client.send", message); + + var wireMessage = new WireMessage(MESSAGE_TYPE.PUBLISH); + wireMessage.payloadMessage = message; + + if (this.connected) { + // Mark qos 1 & 2 message as "ACK required" + // For qos 0 message, invoke onMessageDelivered callback if there is one. + // Then schedule the message. + if (message.qos > 0) { + this._requires_ack(wireMessage); + } else if (this.onMessageDelivered) { + this._notify_msg_sent[wireMessage] = this.onMessageDelivered(wireMessage.payloadMessage); + } + this._schedule_message(wireMessage); + } else { + // Currently disconnected, will not schedule this message + // Check if reconnecting is in progress and disconnected publish is enabled. + if (this._reconnecting && this.disconnectedPublishing) { + // Check the limit which include the "required ACK" messages + var messageCount = Object.keys(this._sentMessages).length + this._buffered_msg_queue.length; + if (messageCount > this.disconnectedBufferSize) { + throw new Error(format(ERROR.BUFFER_FULL, [this.disconnectedBufferSize])); + } else { + if (message.qos > 0) { + // Mark this message as "ACK required" + this._requires_ack(wireMessage); + } else { + wireMessage.sequence = ++this._sequence; + // Add messages in fifo order to array, by adding to start + this._buffered_msg_queue.unshift(wireMessage); + } + } + } else { + throw new Error(format(ERROR.INVALID_STATE, ["not connected"])); + } + } + }; + + ClientImpl.prototype.disconnect = function () { + this._trace("Client.disconnect"); + + if (this._reconnecting) { + // disconnect() function is called while reconnect is in progress. + // Terminate the auto reconnect process. + this._reconnectTimeout.cancel(); + this._reconnectTimeout = null; + this._reconnecting = false; + } + + if (!this.socket) + throw new Error(format(ERROR.INVALID_STATE, ["not connecting or connected"])); + + var wireMessage = new WireMessage(MESSAGE_TYPE.DISCONNECT); + + // Run the disconnected call back as soon as the message has been sent, + // in case of a failure later on in the disconnect processing. + // as a consequence, the _disconected call back may be run several times. + this._notify_msg_sent[wireMessage] = scope(this._disconnected, this); + + this._schedule_message(wireMessage); + }; + + ClientImpl.prototype.getTraceLog = function () { + if ( this._traceBuffer !== null ) { + this._trace("Client.getTraceLog", new Date()); + this._trace("Client.getTraceLog in flight messages", this._sentMessages.length); + for (var key in this._sentMessages) + this._trace("_sentMessages ",key, this._sentMessages[key]); + for (var key in this._receivedMessages) + this._trace("_receivedMessages ",key, this._receivedMessages[key]); + + return this._traceBuffer; + } + }; + + ClientImpl.prototype.startTrace = function () { + if ( this._traceBuffer === null ) { + this._traceBuffer = []; + } + this._trace("Client.startTrace", new Date(), version); + }; + + ClientImpl.prototype.stopTrace = function () { + delete this._traceBuffer; + }; + + ClientImpl.prototype._doConnect = function (wsurl) { + // When the socket is open, this client will send the CONNECT WireMessage using the saved parameters. + if (this.connectOptions.useSSL) { + var uriParts = wsurl.split(":"); + uriParts[0] = "wss"; + wsurl = uriParts.join(":"); + } + this._wsuri = wsurl; + this.connected = false; + + + + if (this.connectOptions.mqttVersion < 4) { + this.socket = new WebSocket(wsurl, ["mqttv3.1"]); + } else { + this.socket = new WebSocket(wsurl, ["mqtt"]); + } + this.socket.binaryType = "arraybuffer"; + this.socket.onopen = scope(this._on_socket_open, this); + this.socket.onmessage = scope(this._on_socket_message, this); + this.socket.onerror = scope(this._on_socket_error, this); + this.socket.onclose = scope(this._on_socket_close, this); + + this.sendPinger = new Pinger(this, this.connectOptions.keepAliveInterval); + this.receivePinger = new Pinger(this, this.connectOptions.keepAliveInterval); + if (this._connectTimeout) { + this._connectTimeout.cancel(); + this._connectTimeout = null; + } + this._connectTimeout = new Timeout(this, this.connectOptions.timeout, this._disconnected, [ERROR.CONNECT_TIMEOUT.code, format(ERROR.CONNECT_TIMEOUT)]); + }; + + + // Schedule a new message to be sent over the WebSockets + // connection. CONNECT messages cause WebSocket connection + // to be started. All other messages are queued internally + // until this has happened. When WS connection starts, process + // all outstanding messages. + ClientImpl.prototype._schedule_message = function (message) { + // Add messages in fifo order to array, by adding to start + this._msg_queue.unshift(message); + // Process outstanding messages in the queue if we have an open socket, and have received CONNACK. + if (this.connected) { + this._process_queue(); + } + }; + + ClientImpl.prototype.store = function(prefix, wireMessage) { + var storedMessage = {type:wireMessage.type, messageIdentifier:wireMessage.messageIdentifier, version:1}; + + switch(wireMessage.type) { + case MESSAGE_TYPE.PUBLISH: + if(wireMessage.pubRecReceived) + storedMessage.pubRecReceived = true; + + // Convert the payload to a hex string. + storedMessage.payloadMessage = {}; + var hex = ""; + var messageBytes = wireMessage.payloadMessage.payloadBytes; + for (var i=0; i= 2) { + var x = parseInt(hex.substring(0, 2), 16); + hex = hex.substring(2, hex.length); + byteStream[i++] = x; + } + var payloadMessage = new Message(byteStream); + + payloadMessage.qos = storedMessage.payloadMessage.qos; + payloadMessage.destinationName = storedMessage.payloadMessage.destinationName; + if (storedMessage.payloadMessage.duplicate) + payloadMessage.duplicate = true; + if (storedMessage.payloadMessage.retained) + payloadMessage.retained = true; + wireMessage.payloadMessage = payloadMessage; + + break; + + default: + throw Error(format(ERROR.INVALID_STORED_DATA, [key, value])); + } + + if (key.indexOf("Sent:"+this._localKey) === 0) { + wireMessage.payloadMessage.duplicate = true; + this._sentMessages[wireMessage.messageIdentifier] = wireMessage; + } else if (key.indexOf("Received:"+this._localKey) === 0) { + this._receivedMessages[wireMessage.messageIdentifier] = wireMessage; + } + }; + + ClientImpl.prototype._process_queue = function () { + var message = null; + + // Send all queued messages down socket connection + while ((message = this._msg_queue.pop())) { + this._socket_send(message); + // Notify listeners that message was successfully sent + if (this._notify_msg_sent[message]) { + this._notify_msg_sent[message](); + delete this._notify_msg_sent[message]; + } + } + }; + + /** + * Expect an ACK response for this message. Add message to the set of in progress + * messages and set an unused identifier in this message. + * @ignore + */ + ClientImpl.prototype._requires_ack = function (wireMessage) { + var messageCount = Object.keys(this._sentMessages).length; + if (messageCount > this.maxMessageIdentifier) + throw Error ("Too many messages:"+messageCount); + + while(this._sentMessages[this._message_identifier] !== undefined) { + this._message_identifier++; + } + wireMessage.messageIdentifier = this._message_identifier; + this._sentMessages[wireMessage.messageIdentifier] = wireMessage; + if (wireMessage.type === MESSAGE_TYPE.PUBLISH) { + this.store("Sent:", wireMessage); + } + if (this._message_identifier === this.maxMessageIdentifier) { + this._message_identifier = 1; + } + }; + + /** + * Called when the underlying websocket has been opened. + * @ignore + */ + ClientImpl.prototype._on_socket_open = function () { + // Create the CONNECT message object. + var wireMessage = new WireMessage(MESSAGE_TYPE.CONNECT, this.connectOptions); + wireMessage.clientId = this.clientId; + this._socket_send(wireMessage); + }; + + /** + * Called when the underlying websocket has received a complete packet. + * @ignore + */ + ClientImpl.prototype._on_socket_message = function (event) { + this._trace("Client._on_socket_message", event.data); + var messages = this._deframeMessages(event.data); + for (var i = 0; i < messages.length; i+=1) { + this._handleMessage(messages[i]); + } + }; + + ClientImpl.prototype._deframeMessages = function(data) { + var byteArray = new Uint8Array(data); + var messages = []; + if (this.receiveBuffer) { + var newData = new Uint8Array(this.receiveBuffer.length+byteArray.length); + newData.set(this.receiveBuffer); + newData.set(byteArray,this.receiveBuffer.length); + byteArray = newData; + delete this.receiveBuffer; + } + try { + var offset = 0; + while(offset < byteArray.length) { + var result = decodeMessage(byteArray,offset); + var wireMessage = result[0]; + offset = result[1]; + if (wireMessage !== null) { + messages.push(wireMessage); + } else { + break; + } + } + if (offset < byteArray.length) { + this.receiveBuffer = byteArray.subarray(offset); + } + } catch (error) { + var errorStack = ((error.hasOwnProperty("stack") == "undefined") ? error.stack.toString() : "No Error Stack Available"); + this._disconnected(ERROR.INTERNAL_ERROR.code , format(ERROR.INTERNAL_ERROR, [error.message,errorStack])); + return; + } + return messages; + }; + + ClientImpl.prototype._handleMessage = function(wireMessage) { + + this._trace("Client._handleMessage", wireMessage); + + try { + switch(wireMessage.type) { + case MESSAGE_TYPE.CONNACK: + this._connectTimeout.cancel(); + if (this._reconnectTimeout) + this._reconnectTimeout.cancel(); + + // If we have started using clean session then clear up the local state. + if (this.connectOptions.cleanSession) { + for (var key in this._sentMessages) { + var sentMessage = this._sentMessages[key]; + localStorage.removeItem("Sent:"+this._localKey+sentMessage.messageIdentifier); + } + this._sentMessages = {}; + + for (var key in this._receivedMessages) { + var receivedMessage = this._receivedMessages[key]; + localStorage.removeItem("Received:"+this._localKey+receivedMessage.messageIdentifier); + } + this._receivedMessages = {}; + } + // Client connected and ready for business. + if (wireMessage.returnCode === 0) { + + this.connected = true; + // Jump to the end of the list of uris and stop looking for a good host. + + if (this.connectOptions.uris) + this.hostIndex = this.connectOptions.uris.length; + + } else { + this._disconnected(ERROR.CONNACK_RETURNCODE.code , format(ERROR.CONNACK_RETURNCODE, [wireMessage.returnCode, CONNACK_RC[wireMessage.returnCode]])); + break; + } + + // Resend messages. + var sequencedMessages = []; + for (var msgId in this._sentMessages) { + if (this._sentMessages.hasOwnProperty(msgId)) + sequencedMessages.push(this._sentMessages[msgId]); + } + + // Also schedule qos 0 buffered messages if any + if (this._buffered_msg_queue.length > 0) { + var msg = null; + while ((msg = this._buffered_msg_queue.pop())) { + sequencedMessages.push(msg); + if (this.onMessageDelivered) + this._notify_msg_sent[msg] = this.onMessageDelivered(msg.payloadMessage); + } + } + + // Sort sentMessages into the original sent order. + var sequencedMessages = sequencedMessages.sort(function(a,b) {return a.sequence - b.sequence;} ); + for (var i=0, len=sequencedMessages.length; i + * Most applications will create just one Client object and then call its connect() method, + * however applications can create more than one Client object if they wish. + * In this case the combination of host, port and clientId attributes must be different for each Client object. + *

+ * The send, subscribe and unsubscribe methods are implemented as asynchronous JavaScript methods + * (even though the underlying protocol exchange might be synchronous in nature). + * This means they signal their completion by calling back to the application, + * via Success or Failure callback functions provided by the application on the method in question. + * Such callbacks are called at most once per method invocation and do not persist beyond the lifetime + * of the script that made the invocation. + *

+ * In contrast there are some callback functions, most notably onMessageArrived, + * that are defined on the {@link Paho.Client} object. + * These may get called multiple times, and aren't directly related to specific method invocations made by the client. + * + * @name Paho.Client + * + * @constructor + * + * @param {string} host - the address of the messaging server, as a fully qualified WebSocket URI, as a DNS name or dotted decimal IP address. + * @param {number} port - the port number to connect to - only required if host is not a URI + * @param {string} path - the path on the host to connect to - only used if host is not a URI. Default: '/mqtt'. + * @param {string} clientId - the Messaging client identifier, between 1 and 23 characters in length. + * + * @property {string} host - read only the server's DNS hostname or dotted decimal IP address. + * @property {number} port - read only the server's port. + * @property {string} path - read only the server's path. + * @property {string} clientId - read only used when connecting to the server. + * @property {function} onConnectionLost - called when a connection has been lost. + * after a connect() method has succeeded. + * Establish the call back used when a connection has been lost. The connection may be + * lost because the client initiates a disconnect or because the server or network + * cause the client to be disconnected. The disconnect call back may be called without + * the connectionComplete call back being invoked if, for example the client fails to + * connect. + * A single response object parameter is passed to the onConnectionLost callback containing the following fields: + *

    + *
  1. errorCode + *
  2. errorMessage + *
+ * @property {function} onMessageDelivered - called when a message has been delivered. + * All processing that this Client will ever do has been completed. So, for example, + * in the case of a Qos=2 message sent by this client, the PubComp flow has been received from the server + * and the message has been removed from persistent storage before this callback is invoked. + * Parameters passed to the onMessageDelivered callback are: + *
    + *
  1. {@link Paho.Message} that was delivered. + *
+ * @property {function} onMessageArrived - called when a message has arrived in this Paho.client. + * Parameters passed to the onMessageArrived callback are: + *
    + *
  1. {@link Paho.Message} that has arrived. + *
+ * @property {function} onConnected - called when a connection is successfully made to the server. + * after a connect() method. + * Parameters passed to the onConnected callback are: + *
    + *
  1. reconnect (boolean) - If true, the connection was the result of a reconnect.
  2. + *
  3. URI (string) - The URI used to connect to the server.
  4. + *
+ * @property {boolean} disconnectedPublishing - if set, will enable disconnected publishing in + * in the event that the connection to the server is lost. + * @property {number} disconnectedBufferSize - Used to set the maximum number of messages that the disconnected + * buffer will hold before rejecting new messages. Default size: 5000 messages + * @property {function} trace - called whenever trace is called. TODO + */ + var Client = function (host, port, path, clientId) { + + var uri; + + if (typeof host !== "string") + throw new Error(format(ERROR.INVALID_TYPE, [typeof host, "host"])); + + if (arguments.length == 2) { + // host: must be full ws:// uri + // port: clientId + clientId = port; + uri = host; + var match = uri.match(/^(wss?):\/\/((\[(.+)\])|([^\/]+?))(:(\d+))?(\/.*)$/); + if (match) { + host = match[4]||match[2]; + port = parseInt(match[7]); + path = match[8]; + } else { + throw new Error(format(ERROR.INVALID_ARGUMENT,[host,"host"])); + } + } else { + if (arguments.length == 3) { + clientId = path; + path = "/mqtt"; + } + if (typeof port !== "number" || port < 0) + throw new Error(format(ERROR.INVALID_TYPE, [typeof port, "port"])); + if (typeof path !== "string") + throw new Error(format(ERROR.INVALID_TYPE, [typeof path, "path"])); + + var ipv6AddSBracket = (host.indexOf(":") !== -1 && host.slice(0,1) !== "[" && host.slice(-1) !== "]"); + uri = "ws://"+(ipv6AddSBracket?"["+host+"]":host)+":"+port+path; + } + + var clientIdLength = 0; + for (var i = 0; i 65535) + throw new Error(format(ERROR.INVALID_ARGUMENT, [clientId, "clientId"])); + + var client = new ClientImpl(uri, host, port, path, clientId); + + //Public Properties + Object.defineProperties(this,{ + "host":{ + get: function() { return host; }, + set: function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); } + }, + "port":{ + get: function() { return port; }, + set: function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); } + }, + "path":{ + get: function() { return path; }, + set: function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); } + }, + "uri":{ + get: function() { return uri; }, + set: function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); } + }, + "clientId":{ + get: function() { return client.clientId; }, + set: function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); } + }, + "onConnected":{ + get: function() { return client.onConnected; }, + set: function(newOnConnected) { + if (typeof newOnConnected === "function") + client.onConnected = newOnConnected; + else + throw new Error(format(ERROR.INVALID_TYPE, [typeof newOnConnected, "onConnected"])); + } + }, + "disconnectedPublishing":{ + get: function() { return client.disconnectedPublishing; }, + set: function(newDisconnectedPublishing) { + client.disconnectedPublishing = newDisconnectedPublishing; + } + }, + "disconnectedBufferSize":{ + get: function() { return client.disconnectedBufferSize; }, + set: function(newDisconnectedBufferSize) { + client.disconnectedBufferSize = newDisconnectedBufferSize; + } + }, + "onConnectionLost":{ + get: function() { return client.onConnectionLost; }, + set: function(newOnConnectionLost) { + if (typeof newOnConnectionLost === "function") + client.onConnectionLost = newOnConnectionLost; + else + throw new Error(format(ERROR.INVALID_TYPE, [typeof newOnConnectionLost, "onConnectionLost"])); + } + }, + "onMessageDelivered":{ + get: function() { return client.onMessageDelivered; }, + set: function(newOnMessageDelivered) { + if (typeof newOnMessageDelivered === "function") + client.onMessageDelivered = newOnMessageDelivered; + else + throw new Error(format(ERROR.INVALID_TYPE, [typeof newOnMessageDelivered, "onMessageDelivered"])); + } + }, + "onMessageArrived":{ + get: function() { return client.onMessageArrived; }, + set: function(newOnMessageArrived) { + if (typeof newOnMessageArrived === "function") + client.onMessageArrived = newOnMessageArrived; + else + throw new Error(format(ERROR.INVALID_TYPE, [typeof newOnMessageArrived, "onMessageArrived"])); + } + }, + "trace":{ + get: function() { return client.traceFunction; }, + set: function(trace) { + if(typeof trace === "function"){ + client.traceFunction = trace; + }else{ + throw new Error(format(ERROR.INVALID_TYPE, [typeof trace, "onTrace"])); + } + } + }, + }); + + /** + * Connect this Messaging client to its server. + * + * @name Paho.Client#connect + * @function + * @param {object} connectOptions - Attributes used with the connection. + * @param {number} connectOptions.timeout - If the connect has not succeeded within this + * number of seconds, it is deemed to have failed. + * The default is 30 seconds. + * @param {string} connectOptions.userName - Authentication username for this connection. + * @param {string} connectOptions.password - Authentication password for this connection. + * @param {Paho.Message} connectOptions.willMessage - sent by the server when the client + * disconnects abnormally. + * @param {number} connectOptions.keepAliveInterval - the server disconnects this client if + * there is no activity for this number of seconds. + * The default value of 60 seconds is assumed if not set. + * @param {boolean} connectOptions.cleanSession - if true(default) the client and server + * persistent state is deleted on successful connect. + * @param {boolean} connectOptions.useSSL - if present and true, use an SSL Websocket connection. + * @param {object} connectOptions.invocationContext - passed to the onSuccess callback or onFailure callback. + * @param {function} connectOptions.onSuccess - called when the connect acknowledgement + * has been received from the server. + * A single response object parameter is passed to the onSuccess callback containing the following fields: + *
    + *
  1. invocationContext as passed in to the onSuccess method in the connectOptions. + *
+ * @param {function} connectOptions.onFailure - called when the connect request has failed or timed out. + * A single response object parameter is passed to the onFailure callback containing the following fields: + *
    + *
  1. invocationContext as passed in to the onFailure method in the connectOptions. + *
  2. errorCode a number indicating the nature of the error. + *
  3. errorMessage text describing the error. + *
+ * @param {array} connectOptions.hosts - If present this contains either a set of hostnames or fully qualified + * WebSocket URIs (ws://iot.eclipse.org:80/ws), that are tried in order in place + * of the host and port paramater on the construtor. The hosts are tried one at at time in order until + * one of then succeeds. + * @param {array} connectOptions.ports - If present the set of ports matching the hosts. If hosts contains URIs, this property + * is not used. + * @param {boolean} connectOptions.reconnect - Sets whether the client will automatically attempt to reconnect + * to the server if the connection is lost. + *
    + *
  • If set to false, the client will not attempt to automatically reconnect to the server in the event that the + * connection is lost.
  • + *
  • If set to true, in the event that the connection is lost, the client will attempt to reconnect to the server. + * It will initially wait 1 second before it attempts to reconnect, for every failed reconnect attempt, the delay + * will double until it is at 2 minutes at which point the delay will stay at 2 minutes.
  • + *
+ * @param {number} connectOptions.mqttVersion - The version of MQTT to use to connect to the MQTT Broker. + *
    + *
  • 3 - MQTT V3.1
  • + *
  • 4 - MQTT V3.1.1
  • + *
+ * @param {boolean} connectOptions.mqttVersionExplicit - If set to true, will force the connection to use the + * selected MQTT Version or will fail to connect. + * @param {array} connectOptions.uris - If present, should contain a list of fully qualified WebSocket uris + * (e.g. ws://iot.eclipse.org:80/ws), that are tried in order in place of the host and port parameter of the construtor. + * The uris are tried one at a time in order until one of them succeeds. Do not use this in conjunction with hosts as + * the hosts array will be converted to uris and will overwrite this property. + * @throws {InvalidState} If the client is not in disconnected state. The client must have received connectionLost + * or disconnected before calling connect for a second or subsequent time. + */ + this.connect = function (connectOptions) { + connectOptions = connectOptions || {} ; + validate(connectOptions, {timeout:"number", + userName:"string", + password:"string", + willMessage:"object", + keepAliveInterval:"number", + cleanSession:"boolean", + useSSL:"boolean", + invocationContext:"object", + onSuccess:"function", + onFailure:"function", + hosts:"object", + ports:"object", + reconnect:"boolean", + mqttVersion:"number", + mqttVersionExplicit:"boolean", + uris: "object"}); + + // If no keep alive interval is set, assume 60 seconds. + if (connectOptions.keepAliveInterval === undefined) + connectOptions.keepAliveInterval = 60; + + if (connectOptions.mqttVersion > 4 || connectOptions.mqttVersion < 3) { + throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.mqttVersion, "connectOptions.mqttVersion"])); + } + + if (connectOptions.mqttVersion === undefined) { + connectOptions.mqttVersionExplicit = false; + connectOptions.mqttVersion = 4; + } else { + connectOptions.mqttVersionExplicit = true; + } + + //Check that if password is set, so is username + if (connectOptions.password !== undefined && connectOptions.userName === undefined) + throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.password, "connectOptions.password"])); + + if (connectOptions.willMessage) { + if (!(connectOptions.willMessage instanceof Message)) + throw new Error(format(ERROR.INVALID_TYPE, [connectOptions.willMessage, "connectOptions.willMessage"])); + // The will message must have a payload that can be represented as a string. + // Cause the willMessage to throw an exception if this is not the case. + connectOptions.willMessage.stringPayload = null; + + if (typeof connectOptions.willMessage.destinationName === "undefined") + throw new Error(format(ERROR.INVALID_TYPE, [typeof connectOptions.willMessage.destinationName, "connectOptions.willMessage.destinationName"])); + } + if (typeof connectOptions.cleanSession === "undefined") + connectOptions.cleanSession = true; + if (connectOptions.hosts) { + + if (!(connectOptions.hosts instanceof Array) ) + throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.hosts, "connectOptions.hosts"])); + if (connectOptions.hosts.length <1 ) + throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.hosts, "connectOptions.hosts"])); + + var usingURIs = false; + for (var i = 0; i + * @param {object} subscribeOptions - used to control the subscription + * + * @param {number} subscribeOptions.qos - the maximum qos of any publications sent + * as a result of making this subscription. + * @param {object} subscribeOptions.invocationContext - passed to the onSuccess callback + * or onFailure callback. + * @param {function} subscribeOptions.onSuccess - called when the subscribe acknowledgement + * has been received from the server. + * A single response object parameter is passed to the onSuccess callback containing the following fields: + *
    + *
  1. invocationContext if set in the subscribeOptions. + *
+ * @param {function} subscribeOptions.onFailure - called when the subscribe request has failed or timed out. + * A single response object parameter is passed to the onFailure callback containing the following fields: + *
    + *
  1. invocationContext - if set in the subscribeOptions. + *
  2. errorCode - a number indicating the nature of the error. + *
  3. errorMessage - text describing the error. + *
+ * @param {number} subscribeOptions.timeout - which, if present, determines the number of + * seconds after which the onFailure calback is called. + * The presence of a timeout does not prevent the onSuccess + * callback from being called when the subscribe completes. + * @throws {InvalidState} if the client is not in connected state. + */ + this.subscribe = function (filter, subscribeOptions) { + if (typeof filter !== "string" && filter.constructor !== Array) + throw new Error("Invalid argument:"+filter); + subscribeOptions = subscribeOptions || {} ; + validate(subscribeOptions, {qos:"number", + invocationContext:"object", + onSuccess:"function", + onFailure:"function", + timeout:"number" + }); + if (subscribeOptions.timeout && !subscribeOptions.onFailure) + throw new Error("subscribeOptions.timeout specified with no onFailure callback."); + if (typeof subscribeOptions.qos !== "undefined" && !(subscribeOptions.qos === 0 || subscribeOptions.qos === 1 || subscribeOptions.qos === 2 )) + throw new Error(format(ERROR.INVALID_ARGUMENT, [subscribeOptions.qos, "subscribeOptions.qos"])); + client.subscribe(filter, subscribeOptions); + }; + + /** + * Unsubscribe for messages, stop receiving messages sent to destinations described by the filter. + * + * @name Paho.Client#unsubscribe + * @function + * @param {string} filter - describing the destinations to receive messages from. + * @param {object} unsubscribeOptions - used to control the subscription + * @param {object} unsubscribeOptions.invocationContext - passed to the onSuccess callback + or onFailure callback. + * @param {function} unsubscribeOptions.onSuccess - called when the unsubscribe acknowledgement has been received from the server. + * A single response object parameter is passed to the + * onSuccess callback containing the following fields: + *
    + *
  1. invocationContext - if set in the unsubscribeOptions. + *
+ * @param {function} unsubscribeOptions.onFailure called when the unsubscribe request has failed or timed out. + * A single response object parameter is passed to the onFailure callback containing the following fields: + *
    + *
  1. invocationContext - if set in the unsubscribeOptions. + *
  2. errorCode - a number indicating the nature of the error. + *
  3. errorMessage - text describing the error. + *
+ * @param {number} unsubscribeOptions.timeout - which, if present, determines the number of seconds + * after which the onFailure callback is called. The presence of + * a timeout does not prevent the onSuccess callback from being + * called when the unsubscribe completes + * @throws {InvalidState} if the client is not in connected state. + */ + this.unsubscribe = function (filter, unsubscribeOptions) { + if (typeof filter !== "string" && filter.constructor !== Array) + throw new Error("Invalid argument:"+filter); + unsubscribeOptions = unsubscribeOptions || {} ; + validate(unsubscribeOptions, {invocationContext:"object", + onSuccess:"function", + onFailure:"function", + timeout:"number" + }); + if (unsubscribeOptions.timeout && !unsubscribeOptions.onFailure) + throw new Error("unsubscribeOptions.timeout specified with no onFailure callback."); + client.unsubscribe(filter, unsubscribeOptions); + }; + + /** + * Send a message to the consumers of the destination in the Message. + * + * @name Paho.Client#send + * @function + * @param {string|Paho.Message} topic - mandatory The name of the destination to which the message is to be sent. + * - If it is the only parameter, used as Paho.Message object. + * @param {String|ArrayBuffer} payload - The message data to be sent. + * @param {number} qos The Quality of Service used to deliver the message. + *
+ *
0 Best effort (default). + *
1 At least once. + *
2 Exactly once. + *
+ * @param {Boolean} retained If true, the message is to be retained by the server and delivered + * to both current and future subscriptions. + * If false the server only delivers the message to current subscribers, this is the default for new Messages. + * A received message has the retained boolean set to true if the message was published + * with the retained boolean set to true + * and the subscrption was made after the message has been published. + * @throws {InvalidState} if the client is not connected. + */ + this.send = function (topic,payload,qos,retained) { + var message ; + + if(arguments.length === 0){ + throw new Error("Invalid argument."+"length"); + + }else if(arguments.length == 1) { + + if (!(topic instanceof Message) && (typeof topic !== "string")) + throw new Error("Invalid argument:"+ typeof topic); + + message = topic; + if (typeof message.destinationName === "undefined") + throw new Error(format(ERROR.INVALID_ARGUMENT,[message.destinationName,"Message.destinationName"])); + client.send(message); + + }else { + //parameter checking in Message object + message = new Message(payload); + message.destinationName = topic; + if(arguments.length >= 3) + message.qos = qos; + if(arguments.length >= 4) + message.retained = retained; + client.send(message); + } + }; + + /** + * Publish a message to the consumers of the destination in the Message. + * Synonym for Paho.Mqtt.Client#send + * + * @name Paho.Client#publish + * @function + * @param {string|Paho.Message} topic - mandatory The name of the topic to which the message is to be published. + * - If it is the only parameter, used as Paho.Message object. + * @param {String|ArrayBuffer} payload - The message data to be published. + * @param {number} qos The Quality of Service used to deliver the message. + *
+ *
0 Best effort (default). + *
1 At least once. + *
2 Exactly once. + *
+ * @param {Boolean} retained If true, the message is to be retained by the server and delivered + * to both current and future subscriptions. + * If false the server only delivers the message to current subscribers, this is the default for new Messages. + * A received message has the retained boolean set to true if the message was published + * with the retained boolean set to true + * and the subscrption was made after the message has been published. + * @throws {InvalidState} if the client is not connected. + */ + this.publish = function(topic,payload,qos,retained) { + var message ; + + if(arguments.length === 0){ + throw new Error("Invalid argument."+"length"); + + }else if(arguments.length == 1) { + + if (!(topic instanceof Message) && (typeof topic !== "string")) + throw new Error("Invalid argument:"+ typeof topic); + + message = topic; + if (typeof message.destinationName === "undefined") + throw new Error(format(ERROR.INVALID_ARGUMENT,[message.destinationName,"Message.destinationName"])); + client.send(message); + + }else { + //parameter checking in Message object + message = new Message(payload); + message.destinationName = topic; + if(arguments.length >= 3) + message.qos = qos; + if(arguments.length >= 4) + message.retained = retained; + client.send(message); + } + }; + + /** + * Normal disconnect of this Messaging client from its server. + * + * @name Paho.Client#disconnect + * @function + * @throws {InvalidState} if the client is already disconnected. + */ + this.disconnect = function () { + client.disconnect(); + }; + + /** + * Get the contents of the trace log. + * + * @name Paho.Client#getTraceLog + * @function + * @return {Object[]} tracebuffer containing the time ordered trace records. + */ + this.getTraceLog = function () { + return client.getTraceLog(); + }; + + /** + * Start tracing. + * + * @name Paho.Client#startTrace + * @function + */ + this.startTrace = function () { + client.startTrace(); + }; + + /** + * Stop tracing. + * + * @name Paho.Client#stopTrace + * @function + */ + this.stopTrace = function () { + client.stopTrace(); + }; + + this.isConnected = function() { + return client.connected; + }; + }; + + /** + * An application message, sent or received. + *

+ * All attributes may be null, which implies the default values. + * + * @name Paho.Message + * @constructor + * @param {String|ArrayBuffer} payload The message data to be sent. + *

+ * @property {string} payloadString read only The payload as a string if the payload consists of valid UTF-8 characters. + * @property {ArrayBuffer} payloadBytes read only The payload as an ArrayBuffer. + *

+ * @property {string} destinationName mandatory The name of the destination to which the message is to be sent + * (for messages about to be sent) or the name of the destination from which the message has been received. + * (for messages received by the onMessage function). + *

+ * @property {number} qos The Quality of Service used to deliver the message. + *

+ *
0 Best effort (default). + *
1 At least once. + *
2 Exactly once. + *
+ *

+ * @property {Boolean} retained If true, the message is to be retained by the server and delivered + * to both current and future subscriptions. + * If false the server only delivers the message to current subscribers, this is the default for new Messages. + * A received message has the retained boolean set to true if the message was published + * with the retained boolean set to true + * and the subscrption was made after the message has been published. + *

+ * @property {Boolean} duplicate read only If true, this message might be a duplicate of one which has already been received. + * This is only set on messages received from the server. + * + */ + var Message = function (newPayload) { + var payload; + if ( typeof newPayload === "string" || + newPayload instanceof ArrayBuffer || + (ArrayBuffer.isView(newPayload) && !(newPayload instanceof DataView)) + ) { + payload = newPayload; + } else { + throw (format(ERROR.INVALID_ARGUMENT, [newPayload, "newPayload"])); + } + + var destinationName; + var qos = 0; + var retained = false; + var duplicate = false; + + Object.defineProperties(this,{ + "payloadString":{ + enumerable : true, + get : function () { + if (typeof payload === "string") + return payload; + else + return parseUTF8(payload, 0, payload.length); + } + }, + "payloadBytes":{ + enumerable: true, + get: function() { + if (typeof payload === "string") { + var buffer = new ArrayBuffer(UTF8Length(payload)); + var byteStream = new Uint8Array(buffer); + stringToUTF8(payload, byteStream, 0); + + return byteStream; + } else { + return payload; + } + } + }, + "destinationName":{ + enumerable: true, + get: function() { return destinationName; }, + set: function(newDestinationName) { + if (typeof newDestinationName === "string") + destinationName = newDestinationName; + else + throw new Error(format(ERROR.INVALID_ARGUMENT, [newDestinationName, "newDestinationName"])); + } + }, + "qos":{ + enumerable: true, + get: function() { return qos; }, + set: function(newQos) { + if (newQos === 0 || newQos === 1 || newQos === 2 ) + qos = newQos; + else + throw new Error("Invalid argument:"+newQos); + } + }, + "retained":{ + enumerable: true, + get: function() { return retained; }, + set: function(newRetained) { + if (typeof newRetained === "boolean") + retained = newRetained; + else + throw new Error(format(ERROR.INVALID_ARGUMENT, [newRetained, "newRetained"])); + } + }, + "topic":{ + enumerable: true, + get: function() { return destinationName; }, + set: function(newTopic) {destinationName=newTopic;} + }, + "duplicate":{ + enumerable: true, + get: function() { return duplicate; }, + set: function(newDuplicate) {duplicate=newDuplicate;} + } + }); + }; + + // Module contents. + return { + Client: Client, + Message: Message + }; + // eslint-disable-next-line no-nested-ternary + })(typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}); + return PahoMQTT; +}); + diff --git a/packages/@jsonql/primus/package.json b/packages/@jsonql/primus/package.json index a287607f6c4aa6da07097b664d24e856aa89ac63..dd25ca556a41999ff5e876325deb57291d0c6edd 100644 --- a/packages/@jsonql/primus/package.json +++ b/packages/@jsonql/primus/package.json @@ -15,6 +15,7 @@ "server", "node" ], + "homepage": "jsonql.org", "author": "Joel Chu ", "license": "ISC" } diff --git a/packages/@jsonql/rx/package.json b/packages/@jsonql/rx/package.json index 821a5615759017275662ef2b651b867144a24dad..2dfb41c941b940b0298b2840dbc443025a4be543 100644 --- a/packages/@jsonql/rx/package.json +++ b/packages/@jsonql/rx/package.json @@ -16,6 +16,7 @@ "contributors": [ "NEWBRAN LTD " ], + "homepage": "jsonql.org", "repository": { "type": "git", "url": "git+ssh://git@gitee.com:to1source/jsonql.git" diff --git a/packages/@jsonql/socketio/package.json b/packages/@jsonql/socketio/package.json index e67109eedfc3b834b654e67601e1716f0b220de8..47fd2e8dca8cdd1025a13d2652c2186d7be8c757 100644 --- a/packages/@jsonql/socketio/package.json +++ b/packages/@jsonql/socketio/package.json @@ -26,7 +26,7 @@ ], "author": "Joel Chu ", "license": "ISC", - "homepage": "jsonql.js.org", + "homepage": "jsonql.org", "devDependencies": { "ava": "^2.2.0", "fs-extra": "^8.1.0", diff --git a/packages/@jsonql/vue/package.json b/packages/@jsonql/vue/package.json index 44e8ca0f0e150b0a6eef2898d39a8a67267e3a90..c04e5a8eb7b3673d51a1d84a229223dbdcd92e04 100644 --- a/packages/@jsonql/vue/package.json +++ b/packages/@jsonql/vue/package.json @@ -42,5 +42,6 @@ "browserslist": [ "> 1%", "last 2 versions" - ] + ], + "homepage": "jsonql.org" } diff --git a/packages/@jsonql/ws/package.json b/packages/@jsonql/ws/package.json index 1e56c46c0b5ca92d4de1231322b18c59b615a00c..7d10e00a0a67aee2f2177f8975c29e93050d6623 100644 --- a/packages/@jsonql/ws/package.json +++ b/packages/@jsonql/ws/package.json @@ -75,6 +75,7 @@ "engine": { "node": ">=8" }, + "homepage": "jsonql.org", "repository": { "type": "git", "url": "git+ssh://git@gitee.com:to1source/jsonql.git" diff --git a/packages/constants/package.json b/packages/constants/package.json index 3ca9661c41f5ca23b0fc9a874ae9b9af1997d500..6e29af9f3fc2a622541f13dd6c735cf14e4fc297 100755 --- a/packages/constants/package.json +++ b/packages/constants/package.json @@ -26,7 +26,7 @@ "type": "git", "url": "git+ssh://git@gitee.com:to1source/jsonql.git" }, - "homepage": "https://jsonql.js.org", + "homepage": "jsonql.org", "bugs": { "url": "https://gitee.com/to1source/jsonql/issues" }, diff --git a/packages/contract-cli/lib/ast/acorn.js b/packages/contract-cli/lib/ast/acorn.js index 21bed3da1d9f2a069c15b930f232575f55dc6be0..9db5b4e80a71aae15e07c6d51f3ffb246930b2f9 100644 --- a/packages/contract-cli/lib/ast/acorn.js +++ b/packages/contract-cli/lib/ast/acorn.js @@ -1,7 +1,7 @@ // put all the acorn related methods here // later on we can switch between acorn or typescript -const acorn = require('acorn'); -const { DEFAULT_TYPE } = require('jsonql-constants'); +const acorn = require('acorn') +const { DEFAULT_TYPE } = require('jsonql-constants') /** * First filter we only interested in ExpressionStatement @@ -63,7 +63,7 @@ function extractReturns(tree) { try { return takeDownArray(tree.right.body.body .filter(arg => arg.type === 'ReturnStatement') - .map(arg => processArgOutput(arg.argument))); + .map(arg => processArgOutput(arg.argument))) } catch (e) { return false; } @@ -85,4 +85,4 @@ module.exports = { extractReturns, extractParams, isExpression -}; +} diff --git a/packages/contract-cli/lib/generator/es-extra.js b/packages/contract-cli/lib/generator/es-extra.js index 30f3fb61881b4d12d22e19e6ca7cef0d1c44a3d5..9aa343559482aaefd3eb61462912132ffea471f4 100644 --- a/packages/contract-cli/lib/generator/es-extra.js +++ b/packages/contract-cli/lib/generator/es-extra.js @@ -4,7 +4,7 @@ const { join } = require('path') const fsx = require('fs-extra') const { forEach } = require('lodash') -const { inArray } = require('jsonql-params-validator') +const { inArray } = require('jsonql-utils') const { DEFAULT_RESOLVER_LIST_FILE_NAME, DEFAULT_RESOLVER_IMPORT_FILE_NAME, @@ -14,7 +14,8 @@ const { AUTH_TYPE, MODULE_TYPE } = require('jsonql-constants') -const debug = require('debug')('jsonql-contract:es-extra') +const { getDebug } = require('../utils') +const debug = getDebug('es-extra') const IMPORT_JS_TEMPLATE = 'import.template.js'; diff --git a/packages/contract-cli/lib/generator/files.js b/packages/contract-cli/lib/generator/files.js index a9482eed41b225f395a108d6788adf3946a0eb5b..fb11c1981dec327a98c8cbad3d23578fe7f0c75e 100644 --- a/packages/contract-cli/lib/generator/files.js +++ b/packages/contract-cli/lib/generator/files.js @@ -5,7 +5,6 @@ const fsx = require('fs-extra') const glob = require('glob') const colors = require('colors/safe') const { merge } = require('lodash') -const debug = require('debug')('jsonql-contract:generator:files') // our modules const { RETURN_AS_JSON, MODULE_TYPE, SCRIPT_TYPE } = require('jsonql-constants') const getResolver = require('./get-resolver') @@ -21,8 +20,9 @@ const { extractParams, isExpression } = require('../ast') -const { getTimestamp } = require('../utils') +const { getTimestamp, getDebug } = require('../utils') const postProcess = require('./es-extra') +const debug = getDebug('generator:files') // this was lost when using as api using let let sourceType; diff --git a/packages/contract-cli/lib/generator/get-resolver.js b/packages/contract-cli/lib/generator/get-resolver.js index 7761858127882c88c16cab80b6682f4638d54bb9..7a4765572459e85c51b4418dd1926e12ed71502c 100644 --- a/packages/contract-cli/lib/generator/get-resolver.js +++ b/packages/contract-cli/lib/generator/get-resolver.js @@ -1,6 +1,7 @@ -// this is one huge method so make this a standalone here +// this is the HEART of this module const { join, basename } = require('path') const { compact } = require('lodash') +const { INDEX_KEY } = require('jsonql-constants') const { inTypesArray, isAuthType, @@ -9,8 +10,9 @@ const { addPublicKey, packOutput } = require('./helpers') -const debug = require('debug')('jsonql-contract:generator:get-resolvers') -const { INDEX } = require('jsonql-constants') +const { getDebug } = require('../utils') +const debug = getDebug('generator:get-resolvers') + /** * breaking out the getResolver, this one will able to get normal or when @@ -18,7 +20,6 @@ const { INDEX } = require('jsonql-constants') * */ - /** * filter to get which is resolver or not * @param {string} inDir the base path @@ -27,7 +28,7 @@ const { INDEX } = require('jsonql-constants') * @return {function} work out if it's or not */ const getResolver = function(inDir, fileType, config) { - const indexFile = [INDEX, fileType].join('.') + const indexFile = [INDEX_KEY, fileType].join('.') // return a function here return baseFile => { const failed = {ok: false}; diff --git a/packages/contract-cli/lib/generator/helpers.js b/packages/contract-cli/lib/generator/helpers.js index 348a74c6fc19c63681c0964e492792f662b68065..be472f92463f1155f014711fbe93e4d6e4672e64 100644 --- a/packages/contract-cli/lib/generator/helpers.js +++ b/packages/contract-cli/lib/generator/helpers.js @@ -1,7 +1,8 @@ // a bunch of small methods const fsx = require('fs-extra') const { merge, extend, camelCase } = require('lodash') -const { isKeyInObject, isObject } = require('jsonql-params-validator') +const { isObject } = require('jsonql-params-validator') +const { isKeyInObject, isContract } = require('jsonql-utils') const { RESOLVER_TYPES, AUTH_TYPE, @@ -13,11 +14,12 @@ const { PRIVATE_KEY } = require('jsonql-constants') const { getJsdoc } = require('../ast') +const { getDebug } = require('../utils') // Not using the stock one from jsonql-constants let AUTH_TYPE_METHODS; -const debug = require('debug')('jsonql-contract:generator:helpers') +const debug = getDebug('generator:helpers') /////////////////////////////// // get-resolvers helpers // @@ -44,7 +46,7 @@ const isAuthType = function(type, file, config) { const { loginHandlerName, logoutHandlerName, validatorHandlerName } = config; AUTH_TYPE_METHODS = [loginHandlerName, logoutHandlerName, validatorHandlerName]; } - debug('AUTH_TYPE_METHODS', resolverName, AUTH_TYPE_METHODS) + // debug('AUTH_TYPE_METHODS', resolverName, AUTH_TYPE_METHODS) return type === AUTH_TYPE && !!AUTH_TYPE_METHODS.filter(method => resolverName === method).length; } @@ -136,7 +138,7 @@ const packOutput = (type, fileName, ok, baseFile, isPublic, config) => ( */ const takeDownArray = function(arr) { if (arr.length && arr.length === 1) { - return takeDownArray(arr[0]); + return takeDownArray(arr[0]) } return arr; } @@ -191,15 +193,6 @@ function logToFile(dir, file, params) { } } -/** - * validate the contract object is a contract file - * @param {object} contract input - * @return {boolean} true on OK - */ -const isContract = (contract) => ( - contract && isObject(contract) && (isKeyInObject(contract, QUERY_NAME) || isKeyInObject(contract, MUTATION_NAME)) -) - // export module.exports = { inTypesArray, diff --git a/packages/contract-cli/lib/get-paths.js b/packages/contract-cli/lib/get-paths.js index fae9d585bb6b2558f6a4297e1eb33aa65f2f671a..9115dd0a1531ccacfe5b47332c7f22e9dc3786b8 100755 --- a/packages/contract-cli/lib/get-paths.js +++ b/packages/contract-cli/lib/get-paths.js @@ -1,7 +1,9 @@ const fs = require('fs') -const debug = require('debug')('jsonql-contract:paths') const { resolve } = require('path') +const { getDebug } = require('./utils') +const debug = getDebug('paths') + /** * The input from cmd and include are different and cause problem for client * @param {mixed} args array or object diff --git a/packages/contract-cli/lib/public-contract/index.js b/packages/contract-cli/lib/public-contract/index.js index 6322cb74cb795f258a2bc1f3a13e62d76a3f7846..cd6a602b9d242d6f537aa4f917cf66533427fbb5 100644 --- a/packages/contract-cli/lib/public-contract/index.js +++ b/packages/contract-cli/lib/public-contract/index.js @@ -3,7 +3,9 @@ const { join } = require('path') const fsx = require('fs-extra') const { merge } = require('lodash') const { JsonqlError } = require('jsonql-errors') -const debug = require('debug')('jsonql-contract:public-contract') + +const { getDebug } = require('../utils') +const debug = getDebug('public-contract') /** * we need to remove the file info from the contract if this is for public * @param {object} json contract @@ -72,7 +74,7 @@ function getEnvContractFile(contractDir) { function getExpired(config, contractJson) { const { expired } = config; const { timestamp } = contractJson; - // the timestamp now comes with milsecond ... + // the timestamp now comes with milsecond ... if (expired && expired > timestamp) { return { expired } } diff --git a/packages/contract-cli/lib/utils.js b/packages/contract-cli/lib/utils.js index 413a57610790d6487d8f8b0a9964360fd283db8f..4cf69bdaa97e9ea9f1adbdef250a163ca142f026 100644 --- a/packages/contract-cli/lib/utils.js +++ b/packages/contract-cli/lib/utils.js @@ -6,10 +6,14 @@ const { isObject } = require('jsonql-params-validator') const checkOptions = require('./options') // we keep the lodash reference for chain later const { transform } = require('lodash') -const debug = require('debug')('jsonql-contract:utils') + const { JsonqlError } = require('jsonql-errors') // timestamp with mil seconds -const getTimestamp = () => Date.now() +const { timestamp, getDebug } = require('jsonql-utils') +const MODULE_NAME = 'jsonql-contract' + +const getTimestamp = () => timestamp(true) +const debug = getDebug('utils', MODULE_NAME) /** * check if there is a config file and use that value instead @@ -107,5 +111,6 @@ const checkFile = config => { module.exports = { getTimestamp, checkFile, - applyDefaultOptions + applyDefaultOptions, + getDebug: (name) => getDebug(name, MODULE_NAME) } diff --git a/packages/contract-cli/package.json b/packages/contract-cli/package.json index fa6bc196ec82ba74b19651e68e72747508999946..03b10e5089c147852ccdfb973b0e278e95819ac2 100755 --- a/packages/contract-cli/package.json +++ b/packages/contract-cli/package.json @@ -1,7 +1,7 @@ { "name": "jsonql-contract", - "version": "1.7.7", - "description": "An command line tool to generate the contract.json for jsonql", + "version": "1.7.8", + "description": "JS API / command line tool to generate the contract.json for jsonql", "main": "index.js", "files": [ "lib", @@ -43,16 +43,17 @@ "colors": "^1.3.3", "fs-extra": "^8.1.0", "glob": "^7.1.4", - "jsdoc-api": "^5.0.2", - "jsonql-constants": "^1.7.9", - "jsonql-errors": "^1.0.9", - "jsonql-params-validator": "^1.4.3", + "jsdoc-api": "^5.0.3", + "jsonql-constants": "^1.8.2", + "jsonql-errors": "^1.1.2", + "jsonql-params-validator": "^1.4.6", + "jsonql-utils": "^0.4.6", "kefir": "^3.8.6", "lodash": "^4.17.15", - "yargs": "^13.3.0" + "yargs": "^14.0.0" }, "devDependencies": { - "ava": "^2.2.0", + "ava": "^2.3.0", "debug": "^4.1.1", "nyc": "^14.1.1", "request": "^2.88.0" @@ -77,6 +78,7 @@ "engine": { "node": ">=8" }, + "homepage": "jsonql.org", "repository": { "type": "git", "url": "git+ssh://git@gitee.com:to1source/jsonql.git" diff --git a/packages/errors/package.json b/packages/errors/package.json index c7bd6fd53a87b34fa35a35689bdc395387ca3f2c..fa3d6bdd34e2f739bf17e58fa54d2ddb865385d1 100644 --- a/packages/errors/package.json +++ b/packages/errors/package.json @@ -70,6 +70,7 @@ "engine": { "node": ">=8" }, + "homepage": "jsonql.org", "author": "Joel Chu ", "license": "ISC", "dependencies": { diff --git a/packages/http-client/package.json b/packages/http-client/package.json index 918ad1e93521101ee975c72b2c829872a7372deb..5fc060c0397841b7f1242c9c97661bec3a901f18 100755 --- a/packages/http-client/package.json +++ b/packages/http-client/package.json @@ -39,6 +39,7 @@ "src", "index.js" ], + "homepage": "jsonql.org", "author": "to1source ", "contributors": [ "NEWBRAN LTD " diff --git a/packages/jwt/package.json b/packages/jwt/package.json index e3553311d5c26a33a3dd1ef58851f0946d134cb0..1501f6873b018c0c4b78772b25ca6fbeff8d1e73 100644 --- a/packages/jwt/package.json +++ b/packages/jwt/package.json @@ -105,6 +105,7 @@ "engine": { "node": ">=8" }, + "homepage": "jsonql.org", "repository": { "type": "git", "url": "git+ssh://git@gitee.com:to1source/jsonql.git" diff --git a/packages/koa/package.json b/packages/koa/package.json index bb9ef729a782da1fe99619d757950765aa4d76b7..f7df1829b55a768e28d66be005e0bcab24e2e0b6 100755 --- a/packages/koa/package.json +++ b/packages/koa/package.json @@ -1,6 +1,6 @@ { "name": "jsonql-koa", - "version": "1.3.8", + "version": "1.3.4", "description": "jsonql Koa middleware", "main": "index.js", "files": [ @@ -9,26 +9,7 @@ "contract.js" ], "scripts": { - "test:debug": "DEBUG=jsonql* ava --verbose", - "test": "ava", - "coverage": "nyc ava --verbose", - "test:notfound": "DEBUG=jsonql-koa* ava --verbose ./tests/resolverNotFound.test.js", - "test:es6": "DEBUG=jsonql-koa* ava --verbose ./tests/es6-module.test.js", - "test:jwt": "DEBUG=jsonql-koa*,jsonql-jwt* ava ./tests/jwt.test.js", - "test:jwt-auth": "DEBUG=jsonql-koa* ava ./tests/jwt-auth.test.js", - "test:fail": "ava ./tests/fail.test.js", - "test:basic": "DEBUG=jsonql* ava ./tests/koa.test.js", - "test:contract": "DEBUG=jsonql-koa* ava ./tests/contractWithAuth.test.js", - "test:auth": "DEBUG=jsonql* ava ./tests/auth.test.js", - "test:error": "DEBUG=jsonql-koa* ava ./tests/resolverNotFound.test.js", - "test:config": "DEBUG=jsonql-* ava ./tests/config.test.js", - "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", - "test:clients": "DEBUG=jsonql* ava --verbose ./tests/node-client.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" + "deprecated": "npm deprecate jsonql-ws-client \"This module is no longer maintain, please install our latest @jsonql/koa and check https://jsonql.js.org for up to date version\"" }, "keywords": [ "jsonql", @@ -58,6 +39,7 @@ "contributors": [ "NEWBRAN LTD " ], + "homepage": "jsonql.org", "license": "MIT", "devDependencies": { "ava": "^2.3.0", diff --git a/packages/node-client/package.json b/packages/node-client/package.json index 5af72c0f8c18f395f88bae3a3cd8e2872ee1e26f..e6b6bfd92f1960a9723c8711876f9b26bca38527 100755 --- a/packages/node-client/package.json +++ b/packages/node-client/package.json @@ -13,6 +13,7 @@ "test:jwt": "DEBUG=jsonql-node-client* ava tests/jwt.test.js", "test:contract": "DEBUG=jsonql-node-client:main,jsonql-koa:process-contract ava --verbose tests/contract.test.js" }, + "homepage": "jsonql.org", "repository": { "type": "git", "url": "git+ssh://git@gitee.com:to1source/jsonql.git" diff --git a/packages/post/README.md b/packages/post/README.md new file mode 100644 index 0000000000000000000000000000000000000000..bafdc9499fa0646fbb2a67911bac76a94b117d3f --- /dev/null +++ b/packages/post/README.md @@ -0,0 +1,12 @@ +# Post install + +This is NOT going to get publish to NPM!!! + +This is just an internal scripts that add a `postinstall` field to the package.json +and add the `post-install.js` to every package. + +And whenever we updated, it will update all the packages in this mono-repository + +--- + +JOEL diff --git a/packages/post/package.json b/packages/post/package.json new file mode 100644 index 0000000000000000000000000000000000000000..4dccbfb8a32b8de17a48d83030707ffcb480cd31 --- /dev/null +++ b/packages/post/package.json @@ -0,0 +1,16 @@ +{ + "name": "post", + "version": "1.0.0", + "description": "This is NOT going to get publish to NPM!!!", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "fs-extra": "^8.1.0", + "glob": "^7.1.4" + } +} diff --git a/packages/resolver/package.json b/packages/resolver/package.json index 627488e3d16f43b2ceaf4f4217808c435e62a487..5f09aebe5f9655b2d78716af59cf0a22824ca721 100644 --- a/packages/resolver/package.json +++ b/packages/resolver/package.json @@ -17,7 +17,7 @@ ], "author": "Joel Chu ", "license": "ISC", - "homepage": "https://jsonql.js.org", + "homepage": "jsonql.org", "bugs": { "url": "https://gitee.com/to1source/jsonql/issues" }, diff --git a/packages/utils/es.js b/packages/utils/es.js index 6bb1331f6cab60f419b9b53578b43fea6df9edc3..0a5ec7b1e17925c94973321b2b124361e6e8a6e4 100644 --- a/packages/utils/es.js +++ b/packages/utils/es.js @@ -20,7 +20,8 @@ import { urlParams, cacheBurst, cacheBurstUrl, - dasherize + dasherize, + getConfigValue } from './src/generic' import { isJsonqlPath, @@ -42,6 +43,8 @@ import { resultHandler } from './src/middleware' import { + toPayload, + formatPayload, createQuery, createQueryStr, createMutation, @@ -76,13 +79,13 @@ export { getDebug, inArray, isKeyInObject, - injectToFn, dasherize, createEvt, timestamp, urlParams, cacheBurst, cacheBurstUrl, + getConfigValue, // koa isJsonqlPath, isJsonqlRequest, @@ -101,6 +104,8 @@ export { packError, resultHandler, // params-api + toPayload, + formatPayload, createQuery, createQueryStr, createMutation, @@ -113,5 +118,6 @@ export { // node buff, // objectDefineProp + injectToFn, objDefineProps } diff --git a/packages/utils/package.json b/packages/utils/package.json index d58525653828656d5ace48e71e8a168c84704411..ee3322c80c0810fdb508ac4101397c7be1ecd4ea 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "jsonql-utils", - "version": "0.4.2", + "version": "0.4.6", "description": "This is a jsonql dependency module, not for generate use.", "main": "main.js", "module": "es.js", @@ -12,6 +12,7 @@ "scripts": { "test": "ava --verbose", "prepare": "npm run test", + "test:define": "DEBUG=jsonql* ava --verbose ./tests/define-property.test.js", "test:params": "DEBUG=jsonql* ava ./tests/params-api.test.js" }, "keywords": [ @@ -27,7 +28,7 @@ "type": "git", "url": "git+ssh://git@gitee.com:to1source/jsonql.git" }, - "homepage": "https://jsonql.js.org", + "homepage": "jsonql.org", "bugs": { "url": "https://gitee.com/to1source/jsonql/issues" }, @@ -51,7 +52,7 @@ "dependencies": { "debug": "^4.1.1", "esm": "^3.2.25", - "jsonql-constants": "^1.8.0", + "jsonql-constants": "^1.8.2", "jsonql-errors": "^1.1.2", "lodash-es": "^4.17.15" }, diff --git a/packages/utils/src/generic.js b/packages/utils/src/generic.js index 1b9aa11d34d963290f5a0d09a8192b6337daa57e..0448f0bbd5f58e29c53b20c8fa4ec73ae2bbaa92 100644 --- a/packages/utils/src/generic.js +++ b/packages/utils/src/generic.js @@ -88,3 +88,13 @@ export const dasherize = str => ( .replace(/[-_\s]+/g, '-') .toLowerCase() ) + +/** + * simple util method to get the value + * @param {string} name of the key + * @param {object} obj to take value from + * @return {*} the object value id by name or undefined + */ +export const getConfigValue = (name, obj) => ( + (name in obj) ? obj[name] : undefined +) diff --git a/packages/utils/src/obj-define-props.js b/packages/utils/src/obj-define-props.js index e2815ee5f5a6acc56fae7cfa88f81696c252d911..02c0f30cfb84e65c784c71844e09bf62b72e48ea 100644 --- a/packages/utils/src/obj-define-props.js +++ b/packages/utils/src/obj-define-props.js @@ -1,3 +1,5 @@ + + /** * this is essentially the same as the injectToFn * but this will not allow overwrite and set the setter and getter @@ -28,13 +30,19 @@ export function objDefineProps(obj, name, setter, getter = null) { */ export function injectToFn(resolver, name, data, overwrite = false) { let check = Object.getOwnPropertyDescriptor(resolver, name) - if (ovewrite === false && check !== undefined) { + if (overwrite === false && check !== undefined) { + // console.info(`NOT INJECTED`) return resolver; } - + /* this will throw error! + if (overwrite === true && check !== undefined) { + delete resolver[name] // delete this property + } + */ + // console.info(`INJECTED`) Object.defineProperty(resolver, name, { value: data, - writable: false // make this immutatble + writable: overwrite // if its set to true then we should able to overwrite it }) return resolver; diff --git a/packages/utils/src/params-api.js b/packages/utils/src/params-api.js index 25a4c60fbac2552e46e96ad4d9edfcba5027c3fe..eba779b449a5e60a690d52cae6697ef9e0d6487b 100644 --- a/packages/utils/src/params-api.js +++ b/packages/utils/src/params-api.js @@ -16,8 +16,20 @@ import { checkIsObject } from './object' */ import { isString, isArray, isPlainObject } from 'lodash-es' -// make sure it's an object -const formatPayload = payload => isString(payload) ? JSON.parse(payload) : payload; +/** + * make sure it's an object (it was call formatPayload but it doesn't make sense) + * @param {*} payload the object comes in could be string based + * @return {object} the transformed payload + */ +export const toPayload = payload => isString(payload) ? JSON.parse(payload) : payload; + +/** + * @param {*} args arguments to send + *@return {object} formatted payload + */ +export const formatPayload = (args) => ( + { [QUERY_ARG_NAME]: args } +) /** * Get name from the payload (ported back from jsonql-koa) @@ -36,7 +48,7 @@ export function getNameFromPayload(payload) { */ export function createQuery(resolverName, args = [], jsonp = false) { if (isString(resolverName) && isArray(args)) { - let payload = { [QUERY_ARG_NAME]: args } + let payload = formatPayload(args) if (jsonp === true) { return payload; } @@ -102,7 +114,7 @@ export function getQueryFromArgs(resolverName, payload) { * @return {*} result processed result */ function processPayload(payload, processor) { - const p = formatPayload(payload) + const p = toPayload(payload) const resolverName = getNameFromPayload(p) return Reflect.apply(processor, null, [resolverName, p]) } diff --git a/packages/utils/tests/define-property.test.js b/packages/utils/tests/define-property.test.js new file mode 100644 index 0000000000000000000000000000000000000000..b5ed6693480b15a745be097ea7bf69721e4d7d4a --- /dev/null +++ b/packages/utils/tests/define-property.test.js @@ -0,0 +1,41 @@ +// need to test the two new object define property methods + +const test = require('ava') +const { injectToFn, objDefineProps } = require('../main') + + +test(`It should able to overwrite with the flag`, t => { + // just a dummy for injection + let baseFn = () => 'I am fine' + let key = 'someProps' + let value = 'I am not fine' + // we need to set the flag from the beginning + let injectedFn1 = injectToFn(baseFn, key, value, true) + + t.is(injectedFn1[key], value) + + let value2 = 'I am OK now' + + let injectedFn2 = injectToFn(injectedFn1, key, value2, true) + + // t.not(injectedFn2[key], value2) + + // let injectedFn3 = injectToFn(injectedFn1, key, value2, true) + + t.is(injectedFn2[key], value2) +}) + +test(`It should never allow to overwrite the setter or getter`, t => { + + let baseFn = () => 'that is cool' + let key = 'seriousProp' + + let setter = (value) => { + console.info(value) + } + + let fn1 = objDefineProps(baseFn, key, setter) + + t.is(fn1[key], null) + +}) diff --git a/packages/validator/package.json b/packages/validator/package.json index 1dd9e70d7f1a6859f0ef5a1da16b5bdc4778be3a..53eefa68b6a86e80f6994163c7ec592688217121 100644 --- a/packages/validator/package.json +++ b/packages/validator/package.json @@ -88,7 +88,7 @@ "type": "git", "url": "git+ssh://git@gitee.com:to1source/jsonql.git" }, - "homepage": "https://jsonql.js.org", + "homepage": "jsonql.org", "bugs": { "url": "https://gitee.com/to1source/jsonql/issues" }, diff --git a/packages/web-console/package.json b/packages/web-console/package.json index 103c8e3c2639ce4a3d949c33370711114e7b0e66..c9b24503aa4822d571877a1522d141328a14a42a 100644 --- a/packages/web-console/package.json +++ b/packages/web-console/package.json @@ -40,6 +40,7 @@ "vue-template-compiler": "^2.6.10", "vuex": "^3.1.1" }, + "homepage": "jsonql.org", "repository": { "type": "git", "url": "git+ssh://git@gitee.com:to1source/jsonql.git" diff --git a/packages/ws-base/README.md b/packages/ws-base/README.md new file mode 100644 index 0000000000000000000000000000000000000000..baaab71ecd892c377fa3fab88d34fa67e82b4400 --- /dev/null +++ b/packages/ws-base/README.md @@ -0,0 +1,12 @@ +# jsonql-ws-base + +This module is the foundation for all the jsonql socket related code. +This is NOT intend to use directly. + +Please checkout our main documentation site at [jsonql.org](https://jsonql.js.org) + +--- + +Joel Chu (c) 2019 + +Co-develop by NEWBRAN LTD UK and TO1SOURCE China diff --git a/packages/ws-base/client.js b/packages/ws-base/client.js new file mode 100644 index 0000000000000000000000000000000000000000..4f4f20f6a626a2447bf152b72193d2f005da5573 --- /dev/null +++ b/packages/ws-base/client.js @@ -0,0 +1,2 @@ +// all the client code for import +// import * as jsonqlWsBaseClient from 'jsonql-ws-base/client' diff --git a/packages/ws-base/main.js b/packages/ws-base/main.js new file mode 100644 index 0000000000000000000000000000000000000000..56eb40553d6c7c07e2191868e9debdbbd79e99b0 --- /dev/null +++ b/packages/ws-base/main.js @@ -0,0 +1 @@ +// The CJS main export using ESM diff --git a/packages/ws-base/module.js b/packages/ws-base/module.js new file mode 100644 index 0000000000000000000000000000000000000000..e02e33df25bd98aa4f3fa54806e3d9101884860b --- /dev/null +++ b/packages/ws-base/module.js @@ -0,0 +1 @@ +// ES6 module export that combine the server.js and client.js diff --git a/packages/ws-base/package.json b/packages/ws-base/package.json new file mode 100644 index 0000000000000000000000000000000000000000..0aafe355e3dea1c1bbc3226a65ffdfeb74579af2 --- /dev/null +++ b/packages/ws-base/package.json @@ -0,0 +1,72 @@ +{ + "name": "jsonql-ws-base", + "version": "0.0.1", + "description": "This is the foundation for all jsonql socket related client and server, NOT intend to use directly.", + "main": "main.js", + "module": "module.js", + "files": [ + "main.js", + "module.js", + "server.js", + "client.js", + "src" + ], + "scripts": { + "test": "ava --verbose", + "prepare": "npm run test", + "postinstall": "node ./src/post-install.js" + }, + "keywords": [ + "jsonql", + "socket", + "mqtt", + "socket.io", + "ws", + "WebSocket", + "Primus" + ], + "author": "Joel Chu ", + "license": "ISC", + "ava": { + "files": [ + "tests/*.test.js", + "!tests/helpers/*.*", + "!tests/fixtures/*.*", + "!tests/jwt/*.*" + ], + "require": [ + "esm" + ], + "cache": true, + "concurrency": 5, + "failFast": true, + "failWithoutAssertions": false, + "tap": false, + "compileEnhancements": false + }, + "engine": { + "node": ">=8" + }, + "repository": { + "type": "git", + "url": "git+ssh://git@gitee.com:to1source/jsonql.git" + }, + "homepage": "jsonql.org", + "bugs": { + "url": "https://gitee.com/to1source/jsonql/issues" + }, + "dependencies": { + "jsonql-constants": "^1.8.2", + "jsonql-errors": "^1.1.2", + "jsonql-jwt": "^1.3.0", + "jsonql-params-validator": "^1.4.6", + "jsonql-utils": "^0.4.4", + "nb-event-service": "^1.8.3" + }, + "devDependencies": { + "esm": "^3.2.25", + "ava": "^2.3.0", + "fs-extra": "^8.1.0", + "kefir": "^3.8.6" + } +} diff --git a/packages/ws-base/server.js b/packages/ws-base/server.js new file mode 100644 index 0000000000000000000000000000000000000000..bbef0b90d88b8c40a4ccd7a753312b123a5cdcba --- /dev/null +++ b/packages/ws-base/server.js @@ -0,0 +1,2 @@ +// all the server code top level export +// we could import it like import 'jsonql-ws-base/server' diff --git a/packages/ws-base/src/client/client-event-handler.js b/packages/ws-base/src/client/client-event-handler.js new file mode 100644 index 0000000000000000000000000000000000000000..ef3ede423ea3db8e519f40fa338d7f2f6d2feebd --- /dev/null +++ b/packages/ws-base/src/client/client-event-handler.js @@ -0,0 +1,91 @@ +// @TODO port what is in the ws-main-handler +// because all the client side call are via the ee +// and that makes it re-usable between different client setup +import { + MESSAGE_PROP_NAME, + RESULT_PROP_NAME, + EMIT_EVT, + SOCKET_IO, + WS +} from './utils/constants' +import { + LOGIN_EVENT_NAME, + LOGOUT_EVENT_NAME, + NOT_LOGIN_ERR_MSG, + ERROR_PROP_NAME +} from 'jsonql-constants' + +import { getDebug, createEvt, clearMainEmitEvt } from './utils' +import triggerNamespacesOnError from './utils/trigger-namespaces-on-error' +const debugFn = getDebug('client-event-handler') + +/** + * A fake ee handler + * @param {string} namespace nsp + * @param {object} ee EventEmitter + * @return {void} + */ +const notLoginWsHandler = (namespace, ee) => { + ee.$only( + createEvt(namespace, EMIT_EVT), + function(resolverName, args) { + debugFn('noLoginHandler hijack the ws call', namespace, resolverName, args) + let error = { + message: NOT_LOGIN_ERR_MSG + } + // It should just throw error here and should not call the result + // because that's channel for handling normal event not the fake one + ee.$call(createEvt(namespace, resolverName, ERROR_PROP_NAME), [error]) + // also trigger the result handler, but wrap inside the error key + ee.$call(createEvt(namespace, resolverName, RESULT_PROP_NAME), [{ error }]) + } + ) +} + +/** + * centralize all the comm in one place + * @param {object} opts configuration + * @param {array} namespaces namespace(s) + * @param {object} ee Event Emitter instance + * @param {function} bindWsHandler binding the ee to ws + * @param {array} namespaces array of namespace available + * @param {object} nsps namespaced nsp + * @return {void} nothing + */ +export default function clientEventHandler(opts, nspMap, ee, bindWsHandler, namespaces, nsps) { + // loop + // @BUG for io this has to be in order the one with auth need to get call first + // The order of login is very import we need to run a waterfall here to make sure + // one is execute then the other + namespaces.forEach(namespace => { + if (nsps[namespace]) { + debugFn('call bindWsHandler', namespace) + let args = [namespace, nsps[namespace], ee] + if (opts.serverType === SOCKET_IO) { + let { nspSet } = nspMap; + args.push(nspSet[namespace]) + args.push(opts) + } + Reflect.apply(bindWsHandler, null, args) + } else { + // a dummy placeholder + notLoginWsHandler(namespace, ee) + } + }) + // this will be available regardless enableAuth + // because the server can log the client out + ee.$on(LOGOUT_EVENT_NAME, function() { + debugFn('LOGOUT_EVENT_NAME') + // disconnect(nsps, opts.serverType) + // we need to issue error to all the namespace onError handler + triggerNamespacesOnError(ee, namespaces, LOGOUT_EVENT_NAME) + // rebind all of the handler to the fake one + namespaces.forEach( namespace => { + clearMainEmitEvt(ee, namespace) + // clear out the nsp + nsps[namespace] = false; + // add a NOT LOGIN error if call + notLoginWsHandler(namespace, ee) + }) + }) +} diff --git a/packages/ws-base/src/client/create-socket-client.js b/packages/ws-base/src/client/create-socket-client.js new file mode 100644 index 0000000000000000000000000000000000000000..60b4df6c9152bae38141c4eb82a22f979da907da --- /dev/null +++ b/packages/ws-base/src/client/create-socket-client.js @@ -0,0 +1,29 @@ +// import { JsonqlError } from 'jsonql-errors' +/* +this have moved out of this package +@TODO need to figure out how to inject them back here +import createWsClient from './ws' +import createIoClient from './io' +*/ +// import { SOCKET_IO, WS, SOCKET_NOT_DEFINE_ERR } from './utils/constants' + +/** + * get the create client instance function + * @param {string} type of client + * @return {function} the actual methods + * @public + */ +export default function createSocketClient(opts, nspMap, ee) { + // idea, instead of serverType we pass the create client method via the opts + return Reflect.apply(opts.createClientMethod, null, [opts, nspMap, ee]) + /* + switch (opts.serverType) { + case SOCKET_IO: + return createIoClient(opts, nspMap, ee) + case WS: + return createWsClient(opts, nspMap, ee) + default: + throw new JsonqlError(SOCKET_NOT_DEFINE_ERR) + } + */ +} diff --git a/packages/ws-base/src/client/generator.js b/packages/ws-base/src/client/generator.js new file mode 100644 index 0000000000000000000000000000000000000000..ef752d0496e44fe9ffdfde50adbc4dd32858c2d9 --- /dev/null +++ b/packages/ws-base/src/client/generator.js @@ -0,0 +1,253 @@ +// generator resolvers +// this will be a mini client server architect +// The reason is when the enableAuth setup - the private route +// might not be validated, but we need the callable point is ready +// therefore this part will always take the contract and generate +// callable api for the developer to setup their front end +// the only thing is - when they call they might get an error or +// NOT_LOGIN_IN and they can react to this error accordingly +import { + JsonqlResolverNotFoundError, + JsonqlValidationError, + JsonqlError, + finalCatch +} from 'jsonql-errors' +import { + validateAsync, + validateSync, + isKeyInObject, + isString +} from 'jsonql-params-validator' +import { + ERROR_TYPE, + DATA_KEY, + ERROR_KEY, + ERROR_PROP_NAME, + MESSAGE_PROP_NAME, + RESULT_PROP_NAME, + SEND_MSG_PROP_NAME, + LOGIN_EVENT_NAME, + READY_PROP_NAME, + LOGOUT_EVENT_NAME +} from 'jsonql-constants' +const { injectToFn, objDefineProps } from 'jsonql-utils' + +import { getDebug, constants, createEvt, toArray } from './utils' +const { EMIT_EVT, NOT_ALLOW_OP, UNKNOWN_RESULT, MY_NAMESPACE } = constants; +const debugFn = getDebug('generator') + +/** + * prepare the methods + * @param {object} opts configuration + * @param {object} nspMap resolvers index by their namespace + * @param {object} ee EventEmitter + * @return {object} of resolvers + * @public + */ +export default function generator(opts, nspMap, ee) { + const obj = {}; + const { nspSet } = nspMap; + for (let namespace in nspSet) { + let list = nspSet[namespace] + for (let resolverName in list) { + let params = list[resolverName] + let fn = createResolver(ee, namespace, resolverName, params) + obj[resolverName] = setupResolver(namespace, resolverName, params, fn, ee) + } + } + // add error handler + createNamespaceErrorHandler(obj, ee, nspSet) + // add onReady handler + createOnReadyhandler(obj, ee, nspSet) + // Auth related methods + createAuthMethods(obj, ee, opts) + // this is a helper method for the developer to know the namespace inside + obj.getNsp = () => { + return Object.keys(nspSet) + } + // output + return obj; +} + +/** + * create the actual function to send message to server + * @param {object} ee EventEmitter instance + * @param {string} namespace this resolver end point + * @param {string} resolverName name of resolver as event name + * @param {object} params from contract + * @return {function} resolver + */ +function createResolver(ee, namespace, resolverName, params) { + // note we pass the new withResult=true option + return function(...args) { + return validateAsync(args, params.params, true) + .then( _args => actionCall(ee, namespace, resolverName, _args) ) + .catch(finalCatch) + } +} + +/** + * just wrapper + * @param {object} ee EventEmitter + * @param {string} namespace where this belongs + * @param {string} resolverName resolver + * @param {array} args arguments + * @return {void} nothing + */ +function actionCall(ee, namespace, resolverName, args = []) { + debugFn(`actionCall: ${namespace} ${resolverName}`, args) + ee.$trigger(createEvt(namespace, EMIT_EVT), [ + resolverName, + toArray(args) + ]) +} + +/** + * break out to use in different places to handle the return from server + * @param {object} data from server + * @param {function} resolver from promise + * @param {function} rejecter from promise + * @return {void} nothing + */ +function respondHandler(data, resolver, rejecter) { + if (isKeyInObject(data, ERROR_KEY)) { + debugFn('rejecter called', data[ERROR_KEY]) + rejecter(data[ERROR_KEY]) + } else if (isKeyInObject(data, DATA_KEY)) { + debugFn('resolver called', data[DATA_KEY]) + resolver(data[DATA_KEY]) + } else { + debugFn('UNKNOWN_RESULT', data) + rejecter({message: UNKNOWN_RESULT, error: data}) + } +} + +/** + * Add extra property to the resolver + * @param {string} namespace where this belongs + * @param {string} resolverName name as event name + * @param {object} params from contract + * @param {function} fn resolver function + * @param {object} ee EventEmitter + * @return {function} resolver + */ +const setupResolver = (namespace, resolverName, params, fn, ee) => { + // also need to setup a getter to get back the namespace of this resolver + let _fn = injectToFn(fn, MY_NAMESPACE, namespace) + // onResult handler + _fn = objDefineProps(_fn, RESULT_PROP_NAME, function(resultCallback) { + if (typeof resultCallback === 'function') { + ee.$only( + createEvt(namespace, resolverName, RESULT_PROP_NAME), + function resultHandler(result) { + respondHandler(result, resultCallback, (error) => { + ee.$trigger(createEvt(namespace, resolverName, ERROR_PROP_NAME), error) + }) + } + ) + } + }) + // we do need to add the send prop back because it's the only way to deal with + // bi-directional data stream + _fn = objDefineProps(_fn, MESSAGE_PROP_NAME, function(messageCallback) { + // we expect this to be a function + if (typeof messageCallback === 'function') { + // did that add to the callback + let onMessageCallback = (args) => { + respondHandler(args, messageCallback, (error) => { + ee.$trigger(createEvt(namespace, resolverName, ERROR_PROP_NAME), error) + }) + } + // register the handler for this message event + ee.$only(createEvt(namespace, resolverName, MESSAGE_PROP_NAME), onMessageCallback) + } + }) + // add an ERROR_PROP_NAME handler + _fn = objDefineProps(_fn, ERROR_PROP_NAME, function(resolverErrorHandler) { + if (typeof resolverErrorHandler === 'function') { + // please note ERROR_PROP_NAME can add multiple listners + ee.$only(createEvt(namespace, resolverName, ERROR_PROP_NAME), resolverErrorHandler) + } + }) + // pairing with the server vesrion SEND_MSG_PROP_NAME + _fn = objDefineProps(_fn, SEND_MSG_PROP_NAME, function(messagePayload) { + const result = validateSync(toArray(messagePayload), params.params, true) + // here is the different we don't throw erro instead we trigger an + // onError + if (result[ERROR_KEY] && result[ERROR_KEY].length) { + ee.$call( + createEvt(namespace, resolverName, ERROR_PROP_NAME), + [JsonqlValidationError(resolverName, result[ERROR_KEY])] + ) + } else { + // there is no return only an action call + actionCall(ee, namespace, resolverName, result[DATA_KEY]) + } + }) + return _fn; +} + +/** + * The problem is the namespace can have more than one + * and we only have on onError message + * @param {object} obj the client itself + * @param {object} ee Event Emitter + * @param {object} nspSet namespace keys + * @return {void} + */ +const createNamespaceErrorHandler = (obj, ee, nspSet) => { + // using the onError as name + // @TODO we should follow the convention earlier + // make this a setter for the obj itself + objDefineProps(obj, ERROR_PROP_NAME, function(namespaceErrorHandler) { + if (typeof namespaceErrorHandler === 'function') { + // please note ERROR_PROP_NAME can add multiple listners + for (let namespace in nspSet) { + // this one is very tricky, we need to make sure the trigger is calling + // with the namespace as well as the error + ee.$on(createEvt(namespace, ERROR_PROP_NAME), namespaceErrorHandler) + } + } + }) +} + +/** + * This event will fire when the socket.io.on('connection') and ws.onopen + * @param {object} obj the client itself + * @param {object} ee Event Emitter + * @param {object} nspSet namespace keys + * @return {void} + */ +const createOnReadyhandler = (obj, ee, nspSet) => { + objDefineProps(obj, READY_PROP_NAME, function(onReadyCallback) { + if (typeof onReadyCallback === 'function') { + // reduce it down to just one flat level + let result = ee.$on(READY_PROP_NAME, onReadyCallback) + } + }) +} + +/** + * Create auth related methods + * @param {object} obj the client itself + * @param {object} ee Event Emitter + * @param {object} opts configuration + * @return {void} + */ +const createAuthMethods = (obj, ee, opts) => { + if (opts.enableAuth) { + // create an additonal login handler + // we require the token + obj[opts.loginHandlerName] = (token) => { + debugFn(opts.loginHandlerName, token) + if (token && isString(token)) { + return ee.$trigger(LOGIN_EVENT_NAME, [token]) + } + throw new JsonqlValidationError(opts.loginHandlerName) + } + // logout event handler + obj[opts.logoutHandlerName] = (...args) => { + ee.$trigger(LOGOUT_EVENT_NAME, args) + } + } +} diff --git a/packages/ws-base/src/client/main.js b/packages/ws-base/src/client/main.js new file mode 100644 index 0000000000000000000000000000000000000000..ad57f157c4a1eaf7e1f9a8075199e9fa1e4d1407 --- /dev/null +++ b/packages/ws-base/src/client/main.js @@ -0,0 +1,47 @@ +// main api to get the ws-client + +import createSocketClient from './create-socket-client' +import generator from './generator' + +import { checkOptions, ee, processContract } from './utils' + +/** + * The main interface to create the wsClient for use + * @param {function} clientGenerator this is an internal way to generate node or browser client + * @return {function} wsClient + * @public + */ +export default function main() { + /** + * @param {object} config configuration + * @param {object} [eventEmitter=false] this will be the bridge between clients + * @return {object} wsClient + */ + const wsClient = (config, eventEmitter = false) => { + return checkOptions(config) + .then(opts => ({ + opts, + nspMap: processContract(opts), + ee: eventEmitter || new ee() + }) + ) + //.then(clientGenerator) // @TODO we don't need this step anymore, remove later + .then( + ({ opts, nspMap, ee }) => createSocketClient(opts, nspMap, ee) + ) + .then( + ({ opts, nspMap, ee }) => generator(opts, nspMap, ee) + ) + .catch(err => { + console.error('jsonql-ws-client init error', err) + }) + } + + // @TODO use the Object.addProperty trick - do we need this anymore? + Object.defineProperty(wsClient, 'CLIENT_TYPE_INFO', { + value: '__PLACEHOLDER__', + writable: false + }) + + return wsClient +} diff --git a/packages/ws-base/src/client/node/client-generator.js b/packages/ws-base/src/client/node/client-generator.js new file mode 100644 index 0000000000000000000000000000000000000000..788a3153df2b53ac2182f4178dc6bba3c3b87027 --- /dev/null +++ b/packages/ws-base/src/client/node/client-generator.js @@ -0,0 +1,53 @@ +// client generator for node.js + +// @TODO move this out of the jsonql-jwt +/* +const { + socketIoNodeHandshakeLogin, + socketIoNodeRoundtripLogin, + socketIoNodeClientAsync +} = require('./socketio-client') + +const { + wsNodeClient, + wsNodeAuthClient +} = require('./ws-client') +*/ + +const { chainPromises } = require('jsonql-jwt') + +const { JsonqlError } = require('jsonql-errors') +const { JS_WS_SOCKET_IO_NAME, JS_WS_NAME } = require('jsonql-constants') +const { isString } = require('jsonql-params-validator') +const debug = require('debug')('jsonql-ws-client:client-generator:cjs') + +/** + * @TODO we have taken out all the socket client to their respective package + * now we need to figure out how to inject them back into this client generator + * websocket client generator + * @param {object} payload with opts, nspMap, ee + * @return {object} same just mutate it + */ +const clientGenerator = ({ opts, nspMap, ee }) => { + // debug(nspMap) + switch (opts.serverType) { + case JS_WS_SOCKET_IO_NAME: + // the socket.io normal client is not Promise so we make them all the same + opts.nspClient = socketIoNodeClientAsync; + // (...args) => Promise.resolve(Reflect.apply(socketIoNodeClient, null, args)) + // we also need to determine the type of socket.io login here + opts.nspAuthClient = isString(opts.useJwt) ? socketIoNodeRoundtripLogin : socketIoNodeHandshakeLogin; + // debug(opts.nspAuthClient) + break; + case JS_WS_NAME: + opts.nspClient = wsNodeClient; + opts.nspAuthClient = wsNodeAuthClient; + break; + default: + throw new JsonqlError(`Unknown serverType: ${opts.serverType}`) + } + return { opts, nspMap, ee } +} + +// export it +module.exports = clientGenerator; diff --git a/packages/ws-base/src/client/node/main.cjs.js b/packages/ws-base/src/client/node/main.cjs.js new file mode 100644 index 0000000000000000000000000000000000000000..f5a8e6ae005af31223bc7d2eddcfe5aaf872fe9c --- /dev/null +++ b/packages/ws-base/src/client/node/main.cjs.js @@ -0,0 +1,7691 @@ +'use strict'; + +function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } + +var debug$2 = _interopDefault(require('debug')); + +/** + * This is a custom error to throw when server throw a 406 + * This help us to capture the right error, due to the call happens in sequence + * @param {string} message to tell what happen + * @param {mixed} extra things we want to add, 500? + */ +var Jsonql406Error = /*@__PURE__*/(function (Error) { + function Jsonql406Error() { + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + Error.apply(this, args); + this.message = args[0]; + this.detail = args[1]; + // We can't access the static name from an instance + // but we can do it like this + this.className = Jsonql406Error.name; + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, Jsonql406Error); + } + } + + if ( Error ) Jsonql406Error.__proto__ = Error; + Jsonql406Error.prototype = Object.create( Error && Error.prototype ); + Jsonql406Error.prototype.constructor = Jsonql406Error; + + var staticAccessors = { statusCode: { configurable: true },name: { configurable: true } }; + + staticAccessors.statusCode.get = function () { + return 406; + }; + + staticAccessors.name.get = function () { + return 'Jsonql406Error'; + }; + + Object.defineProperties( Jsonql406Error, staticAccessors ); + + return Jsonql406Error; +}(Error)); + +/** + * This is a custom error to throw when server throw a 500 + * This help us to capture the right error, due to the call happens in sequence + * @param {string} message to tell what happen + * @param {mixed} extra things we want to add, 500? + */ +var Jsonql500Error = /*@__PURE__*/(function (Error) { + function Jsonql500Error() { + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + Error.apply(this, args); + + this.message = args[0]; + this.detail = args[1]; + + this.className = Jsonql500Error.name; + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, Jsonql500Error); + } + } + + if ( Error ) Jsonql500Error.__proto__ = Error; + Jsonql500Error.prototype = Object.create( Error && Error.prototype ); + Jsonql500Error.prototype.constructor = Jsonql500Error; + + var staticAccessors = { statusCode: { configurable: true },name: { configurable: true } }; + + staticAccessors.statusCode.get = function () { + return 500; + }; + + staticAccessors.name.get = function () { + return 'Jsonql500Error'; + }; + + Object.defineProperties( Jsonql500Error, staticAccessors ); + + return Jsonql500Error; +}(Error)); + +/** + * This is a custom error to throw when pass credential but fail + * This help us to capture the right error, due to the call happens in sequence + * @param {string} message to tell what happen + * @param {mixed} extra things we want to add, 500? + */ +var JsonqlAuthorisationError = /*@__PURE__*/(function (Error) { + function JsonqlAuthorisationError() { + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + Error.apply(this, args); + this.message = args[0]; + this.detail = args[1]; + + this.className = JsonqlAuthorisationError.name; + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, JsonqlAuthorisationError); + } + } + + if ( Error ) JsonqlAuthorisationError.__proto__ = Error; + JsonqlAuthorisationError.prototype = Object.create( Error && Error.prototype ); + JsonqlAuthorisationError.prototype.constructor = JsonqlAuthorisationError; + + var staticAccessors = { statusCode: { configurable: true },name: { configurable: true } }; + + staticAccessors.statusCode.get = function () { + return 401; + }; + + staticAccessors.name.get = function () { + return 'JsonqlAuthorisationError'; + }; + + Object.defineProperties( JsonqlAuthorisationError, staticAccessors ); + + return JsonqlAuthorisationError; +}(Error)); + +/** + * This is a custom error when not supply the credential and try to get contract + * This help us to capture the right error, due to the call happens in sequence + * @param {string} message to tell what happen + * @param {mixed} extra things we want to add, 500? + */ +var JsonqlContractAuthError = /*@__PURE__*/(function (Error) { + function JsonqlContractAuthError() { + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + Error.apply(this, args); + this.message = args[0]; + this.detail = args[1]; + + this.className = JsonqlContractAuthError.name; + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, JsonqlContractAuthError); + } + } + + if ( Error ) JsonqlContractAuthError.__proto__ = Error; + JsonqlContractAuthError.prototype = Object.create( Error && Error.prototype ); + JsonqlContractAuthError.prototype.constructor = JsonqlContractAuthError; + + var staticAccessors = { statusCode: { configurable: true },name: { configurable: true } }; + + staticAccessors.statusCode.get = function () { + return 401; + }; + + staticAccessors.name.get = function () { + return 'JsonqlContractAuthError'; + }; + + Object.defineProperties( JsonqlContractAuthError, staticAccessors ); + + return JsonqlContractAuthError; +}(Error)); + +/** + * This is a custom error to throw when the resolver throw error and capture inside the middleware + * This help us to capture the right error, due to the call happens in sequence + * @param {string} message to tell what happen + * @param {mixed} extra things we want to add, 500? + */ +var JsonqlResolverAppError = /*@__PURE__*/(function (Error) { + function JsonqlResolverAppError() { + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + Error.apply(this, args); + + this.message = args[0]; + this.detail = args[1]; + + this.className = JsonqlResolverAppError.name; + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, JsonqlResolverAppError); + } + } + + if ( Error ) JsonqlResolverAppError.__proto__ = Error; + JsonqlResolverAppError.prototype = Object.create( Error && Error.prototype ); + JsonqlResolverAppError.prototype.constructor = JsonqlResolverAppError; + + var staticAccessors = { statusCode: { configurable: true },name: { configurable: true } }; + + staticAccessors.statusCode.get = function () { + return 500; + }; + + staticAccessors.name.get = function () { + return 'JsonqlResolverAppError'; + }; + + Object.defineProperties( JsonqlResolverAppError, staticAccessors ); + + return JsonqlResolverAppError; +}(Error)); + +/** + * This is a custom error to throw when could not find the resolver + * This help us to capture the right error, due to the call happens in sequence + * @param {string} message to tell what happen + * @param {mixed} extra things we want to add, 500? + */ +var JsonqlResolverNotFoundError = /*@__PURE__*/(function (Error) { + function JsonqlResolverNotFoundError() { + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + Error.apply(this, args); + + this.message = args[0]; + this.detail = args[1]; + + this.className = JsonqlResolverNotFoundError.name; + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, JsonqlResolverNotFoundError); + } + } + + if ( Error ) JsonqlResolverNotFoundError.__proto__ = Error; + JsonqlResolverNotFoundError.prototype = Object.create( Error && Error.prototype ); + JsonqlResolverNotFoundError.prototype.constructor = JsonqlResolverNotFoundError; + + var staticAccessors = { statusCode: { configurable: true },name: { configurable: true } }; + + staticAccessors.statusCode.get = function () { + return 404; + }; + + staticAccessors.name.get = function () { + return 'JsonqlResolverNotFoundError'; + }; + + Object.defineProperties( JsonqlResolverNotFoundError, staticAccessors ); + + return JsonqlResolverNotFoundError; +}(Error)); + +// this get throw from within the checkOptions when run through the enum failed +var JsonqlEnumError = /*@__PURE__*/(function (Error) { + function JsonqlEnumError() { + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + Error.apply(this, args); + + this.message = args[0]; + this.detail = args[1]; + + this.className = JsonqlEnumError.name; + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, JsonqlEnumError); + } + } + + if ( Error ) JsonqlEnumError.__proto__ = Error; + JsonqlEnumError.prototype = Object.create( Error && Error.prototype ); + JsonqlEnumError.prototype.constructor = JsonqlEnumError; + + var staticAccessors = { name: { configurable: true } }; + + staticAccessors.name.get = function () { + return 'JsonqlEnumError'; + }; + + Object.defineProperties( JsonqlEnumError, staticAccessors ); + + return JsonqlEnumError; +}(Error)); + +// this will throw from inside the checkOptions +var JsonqlTypeError = /*@__PURE__*/(function (Error) { + function JsonqlTypeError() { + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + Error.apply(this, args); + + this.message = args[0]; + this.detail = args[1]; + + this.className = JsonqlTypeError.name; + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, JsonqlTypeError); + } + } + + if ( Error ) JsonqlTypeError.__proto__ = Error; + JsonqlTypeError.prototype = Object.create( Error && Error.prototype ); + JsonqlTypeError.prototype.constructor = JsonqlTypeError; + + var staticAccessors = { name: { configurable: true } }; + + staticAccessors.name.get = function () { + return 'JsonqlTypeError'; + }; + + Object.defineProperties( JsonqlTypeError, staticAccessors ); + + return JsonqlTypeError; +}(Error)); + +// allow supply a custom checker function +// if that failed then we throw this error +var JsonqlCheckerError = /*@__PURE__*/(function (Error) { + function JsonqlCheckerError() { + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + Error.apply(this, args); + this.message = args[0]; + this.detail = args[1]; + + this.className = JsonqlCheckerError.name; + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, JsonqlCheckerError); + } + } + + if ( Error ) JsonqlCheckerError.__proto__ = Error; + JsonqlCheckerError.prototype = Object.create( Error && Error.prototype ); + JsonqlCheckerError.prototype.constructor = JsonqlCheckerError; + + var staticAccessors = { name: { configurable: true } }; + + staticAccessors.name.get = function () { + return 'JsonqlCheckerError'; + }; + + Object.defineProperties( JsonqlCheckerError, staticAccessors ); + + return JsonqlCheckerError; +}(Error)); + +// custom validation error class +// when validaton failed +var JsonqlValidationError = /*@__PURE__*/(function (Error) { + function JsonqlValidationError() { + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + Error.apply(this, args); + + this.message = args[0]; + this.detail = args[1]; + + this.className = JsonqlValidationError.name; + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, JsonqlValidationError); + } + } + + if ( Error ) JsonqlValidationError.__proto__ = Error; + JsonqlValidationError.prototype = Object.create( Error && Error.prototype ); + JsonqlValidationError.prototype.constructor = JsonqlValidationError; + + var staticAccessors = { name: { configurable: true } }; + + staticAccessors.name.get = function () { + return 'JsonqlValidationError'; + }; + + Object.defineProperties( JsonqlValidationError, staticAccessors ); + + return JsonqlValidationError; +}(Error)); + +// the core stuff to id if it's calling with jsonql +var DATA_KEY = 'data'; +var ERROR_KEY = 'error'; + +var JSONQL_PATH = 'jsonql'; +var DEFAULT_TYPE = 'any'; + +// @TODO remove this is not in use +// export const CLIENT_CONFIG_FILE = '.clients.json'; +// export const CONTRACT_CONFIG_FILE = 'jsonql-contract-config.js'; +// type of resolvers +var QUERY_NAME = 'query'; +var MUTATION_NAME = 'mutation'; +var SOCKET_NAME = 'socket'; +var QUERY_ARG_NAME = 'args'; +// for contract-cli +var KEY_WORD = 'continue'; + +var TYPE_KEY = 'type'; +var OPTIONAL_KEY = 'optional'; +var ENUM_KEY = 'enumv'; // need to change this because enum is a reserved word +var ARGS_KEY = 'args'; +var CHECKER_KEY = 'checker'; +var ALIAS_KEY = 'alias'; +var LOGIN_NAME = 'login'; +var ISSUER_NAME = LOGIN_NAME; // legacy issue need to replace them later +var LOGOUT_NAME = 'logout'; + +var OR_SEPERATOR = '|'; + +var STRING_TYPE = 'string'; +var BOOLEAN_TYPE = 'boolean'; +var ARRAY_TYPE = 'array'; +var OBJECT_TYPE = 'object'; + +var NUMBER_TYPE = 'number'; +var ARRAY_TYPE_LFT = 'array.<'; +var ARRAY_TYPE_RGT = '>'; + +var NO_ERROR_MSG = 'No message'; +var NO_STATUS_CODE = -1; +var LOGIN_EVENT_NAME = '__login__'; +var LOGOUT_EVENT_NAME = '__logout__'; + +// for ws servers +var WS_REPLY_TYPE = '__reply__'; +var WS_EVT_NAME = '__event__'; +var WS_DATA_NAME = '__data__'; +var EMIT_REPLY_TYPE = 'emit'; +var ACKNOWLEDGE_REPLY_TYPE = 'acknowledge'; +var ERROR_TYPE = 'error'; + +var JS_WS_SOCKET_IO_NAME = 'socket.io'; +var JS_WS_NAME = 'ws'; + +// for ws client +var MESSAGE_PROP_NAME = 'onMessage'; +var RESULT_PROP_NAME = 'onResult'; +var ERROR_PROP_NAME = 'onError'; +var READY_PROP_NAME = 'onReady'; +var SEND_MSG_PROP_NAME = 'send'; +var NOT_LOGIN_ERR_MSG = 'NOT LOGIN'; +var HSA_ALGO = 'HS256'; + +/** + * This is a custom error to throw whenever a error happen inside the jsonql + * This help us to capture the right error, due to the call happens in sequence + * @param {string} message to tell what happen + * @param {mixed} extra things we want to add, 500? + */ +var JsonqlError = /*@__PURE__*/(function (Error) { + function JsonqlError() { + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + Error.apply(this, args); + + this.message = args[0]; + this.detail = args[1]; + + this.className = JsonqlError.name; + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, JsonqlError); + } + } + + if ( Error ) JsonqlError.__proto__ = Error; + JsonqlError.prototype = Object.create( Error && Error.prototype ); + JsonqlError.prototype.constructor = JsonqlError; + + var staticAccessors = { name: { configurable: true },statusCode: { configurable: true } }; + + staticAccessors.name.get = function () { + return 'JsonqlError'; + }; + + staticAccessors.statusCode.get = function () { + return NO_STATUS_CODE; + }; + + Object.defineProperties( JsonqlError, staticAccessors ); + + return JsonqlError; +}(Error)); + +// this is from an example from Koa team to use for internal middleware ctx.throw +// but after the test the res.body part is unable to extract the required data +// I keep this one here for future reference + +var JsonqlServerError = /*@__PURE__*/(function (Error) { + function JsonqlServerError(statusCode, message) { + Error.call(this, message); + this.statusCode = statusCode; + this.className = JsonqlServerError.name; + } + + if ( Error ) JsonqlServerError.__proto__ = Error; + JsonqlServerError.prototype = Object.create( Error && Error.prototype ); + JsonqlServerError.prototype.constructor = JsonqlServerError; + + var staticAccessors = { name: { configurable: true } }; + + staticAccessors.name.get = function () { + return 'JsonqlServerError'; + }; + + Object.defineProperties( JsonqlServerError, staticAccessors ); + + return JsonqlServerError; +}(Error)); + +/** + * this will put into generator call at the very end and catch + * the error throw from inside then throw again + * this is necessary because we split calls inside and the throw + * will not reach the actual client unless we do it this way + * @param {object} e Error + * @return {void} just throw + */ +function finalCatch(e) { + // this is a hack to get around the validateAsync not actually throw error + // instead it just rejected it with the array of failed parameters + if (Array.isArray(e)) { + // if we want the message then I will have to create yet another function + // to wrap this function to provide the name prop + throw new JsonqlValidationError('', e); + } + var msg = e.message || NO_ERROR_MSG; + var detail = e.detail || e; + switch (true) { + case e instanceof Jsonql406Error: + throw new Jsonql406Error(msg, detail); + case e instanceof Jsonql500Error: + throw new Jsonql500Error(msg, detail); + case e instanceof JsonqlAuthorisationError: + throw new JsonqlAuthorisationError(msg, detail); + case e instanceof JsonqlContractAuthError: + throw new JsonqlContractAuthError(msg, detail); + case e instanceof JsonqlResolverAppError: + throw new JsonqlResolverAppError(msg, detail); + case e instanceof JsonqlResolverNotFoundError: + throw new JsonqlResolverNotFoundError(msg, detail); + case e instanceof JsonqlEnumError: + throw new JsonqlEnumError(msg, detail); + case e instanceof JsonqlTypeError: + throw new JsonqlTypeError(msg, detail); + case e instanceof JsonqlCheckerError: + throw new JsonqlCheckerError(msg, detail); + case e instanceof JsonqlValidationError: + throw new JsonqlValidationError(msg, detail); + case e instanceof JsonqlServerError: + throw new JsonqlServerError(msg, detail); + default: + throw new JsonqlError(msg, detail); + } +} + +var global$1 = (typeof global !== "undefined" ? global : + typeof self !== "undefined" ? self : + typeof window !== "undefined" ? window : {}); + +/** Detect free variable `global` from Node.js. */ +var freeGlobal = typeof global$1 == 'object' && global$1 && global$1.Object === Object && global$1; + +/** Detect free variable `self`. */ +var freeSelf = typeof self == 'object' && self && self.Object === Object && self; + +/** Used as a reference to the global object. */ +var root = freeGlobal || freeSelf || Function('return this')(); + +/** Built-in value references. */ +var Symbol = root.Symbol; + +/** Used for built-in method references. */ +var objectProto = Object.prototype; + +/** Used to check objects for own properties. */ +var hasOwnProperty = objectProto.hasOwnProperty; + +/** + * Used to resolve the + * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) + * of values. + */ +var nativeObjectToString = objectProto.toString; + +/** Built-in value references. */ +var symToStringTag = Symbol ? Symbol.toStringTag : undefined; + +/** + * A specialized version of `baseGetTag` which ignores `Symbol.toStringTag` values. + * + * @private + * @param {*} value The value to query. + * @returns {string} Returns the raw `toStringTag`. + */ +function getRawTag(value) { + var isOwn = hasOwnProperty.call(value, symToStringTag), + tag = value[symToStringTag]; + + try { + value[symToStringTag] = undefined; + var unmasked = true; + } catch (e) {} + + var result = nativeObjectToString.call(value); + if (unmasked) { + if (isOwn) { + value[symToStringTag] = tag; + } else { + delete value[symToStringTag]; + } + } + return result; +} + +/** Used for built-in method references. */ +var objectProto$1 = Object.prototype; + +/** + * Used to resolve the + * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) + * of values. + */ +var nativeObjectToString$1 = objectProto$1.toString; + +/** + * Converts `value` to a string using `Object.prototype.toString`. + * + * @private + * @param {*} value The value to convert. + * @returns {string} Returns the converted string. + */ +function objectToString(value) { + return nativeObjectToString$1.call(value); +} + +/** `Object#toString` result references. */ +var nullTag = '[object Null]', + undefinedTag = '[object Undefined]'; + +/** Built-in value references. */ +var symToStringTag$1 = Symbol ? Symbol.toStringTag : undefined; + +/** + * The base implementation of `getTag` without fallbacks for buggy environments. + * + * @private + * @param {*} value The value to query. + * @returns {string} Returns the `toStringTag`. + */ +function baseGetTag(value) { + if (value == null) { + return value === undefined ? undefinedTag : nullTag; + } + return (symToStringTag$1 && symToStringTag$1 in Object(value)) + ? getRawTag(value) + : objectToString(value); +} + +/** + * Checks if `value` is object-like. A value is object-like if it's not `null` + * and has a `typeof` result of "object". + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is object-like, else `false`. + * @example + * + * _.isObjectLike({}); + * // => true + * + * _.isObjectLike([1, 2, 3]); + * // => true + * + * _.isObjectLike(_.noop); + * // => false + * + * _.isObjectLike(null); + * // => false + */ +function isObjectLike(value) { + return value != null && typeof value == 'object'; +} + +/** `Object#toString` result references. */ +var symbolTag = '[object Symbol]'; + +/** + * Checks if `value` is classified as a `Symbol` primitive or object. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a symbol, else `false`. + * @example + * + * _.isSymbol(Symbol.iterator); + * // => true + * + * _.isSymbol('abc'); + * // => false + */ +function isSymbol(value) { + return typeof value == 'symbol' || + (isObjectLike(value) && baseGetTag(value) == symbolTag); +} + +/** + * A specialized version of `_.map` for arrays without support for iteratee + * shorthands. + * + * @private + * @param {Array} [array] The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array} Returns the new mapped array. + */ +function arrayMap(array, iteratee) { + var index = -1, + length = array == null ? 0 : array.length, + result = Array(length); + + while (++index < length) { + result[index] = iteratee(array[index], index, array); + } + return result; +} + +/** + * Checks if `value` is classified as an `Array` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an array, else `false`. + * @example + * + * _.isArray([1, 2, 3]); + * // => true + * + * _.isArray(document.body.children); + * // => false + * + * _.isArray('abc'); + * // => false + * + * _.isArray(_.noop); + * // => false + */ +var isArray = Array.isArray; + +/** Used as references for various `Number` constants. */ +var INFINITY = 1 / 0; + +/** Used to convert symbols to primitives and strings. */ +var symbolProto = Symbol ? Symbol.prototype : undefined, + symbolToString = symbolProto ? symbolProto.toString : undefined; + +/** + * The base implementation of `_.toString` which doesn't convert nullish + * values to empty strings. + * + * @private + * @param {*} value The value to process. + * @returns {string} Returns the string. + */ +function baseToString(value) { + // Exit early for strings to avoid a performance hit in some environments. + if (typeof value == 'string') { + return value; + } + if (isArray(value)) { + // Recursively convert values (susceptible to call stack limits). + return arrayMap(value, baseToString) + ''; + } + if (isSymbol(value)) { + return symbolToString ? symbolToString.call(value) : ''; + } + var result = (value + ''); + return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result; +} + +/** + * Checks if `value` is the + * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) + * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an object, else `false`. + * @example + * + * _.isObject({}); + * // => true + * + * _.isObject([1, 2, 3]); + * // => true + * + * _.isObject(_.noop); + * // => true + * + * _.isObject(null); + * // => false + */ +function isObject(value) { + var type = typeof value; + return value != null && (type == 'object' || type == 'function'); +} + +/** + * This method returns the first argument it receives. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Util + * @param {*} value Any value. + * @returns {*} Returns `value`. + * @example + * + * var object = { 'a': 1 }; + * + * console.log(_.identity(object) === object); + * // => true + */ +function identity(value) { + return value; +} + +/** `Object#toString` result references. */ +var asyncTag = '[object AsyncFunction]', + funcTag = '[object Function]', + genTag = '[object GeneratorFunction]', + proxyTag = '[object Proxy]'; + +/** + * Checks if `value` is classified as a `Function` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a function, else `false`. + * @example + * + * _.isFunction(_); + * // => true + * + * _.isFunction(/abc/); + * // => false + */ +function isFunction(value) { + if (!isObject(value)) { + return false; + } + // The use of `Object#toString` avoids issues with the `typeof` operator + // in Safari 9 which returns 'object' for typed arrays and other constructors. + var tag = baseGetTag(value); + return tag == funcTag || tag == genTag || tag == asyncTag || tag == proxyTag; +} + +/** Used to detect overreaching core-js shims. */ +var coreJsData = root['__core-js_shared__']; + +/** Used to detect methods masquerading as native. */ +var maskSrcKey = (function() { + var uid = /[^.]+$/.exec(coreJsData && coreJsData.keys && coreJsData.keys.IE_PROTO || ''); + return uid ? ('Symbol(src)_1.' + uid) : ''; +}()); + +/** + * Checks if `func` has its source masked. + * + * @private + * @param {Function} func The function to check. + * @returns {boolean} Returns `true` if `func` is masked, else `false`. + */ +function isMasked(func) { + return !!maskSrcKey && (maskSrcKey in func); +} + +/** Used for built-in method references. */ +var funcProto = Function.prototype; + +/** Used to resolve the decompiled source of functions. */ +var funcToString = funcProto.toString; + +/** + * Converts `func` to its source code. + * + * @private + * @param {Function} func The function to convert. + * @returns {string} Returns the source code. + */ +function toSource(func) { + if (func != null) { + try { + return funcToString.call(func); + } catch (e) {} + try { + return (func + ''); + } catch (e) {} + } + return ''; +} + +/** + * Used to match `RegExp` + * [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns). + */ +var reRegExpChar = /[\\^$.*+?()[\]{}|]/g; + +/** Used to detect host constructors (Safari). */ +var reIsHostCtor = /^\[object .+?Constructor\]$/; + +/** Used for built-in method references. */ +var funcProto$1 = Function.prototype, + objectProto$2 = Object.prototype; + +/** Used to resolve the decompiled source of functions. */ +var funcToString$1 = funcProto$1.toString; + +/** Used to check objects for own properties. */ +var hasOwnProperty$1 = objectProto$2.hasOwnProperty; + +/** Used to detect if a method is native. */ +var reIsNative = RegExp('^' + + funcToString$1.call(hasOwnProperty$1).replace(reRegExpChar, '\\$&') + .replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$' +); + +/** + * The base implementation of `_.isNative` without bad shim checks. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a native function, + * else `false`. + */ +function baseIsNative(value) { + if (!isObject(value) || isMasked(value)) { + return false; + } + var pattern = isFunction(value) ? reIsNative : reIsHostCtor; + return pattern.test(toSource(value)); +} + +/** + * Gets the value at `key` of `object`. + * + * @private + * @param {Object} [object] The object to query. + * @param {string} key The key of the property to get. + * @returns {*} Returns the property value. + */ +function getValue(object, key) { + return object == null ? undefined : object[key]; +} + +/** + * Gets the native function at `key` of `object`. + * + * @private + * @param {Object} object The object to query. + * @param {string} key The key of the method to get. + * @returns {*} Returns the function if it's native, else `undefined`. + */ +function getNative(object, key) { + var value = getValue(object, key); + return baseIsNative(value) ? value : undefined; +} + +/* Built-in method references that are verified to be native. */ +var WeakMap$1 = getNative(root, 'WeakMap'); + +/** Built-in value references. */ +var objectCreate = Object.create; + +/** + * The base implementation of `_.create` without support for assigning + * properties to the created object. + * + * @private + * @param {Object} proto The object to inherit from. + * @returns {Object} Returns the new object. + */ +var baseCreate = (function() { + function object() {} + return function(proto) { + if (!isObject(proto)) { + return {}; + } + if (objectCreate) { + return objectCreate(proto); + } + object.prototype = proto; + var result = new object; + object.prototype = undefined; + return result; + }; +}()); + +/** + * A faster alternative to `Function#apply`, this function invokes `func` + * with the `this` binding of `thisArg` and the arguments of `args`. + * + * @private + * @param {Function} func The function to invoke. + * @param {*} thisArg The `this` binding of `func`. + * @param {Array} args The arguments to invoke `func` with. + * @returns {*} Returns the result of `func`. + */ +function apply(func, thisArg, args) { + switch (args.length) { + case 0: return func.call(thisArg); + case 1: return func.call(thisArg, args[0]); + case 2: return func.call(thisArg, args[0], args[1]); + case 3: return func.call(thisArg, args[0], args[1], args[2]); + } + return func.apply(thisArg, args); +} + +/** + * Copies the values of `source` to `array`. + * + * @private + * @param {Array} source The array to copy values from. + * @param {Array} [array=[]] The array to copy values to. + * @returns {Array} Returns `array`. + */ +function copyArray(source, array) { + var index = -1, + length = source.length; + + array || (array = Array(length)); + while (++index < length) { + array[index] = source[index]; + } + return array; +} + +/** Used to detect hot functions by number of calls within a span of milliseconds. */ +var HOT_COUNT = 800, + HOT_SPAN = 16; + +/* Built-in method references for those with the same name as other `lodash` methods. */ +var nativeNow = Date.now; + +/** + * Creates a function that'll short out and invoke `identity` instead + * of `func` when it's called `HOT_COUNT` or more times in `HOT_SPAN` + * milliseconds. + * + * @private + * @param {Function} func The function to restrict. + * @returns {Function} Returns the new shortable function. + */ +function shortOut(func) { + var count = 0, + lastCalled = 0; + + return function() { + var stamp = nativeNow(), + remaining = HOT_SPAN - (stamp - lastCalled); + + lastCalled = stamp; + if (remaining > 0) { + if (++count >= HOT_COUNT) { + return arguments[0]; + } + } else { + count = 0; + } + return func.apply(undefined, arguments); + }; +} + +/** + * Creates a function that returns `value`. + * + * @static + * @memberOf _ + * @since 2.4.0 + * @category Util + * @param {*} value The value to return from the new function. + * @returns {Function} Returns the new constant function. + * @example + * + * var objects = _.times(2, _.constant({ 'a': 1 })); + * + * console.log(objects); + * // => [{ 'a': 1 }, { 'a': 1 }] + * + * console.log(objects[0] === objects[1]); + * // => true + */ +function constant(value) { + return function() { + return value; + }; +} + +var defineProperty = (function() { + try { + var func = getNative(Object, 'defineProperty'); + func({}, '', {}); + return func; + } catch (e) {} +}()); + +/** + * The base implementation of `setToString` without support for hot loop shorting. + * + * @private + * @param {Function} func The function to modify. + * @param {Function} string The `toString` result. + * @returns {Function} Returns `func`. + */ +var baseSetToString = !defineProperty ? identity : function(func, string) { + return defineProperty(func, 'toString', { + 'configurable': true, + 'enumerable': false, + 'value': constant(string), + 'writable': true + }); +}; + +/** + * Sets the `toString` method of `func` to return `string`. + * + * @private + * @param {Function} func The function to modify. + * @param {Function} string The `toString` result. + * @returns {Function} Returns `func`. + */ +var setToString = shortOut(baseSetToString); + +/** + * The base implementation of `_.findIndex` and `_.findLastIndex` without + * support for iteratee shorthands. + * + * @private + * @param {Array} array The array to inspect. + * @param {Function} predicate The function invoked per iteration. + * @param {number} fromIndex The index to search from. + * @param {boolean} [fromRight] Specify iterating from right to left. + * @returns {number} Returns the index of the matched value, else `-1`. + */ +function baseFindIndex(array, predicate, fromIndex, fromRight) { + var length = array.length, + index = fromIndex + (fromRight ? 1 : -1); + + while ((fromRight ? index-- : ++index < length)) { + if (predicate(array[index], index, array)) { + return index; + } + } + return -1; +} + +/** + * The base implementation of `_.isNaN` without support for number objects. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is `NaN`, else `false`. + */ +function baseIsNaN(value) { + return value !== value; +} + +/** + * A specialized version of `_.indexOf` which performs strict equality + * comparisons of values, i.e. `===`. + * + * @private + * @param {Array} array The array to inspect. + * @param {*} value The value to search for. + * @param {number} fromIndex The index to search from. + * @returns {number} Returns the index of the matched value, else `-1`. + */ +function strictIndexOf(array, value, fromIndex) { + var index = fromIndex - 1, + length = array.length; + + while (++index < length) { + if (array[index] === value) { + return index; + } + } + return -1; +} + +/** + * The base implementation of `_.indexOf` without `fromIndex` bounds checks. + * + * @private + * @param {Array} array The array to inspect. + * @param {*} value The value to search for. + * @param {number} fromIndex The index to search from. + * @returns {number} Returns the index of the matched value, else `-1`. + */ +function baseIndexOf(array, value, fromIndex) { + return value === value + ? strictIndexOf(array, value, fromIndex) + : baseFindIndex(array, baseIsNaN, fromIndex); +} + +/** Used as references for various `Number` constants. */ +var MAX_SAFE_INTEGER = 9007199254740991; + +/** Used to detect unsigned integer values. */ +var reIsUint = /^(?:0|[1-9]\d*)$/; + +/** + * Checks if `value` is a valid array-like index. + * + * @private + * @param {*} value The value to check. + * @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index. + * @returns {boolean} Returns `true` if `value` is a valid index, else `false`. + */ +function isIndex(value, length) { + var type = typeof value; + length = length == null ? MAX_SAFE_INTEGER : length; + + return !!length && + (type == 'number' || + (type != 'symbol' && reIsUint.test(value))) && + (value > -1 && value % 1 == 0 && value < length); +} + +/** + * The base implementation of `assignValue` and `assignMergeValue` without + * value checks. + * + * @private + * @param {Object} object The object to modify. + * @param {string} key The key of the property to assign. + * @param {*} value The value to assign. + */ +function baseAssignValue(object, key, value) { + if (key == '__proto__' && defineProperty) { + defineProperty(object, key, { + 'configurable': true, + 'enumerable': true, + 'value': value, + 'writable': true + }); + } else { + object[key] = value; + } +} + +/** + * Performs a + * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) + * comparison between two values to determine if they are equivalent. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @returns {boolean} Returns `true` if the values are equivalent, else `false`. + * @example + * + * var object = { 'a': 1 }; + * var other = { 'a': 1 }; + * + * _.eq(object, object); + * // => true + * + * _.eq(object, other); + * // => false + * + * _.eq('a', 'a'); + * // => true + * + * _.eq('a', Object('a')); + * // => false + * + * _.eq(NaN, NaN); + * // => true + */ +function eq(value, other) { + return value === other || (value !== value && other !== other); +} + +/** Used for built-in method references. */ +var objectProto$3 = Object.prototype; + +/** Used to check objects for own properties. */ +var hasOwnProperty$2 = objectProto$3.hasOwnProperty; + +/** + * Assigns `value` to `key` of `object` if the existing value is not equivalent + * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) + * for equality comparisons. + * + * @private + * @param {Object} object The object to modify. + * @param {string} key The key of the property to assign. + * @param {*} value The value to assign. + */ +function assignValue(object, key, value) { + var objValue = object[key]; + if (!(hasOwnProperty$2.call(object, key) && eq(objValue, value)) || + (value === undefined && !(key in object))) { + baseAssignValue(object, key, value); + } +} + +/** + * Copies properties of `source` to `object`. + * + * @private + * @param {Object} source The object to copy properties from. + * @param {Array} props The property identifiers to copy. + * @param {Object} [object={}] The object to copy properties to. + * @param {Function} [customizer] The function to customize copied values. + * @returns {Object} Returns `object`. + */ +function copyObject(source, props, object, customizer) { + var isNew = !object; + object || (object = {}); + + var index = -1, + length = props.length; + + while (++index < length) { + var key = props[index]; + + var newValue = customizer + ? customizer(object[key], source[key], key, object, source) + : undefined; + + if (newValue === undefined) { + newValue = source[key]; + } + if (isNew) { + baseAssignValue(object, key, newValue); + } else { + assignValue(object, key, newValue); + } + } + return object; +} + +/* Built-in method references for those with the same name as other `lodash` methods. */ +var nativeMax = Math.max; + +/** + * A specialized version of `baseRest` which transforms the rest array. + * + * @private + * @param {Function} func The function to apply a rest parameter to. + * @param {number} [start=func.length-1] The start position of the rest parameter. + * @param {Function} transform The rest array transform. + * @returns {Function} Returns the new function. + */ +function overRest(func, start, transform) { + start = nativeMax(start === undefined ? (func.length - 1) : start, 0); + return function() { + var args = arguments, + index = -1, + length = nativeMax(args.length - start, 0), + array = Array(length); + + while (++index < length) { + array[index] = args[start + index]; + } + index = -1; + var otherArgs = Array(start + 1); + while (++index < start) { + otherArgs[index] = args[index]; + } + otherArgs[start] = transform(array); + return apply(func, this, otherArgs); + }; +} + +/** + * The base implementation of `_.rest` which doesn't validate or coerce arguments. + * + * @private + * @param {Function} func The function to apply a rest parameter to. + * @param {number} [start=func.length-1] The start position of the rest parameter. + * @returns {Function} Returns the new function. + */ +function baseRest(func, start) { + return setToString(overRest(func, start, identity), func + ''); +} + +/** Used as references for various `Number` constants. */ +var MAX_SAFE_INTEGER$1 = 9007199254740991; + +/** + * Checks if `value` is a valid array-like length. + * + * **Note:** This method is loosely based on + * [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a valid length, else `false`. + * @example + * + * _.isLength(3); + * // => true + * + * _.isLength(Number.MIN_VALUE); + * // => false + * + * _.isLength(Infinity); + * // => false + * + * _.isLength('3'); + * // => false + */ +function isLength(value) { + return typeof value == 'number' && + value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER$1; +} + +/** + * Checks if `value` is array-like. A value is considered array-like if it's + * not a function and has a `value.length` that's an integer greater than or + * equal to `0` and less than or equal to `Number.MAX_SAFE_INTEGER`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is array-like, else `false`. + * @example + * + * _.isArrayLike([1, 2, 3]); + * // => true + * + * _.isArrayLike(document.body.children); + * // => true + * + * _.isArrayLike('abc'); + * // => true + * + * _.isArrayLike(_.noop); + * // => false + */ +function isArrayLike(value) { + return value != null && isLength(value.length) && !isFunction(value); +} + +/** + * Checks if the given arguments are from an iteratee call. + * + * @private + * @param {*} value The potential iteratee value argument. + * @param {*} index The potential iteratee index or key argument. + * @param {*} object The potential iteratee object argument. + * @returns {boolean} Returns `true` if the arguments are from an iteratee call, + * else `false`. + */ +function isIterateeCall(value, index, object) { + if (!isObject(object)) { + return false; + } + var type = typeof index; + if (type == 'number' + ? (isArrayLike(object) && isIndex(index, object.length)) + : (type == 'string' && index in object) + ) { + return eq(object[index], value); + } + return false; +} + +/** + * Creates a function like `_.assign`. + * + * @private + * @param {Function} assigner The function to assign values. + * @returns {Function} Returns the new assigner function. + */ +function createAssigner(assigner) { + return baseRest(function(object, sources) { + var index = -1, + length = sources.length, + customizer = length > 1 ? sources[length - 1] : undefined, + guard = length > 2 ? sources[2] : undefined; + + customizer = (assigner.length > 3 && typeof customizer == 'function') + ? (length--, customizer) + : undefined; + + if (guard && isIterateeCall(sources[0], sources[1], guard)) { + customizer = length < 3 ? undefined : customizer; + length = 1; + } + object = Object(object); + while (++index < length) { + var source = sources[index]; + if (source) { + assigner(object, source, index, customizer); + } + } + return object; + }); +} + +/** Used for built-in method references. */ +var objectProto$4 = Object.prototype; + +/** + * Checks if `value` is likely a prototype object. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a prototype, else `false`. + */ +function isPrototype(value) { + var Ctor = value && value.constructor, + proto = (typeof Ctor == 'function' && Ctor.prototype) || objectProto$4; + + return value === proto; +} + +/** + * The base implementation of `_.times` without support for iteratee shorthands + * or max array length checks. + * + * @private + * @param {number} n The number of times to invoke `iteratee`. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array} Returns the array of results. + */ +function baseTimes(n, iteratee) { + var index = -1, + result = Array(n); + + while (++index < n) { + result[index] = iteratee(index); + } + return result; +} + +/** `Object#toString` result references. */ +var argsTag = '[object Arguments]'; + +/** + * The base implementation of `_.isArguments`. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an `arguments` object, + */ +function baseIsArguments(value) { + return isObjectLike(value) && baseGetTag(value) == argsTag; +} + +/** Used for built-in method references. */ +var objectProto$5 = Object.prototype; + +/** Used to check objects for own properties. */ +var hasOwnProperty$3 = objectProto$5.hasOwnProperty; + +/** Built-in value references. */ +var propertyIsEnumerable = objectProto$5.propertyIsEnumerable; + +/** + * Checks if `value` is likely an `arguments` object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an `arguments` object, + * else `false`. + * @example + * + * _.isArguments(function() { return arguments; }()); + * // => true + * + * _.isArguments([1, 2, 3]); + * // => false + */ +var isArguments = baseIsArguments(function() { return arguments; }()) ? baseIsArguments : function(value) { + return isObjectLike(value) && hasOwnProperty$3.call(value, 'callee') && + !propertyIsEnumerable.call(value, 'callee'); +}; + +/** + * This method returns `false`. + * + * @static + * @memberOf _ + * @since 4.13.0 + * @category Util + * @returns {boolean} Returns `false`. + * @example + * + * _.times(2, _.stubFalse); + * // => [false, false] + */ +function stubFalse() { + return false; +} + +/** Detect free variable `exports`. */ +var freeExports = typeof exports == 'object' && exports && !exports.nodeType && exports; + +/** Detect free variable `module`. */ +var freeModule = freeExports && typeof module == 'object' && module && !module.nodeType && module; + +/** Detect the popular CommonJS extension `module.exports`. */ +var moduleExports = freeModule && freeModule.exports === freeExports; + +/** Built-in value references. */ +var Buffer = moduleExports ? root.Buffer : undefined; + +/* Built-in method references for those with the same name as other `lodash` methods. */ +var nativeIsBuffer = Buffer ? Buffer.isBuffer : undefined; + +/** + * Checks if `value` is a buffer. + * + * @static + * @memberOf _ + * @since 4.3.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a buffer, else `false`. + * @example + * + * _.isBuffer(new Buffer(2)); + * // => true + * + * _.isBuffer(new Uint8Array(2)); + * // => false + */ +var isBuffer = nativeIsBuffer || stubFalse; + +/** `Object#toString` result references. */ +var argsTag$1 = '[object Arguments]', + arrayTag = '[object Array]', + boolTag = '[object Boolean]', + dateTag = '[object Date]', + errorTag = '[object Error]', + funcTag$1 = '[object Function]', + mapTag = '[object Map]', + numberTag = '[object Number]', + objectTag = '[object Object]', + regexpTag = '[object RegExp]', + setTag = '[object Set]', + stringTag = '[object String]', + weakMapTag = '[object WeakMap]'; + +var arrayBufferTag = '[object ArrayBuffer]', + dataViewTag = '[object DataView]', + float32Tag = '[object Float32Array]', + float64Tag = '[object Float64Array]', + int8Tag = '[object Int8Array]', + int16Tag = '[object Int16Array]', + int32Tag = '[object Int32Array]', + uint8Tag = '[object Uint8Array]', + uint8ClampedTag = '[object Uint8ClampedArray]', + uint16Tag = '[object Uint16Array]', + uint32Tag = '[object Uint32Array]'; + +/** Used to identify `toStringTag` values of typed arrays. */ +var typedArrayTags = {}; +typedArrayTags[float32Tag] = typedArrayTags[float64Tag] = +typedArrayTags[int8Tag] = typedArrayTags[int16Tag] = +typedArrayTags[int32Tag] = typedArrayTags[uint8Tag] = +typedArrayTags[uint8ClampedTag] = typedArrayTags[uint16Tag] = +typedArrayTags[uint32Tag] = true; +typedArrayTags[argsTag$1] = typedArrayTags[arrayTag] = +typedArrayTags[arrayBufferTag] = typedArrayTags[boolTag] = +typedArrayTags[dataViewTag] = typedArrayTags[dateTag] = +typedArrayTags[errorTag] = typedArrayTags[funcTag$1] = +typedArrayTags[mapTag] = typedArrayTags[numberTag] = +typedArrayTags[objectTag] = typedArrayTags[regexpTag] = +typedArrayTags[setTag] = typedArrayTags[stringTag] = +typedArrayTags[weakMapTag] = false; + +/** + * The base implementation of `_.isTypedArray` without Node.js optimizations. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a typed array, else `false`. + */ +function baseIsTypedArray(value) { + return isObjectLike(value) && + isLength(value.length) && !!typedArrayTags[baseGetTag(value)]; +} + +/** + * The base implementation of `_.unary` without support for storing metadata. + * + * @private + * @param {Function} func The function to cap arguments for. + * @returns {Function} Returns the new capped function. + */ +function baseUnary(func) { + return function(value) { + return func(value); + }; +} + +/** Detect free variable `exports`. */ +var freeExports$1 = typeof exports == 'object' && exports && !exports.nodeType && exports; + +/** Detect free variable `module`. */ +var freeModule$1 = freeExports$1 && typeof module == 'object' && module && !module.nodeType && module; + +/** Detect the popular CommonJS extension `module.exports`. */ +var moduleExports$1 = freeModule$1 && freeModule$1.exports === freeExports$1; + +/** Detect free variable `process` from Node.js. */ +var freeProcess = moduleExports$1 && freeGlobal.process; + +/** Used to access faster Node.js helpers. */ +var nodeUtil = (function() { + try { + // Use `util.types` for Node.js 10+. + var types = freeModule$1 && freeModule$1.require && freeModule$1.require('util').types; + + if (types) { + return types; + } + + // Legacy `process.binding('util')` for Node.js < 10. + return freeProcess && freeProcess.binding && freeProcess.binding('util'); + } catch (e) {} +}()); + +/* Node.js helper references. */ +var nodeIsTypedArray = nodeUtil && nodeUtil.isTypedArray; + +/** + * Checks if `value` is classified as a typed array. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a typed array, else `false`. + * @example + * + * _.isTypedArray(new Uint8Array); + * // => true + * + * _.isTypedArray([]); + * // => false + */ +var isTypedArray = nodeIsTypedArray ? baseUnary(nodeIsTypedArray) : baseIsTypedArray; + +/** Used for built-in method references. */ +var objectProto$6 = Object.prototype; + +/** Used to check objects for own properties. */ +var hasOwnProperty$4 = objectProto$6.hasOwnProperty; + +/** + * Creates an array of the enumerable property names of the array-like `value`. + * + * @private + * @param {*} value The value to query. + * @param {boolean} inherited Specify returning inherited property names. + * @returns {Array} Returns the array of property names. + */ +function arrayLikeKeys(value, inherited) { + var isArr = isArray(value), + isArg = !isArr && isArguments(value), + isBuff = !isArr && !isArg && isBuffer(value), + isType = !isArr && !isArg && !isBuff && isTypedArray(value), + skipIndexes = isArr || isArg || isBuff || isType, + result = skipIndexes ? baseTimes(value.length, String) : [], + length = result.length; + + for (var key in value) { + if ((inherited || hasOwnProperty$4.call(value, key)) && + !(skipIndexes && ( + // Safari 9 has enumerable `arguments.length` in strict mode. + key == 'length' || + // Node.js 0.10 has enumerable non-index properties on buffers. + (isBuff && (key == 'offset' || key == 'parent')) || + // PhantomJS 2 has enumerable non-index properties on typed arrays. + (isType && (key == 'buffer' || key == 'byteLength' || key == 'byteOffset')) || + // Skip index properties. + isIndex(key, length) + ))) { + result.push(key); + } + } + return result; +} + +/** + * Creates a unary function that invokes `func` with its argument transformed. + * + * @private + * @param {Function} func The function to wrap. + * @param {Function} transform The argument transform. + * @returns {Function} Returns the new function. + */ +function overArg(func, transform) { + return function(arg) { + return func(transform(arg)); + }; +} + +/* Built-in method references for those with the same name as other `lodash` methods. */ +var nativeKeys = overArg(Object.keys, Object); + +/** Used for built-in method references. */ +var objectProto$7 = Object.prototype; + +/** Used to check objects for own properties. */ +var hasOwnProperty$5 = objectProto$7.hasOwnProperty; + +/** + * The base implementation of `_.keys` which doesn't treat sparse arrays as dense. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names. + */ +function baseKeys(object) { + if (!isPrototype(object)) { + return nativeKeys(object); + } + var result = []; + for (var key in Object(object)) { + if (hasOwnProperty$5.call(object, key) && key != 'constructor') { + result.push(key); + } + } + return result; +} + +/** + * Creates an array of the own enumerable property names of `object`. + * + * **Note:** Non-object values are coerced to objects. See the + * [ES spec](http://ecma-international.org/ecma-262/7.0/#sec-object.keys) + * for more details. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Object + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names. + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.keys(new Foo); + * // => ['a', 'b'] (iteration order is not guaranteed) + * + * _.keys('hi'); + * // => ['0', '1'] + */ +function keys(object) { + return isArrayLike(object) ? arrayLikeKeys(object) : baseKeys(object); +} + +/** + * This function is like + * [`Object.keys`](http://ecma-international.org/ecma-262/7.0/#sec-object.keys) + * except that it includes inherited enumerable properties. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names. + */ +function nativeKeysIn(object) { + var result = []; + if (object != null) { + for (var key in Object(object)) { + result.push(key); + } + } + return result; +} + +/** Used for built-in method references. */ +var objectProto$8 = Object.prototype; + +/** Used to check objects for own properties. */ +var hasOwnProperty$6 = objectProto$8.hasOwnProperty; + +/** + * The base implementation of `_.keysIn` which doesn't treat sparse arrays as dense. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names. + */ +function baseKeysIn(object) { + if (!isObject(object)) { + return nativeKeysIn(object); + } + var isProto = isPrototype(object), + result = []; + + for (var key in object) { + if (!(key == 'constructor' && (isProto || !hasOwnProperty$6.call(object, key)))) { + result.push(key); + } + } + return result; +} + +/** + * Creates an array of the own and inherited enumerable property names of `object`. + * + * **Note:** Non-object values are coerced to objects. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Object + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names. + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.keysIn(new Foo); + * // => ['a', 'b', 'c'] (iteration order is not guaranteed) + */ +function keysIn(object) { + return isArrayLike(object) ? arrayLikeKeys(object, true) : baseKeysIn(object); +} + +/** Used to match property names within property paths. */ +var reIsDeepProp = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/, + reIsPlainProp = /^\w*$/; + +/** + * Checks if `value` is a property name and not a property path. + * + * @private + * @param {*} value The value to check. + * @param {Object} [object] The object to query keys on. + * @returns {boolean} Returns `true` if `value` is a property name, else `false`. + */ +function isKey(value, object) { + if (isArray(value)) { + return false; + } + var type = typeof value; + if (type == 'number' || type == 'symbol' || type == 'boolean' || + value == null || isSymbol(value)) { + return true; + } + return reIsPlainProp.test(value) || !reIsDeepProp.test(value) || + (object != null && value in Object(object)); +} + +/* Built-in method references that are verified to be native. */ +var nativeCreate = getNative(Object, 'create'); + +/** + * Removes all key-value entries from the hash. + * + * @private + * @name clear + * @memberOf Hash + */ +function hashClear() { + this.__data__ = nativeCreate ? nativeCreate(null) : {}; + this.size = 0; +} + +/** + * Removes `key` and its value from the hash. + * + * @private + * @name delete + * @memberOf Hash + * @param {Object} hash The hash to modify. + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ +function hashDelete(key) { + var result = this.has(key) && delete this.__data__[key]; + this.size -= result ? 1 : 0; + return result; +} + +/** Used to stand-in for `undefined` hash values. */ +var HASH_UNDEFINED = '__lodash_hash_undefined__'; + +/** Used for built-in method references. */ +var objectProto$9 = Object.prototype; + +/** Used to check objects for own properties. */ +var hasOwnProperty$7 = objectProto$9.hasOwnProperty; + +/** + * Gets the hash value for `key`. + * + * @private + * @name get + * @memberOf Hash + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ +function hashGet(key) { + var data = this.__data__; + if (nativeCreate) { + var result = data[key]; + return result === HASH_UNDEFINED ? undefined : result; + } + return hasOwnProperty$7.call(data, key) ? data[key] : undefined; +} + +/** Used for built-in method references. */ +var objectProto$a = Object.prototype; + +/** Used to check objects for own properties. */ +var hasOwnProperty$8 = objectProto$a.hasOwnProperty; + +/** + * Checks if a hash value for `key` exists. + * + * @private + * @name has + * @memberOf Hash + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ +function hashHas(key) { + var data = this.__data__; + return nativeCreate ? (data[key] !== undefined) : hasOwnProperty$8.call(data, key); +} + +/** Used to stand-in for `undefined` hash values. */ +var HASH_UNDEFINED$1 = '__lodash_hash_undefined__'; + +/** + * Sets the hash `key` to `value`. + * + * @private + * @name set + * @memberOf Hash + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the hash instance. + */ +function hashSet(key, value) { + var data = this.__data__; + this.size += this.has(key) ? 0 : 1; + data[key] = (nativeCreate && value === undefined) ? HASH_UNDEFINED$1 : value; + return this; +} + +/** + * Creates a hash object. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ +function Hash(entries) { + var index = -1, + length = entries == null ? 0 : entries.length; + + this.clear(); + while (++index < length) { + var entry = entries[index]; + this.set(entry[0], entry[1]); + } +} + +// Add methods to `Hash`. +Hash.prototype.clear = hashClear; +Hash.prototype['delete'] = hashDelete; +Hash.prototype.get = hashGet; +Hash.prototype.has = hashHas; +Hash.prototype.set = hashSet; + +/** + * Removes all key-value entries from the list cache. + * + * @private + * @name clear + * @memberOf ListCache + */ +function listCacheClear() { + this.__data__ = []; + this.size = 0; +} + +/** + * Gets the index at which the `key` is found in `array` of key-value pairs. + * + * @private + * @param {Array} array The array to inspect. + * @param {*} key The key to search for. + * @returns {number} Returns the index of the matched value, else `-1`. + */ +function assocIndexOf(array, key) { + var length = array.length; + while (length--) { + if (eq(array[length][0], key)) { + return length; + } + } + return -1; +} + +/** Used for built-in method references. */ +var arrayProto = Array.prototype; + +/** Built-in value references. */ +var splice = arrayProto.splice; + +/** + * Removes `key` and its value from the list cache. + * + * @private + * @name delete + * @memberOf ListCache + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ +function listCacheDelete(key) { + var data = this.__data__, + index = assocIndexOf(data, key); + + if (index < 0) { + return false; + } + var lastIndex = data.length - 1; + if (index == lastIndex) { + data.pop(); + } else { + splice.call(data, index, 1); + } + --this.size; + return true; +} + +/** + * Gets the list cache value for `key`. + * + * @private + * @name get + * @memberOf ListCache + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ +function listCacheGet(key) { + var data = this.__data__, + index = assocIndexOf(data, key); + + return index < 0 ? undefined : data[index][1]; +} + +/** + * Checks if a list cache value for `key` exists. + * + * @private + * @name has + * @memberOf ListCache + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ +function listCacheHas(key) { + return assocIndexOf(this.__data__, key) > -1; +} + +/** + * Sets the list cache `key` to `value`. + * + * @private + * @name set + * @memberOf ListCache + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the list cache instance. + */ +function listCacheSet(key, value) { + var data = this.__data__, + index = assocIndexOf(data, key); + + if (index < 0) { + ++this.size; + data.push([key, value]); + } else { + data[index][1] = value; + } + return this; +} + +/** + * Creates an list cache object. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ +function ListCache(entries) { + var index = -1, + length = entries == null ? 0 : entries.length; + + this.clear(); + while (++index < length) { + var entry = entries[index]; + this.set(entry[0], entry[1]); + } +} + +// Add methods to `ListCache`. +ListCache.prototype.clear = listCacheClear; +ListCache.prototype['delete'] = listCacheDelete; +ListCache.prototype.get = listCacheGet; +ListCache.prototype.has = listCacheHas; +ListCache.prototype.set = listCacheSet; + +/* Built-in method references that are verified to be native. */ +var Map$1 = getNative(root, 'Map'); + +/** + * Removes all key-value entries from the map. + * + * @private + * @name clear + * @memberOf MapCache + */ +function mapCacheClear() { + this.size = 0; + this.__data__ = { + 'hash': new Hash, + 'map': new (Map$1 || ListCache), + 'string': new Hash + }; +} + +/** + * Checks if `value` is suitable for use as unique object key. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is suitable, else `false`. + */ +function isKeyable(value) { + var type = typeof value; + return (type == 'string' || type == 'number' || type == 'symbol' || type == 'boolean') + ? (value !== '__proto__') + : (value === null); +} + +/** + * Gets the data for `map`. + * + * @private + * @param {Object} map The map to query. + * @param {string} key The reference key. + * @returns {*} Returns the map data. + */ +function getMapData(map, key) { + var data = map.__data__; + return isKeyable(key) + ? data[typeof key == 'string' ? 'string' : 'hash'] + : data.map; +} + +/** + * Removes `key` and its value from the map. + * + * @private + * @name delete + * @memberOf MapCache + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ +function mapCacheDelete(key) { + var result = getMapData(this, key)['delete'](key); + this.size -= result ? 1 : 0; + return result; +} + +/** + * Gets the map value for `key`. + * + * @private + * @name get + * @memberOf MapCache + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ +function mapCacheGet(key) { + return getMapData(this, key).get(key); +} + +/** + * Checks if a map value for `key` exists. + * + * @private + * @name has + * @memberOf MapCache + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ +function mapCacheHas(key) { + return getMapData(this, key).has(key); +} + +/** + * Sets the map `key` to `value`. + * + * @private + * @name set + * @memberOf MapCache + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the map cache instance. + */ +function mapCacheSet(key, value) { + var data = getMapData(this, key), + size = data.size; + + data.set(key, value); + this.size += data.size == size ? 0 : 1; + return this; +} + +/** + * Creates a map cache object to store key-value pairs. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ +function MapCache(entries) { + var index = -1, + length = entries == null ? 0 : entries.length; + + this.clear(); + while (++index < length) { + var entry = entries[index]; + this.set(entry[0], entry[1]); + } +} + +// Add methods to `MapCache`. +MapCache.prototype.clear = mapCacheClear; +MapCache.prototype['delete'] = mapCacheDelete; +MapCache.prototype.get = mapCacheGet; +MapCache.prototype.has = mapCacheHas; +MapCache.prototype.set = mapCacheSet; + +/** Error message constants. */ +var FUNC_ERROR_TEXT = 'Expected a function'; + +/** + * Creates a function that memoizes the result of `func`. If `resolver` is + * provided, it determines the cache key for storing the result based on the + * arguments provided to the memoized function. By default, the first argument + * provided to the memoized function is used as the map cache key. The `func` + * is invoked with the `this` binding of the memoized function. + * + * **Note:** The cache is exposed as the `cache` property on the memoized + * function. Its creation may be customized by replacing the `_.memoize.Cache` + * constructor with one whose instances implement the + * [`Map`](http://ecma-international.org/ecma-262/7.0/#sec-properties-of-the-map-prototype-object) + * method interface of `clear`, `delete`, `get`, `has`, and `set`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Function + * @param {Function} func The function to have its output memoized. + * @param {Function} [resolver] The function to resolve the cache key. + * @returns {Function} Returns the new memoized function. + * @example + * + * var object = { 'a': 1, 'b': 2 }; + * var other = { 'c': 3, 'd': 4 }; + * + * var values = _.memoize(_.values); + * values(object); + * // => [1, 2] + * + * values(other); + * // => [3, 4] + * + * object.a = 2; + * values(object); + * // => [1, 2] + * + * // Modify the result cache. + * values.cache.set(object, ['a', 'b']); + * values(object); + * // => ['a', 'b'] + * + * // Replace `_.memoize.Cache`. + * _.memoize.Cache = WeakMap; + */ +function memoize(func, resolver) { + if (typeof func != 'function' || (resolver != null && typeof resolver != 'function')) { + throw new TypeError(FUNC_ERROR_TEXT); + } + var memoized = function() { + var args = arguments, + key = resolver ? resolver.apply(this, args) : args[0], + cache = memoized.cache; + + if (cache.has(key)) { + return cache.get(key); + } + var result = func.apply(this, args); + memoized.cache = cache.set(key, result) || cache; + return result; + }; + memoized.cache = new (memoize.Cache || MapCache); + return memoized; +} + +// Expose `MapCache`. +memoize.Cache = MapCache; + +/** Used as the maximum memoize cache size. */ +var MAX_MEMOIZE_SIZE = 500; + +/** + * A specialized version of `_.memoize` which clears the memoized function's + * cache when it exceeds `MAX_MEMOIZE_SIZE`. + * + * @private + * @param {Function} func The function to have its output memoized. + * @returns {Function} Returns the new memoized function. + */ +function memoizeCapped(func) { + var result = memoize(func, function(key) { + if (cache.size === MAX_MEMOIZE_SIZE) { + cache.clear(); + } + return key; + }); + + var cache = result.cache; + return result; +} + +/** Used to match property names within property paths. */ +var rePropName = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g; + +/** Used to match backslashes in property paths. */ +var reEscapeChar = /\\(\\)?/g; + +/** + * Converts `string` to a property path array. + * + * @private + * @param {string} string The string to convert. + * @returns {Array} Returns the property path array. + */ +var stringToPath = memoizeCapped(function(string) { + var result = []; + if (string.charCodeAt(0) === 46 /* . */) { + result.push(''); + } + string.replace(rePropName, function(match, number, quote, subString) { + result.push(quote ? subString.replace(reEscapeChar, '$1') : (number || match)); + }); + return result; +}); + +/** + * Converts `value` to a string. An empty string is returned for `null` + * and `undefined` values. The sign of `-0` is preserved. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to convert. + * @returns {string} Returns the converted string. + * @example + * + * _.toString(null); + * // => '' + * + * _.toString(-0); + * // => '-0' + * + * _.toString([1, 2, 3]); + * // => '1,2,3' + */ +function toString(value) { + return value == null ? '' : baseToString(value); +} + +/** + * Casts `value` to a path array if it's not one. + * + * @private + * @param {*} value The value to inspect. + * @param {Object} [object] The object to query keys on. + * @returns {Array} Returns the cast property path array. + */ +function castPath(value, object) { + if (isArray(value)) { + return value; + } + return isKey(value, object) ? [value] : stringToPath(toString(value)); +} + +/** Used as references for various `Number` constants. */ +var INFINITY$1 = 1 / 0; + +/** + * Converts `value` to a string key if it's not a string or symbol. + * + * @private + * @param {*} value The value to inspect. + * @returns {string|symbol} Returns the key. + */ +function toKey(value) { + if (typeof value == 'string' || isSymbol(value)) { + return value; + } + var result = (value + ''); + return (result == '0' && (1 / value) == -INFINITY$1) ? '-0' : result; +} + +/** + * The base implementation of `_.get` without support for default values. + * + * @private + * @param {Object} object The object to query. + * @param {Array|string} path The path of the property to get. + * @returns {*} Returns the resolved value. + */ +function baseGet(object, path) { + path = castPath(path, object); + + var index = 0, + length = path.length; + + while (object != null && index < length) { + object = object[toKey(path[index++])]; + } + return (index && index == length) ? object : undefined; +} + +/** + * Gets the value at `path` of `object`. If the resolved value is + * `undefined`, the `defaultValue` is returned in its place. + * + * @static + * @memberOf _ + * @since 3.7.0 + * @category Object + * @param {Object} object The object to query. + * @param {Array|string} path The path of the property to get. + * @param {*} [defaultValue] The value returned for `undefined` resolved values. + * @returns {*} Returns the resolved value. + * @example + * + * var object = { 'a': [{ 'b': { 'c': 3 } }] }; + * + * _.get(object, 'a[0].b.c'); + * // => 3 + * + * _.get(object, ['a', '0', 'b', 'c']); + * // => 3 + * + * _.get(object, 'a.b.c', 'default'); + * // => 'default' + */ +function get(object, path, defaultValue) { + var result = object == null ? undefined : baseGet(object, path); + return result === undefined ? defaultValue : result; +} + +/** + * Appends the elements of `values` to `array`. + * + * @private + * @param {Array} array The array to modify. + * @param {Array} values The values to append. + * @returns {Array} Returns `array`. + */ +function arrayPush(array, values) { + var index = -1, + length = values.length, + offset = array.length; + + while (++index < length) { + array[offset + index] = values[index]; + } + return array; +} + +/** Built-in value references. */ +var getPrototype = overArg(Object.getPrototypeOf, Object); + +/** `Object#toString` result references. */ +var objectTag$1 = '[object Object]'; + +/** Used for built-in method references. */ +var funcProto$2 = Function.prototype, + objectProto$b = Object.prototype; + +/** Used to resolve the decompiled source of functions. */ +var funcToString$2 = funcProto$2.toString; + +/** Used to check objects for own properties. */ +var hasOwnProperty$9 = objectProto$b.hasOwnProperty; + +/** Used to infer the `Object` constructor. */ +var objectCtorString = funcToString$2.call(Object); + +/** + * Checks if `value` is a plain object, that is, an object created by the + * `Object` constructor or one with a `[[Prototype]]` of `null`. + * + * @static + * @memberOf _ + * @since 0.8.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a plain object, else `false`. + * @example + * + * function Foo() { + * this.a = 1; + * } + * + * _.isPlainObject(new Foo); + * // => false + * + * _.isPlainObject([1, 2, 3]); + * // => false + * + * _.isPlainObject({ 'x': 0, 'y': 0 }); + * // => true + * + * _.isPlainObject(Object.create(null)); + * // => true + */ +function isPlainObject(value) { + if (!isObjectLike(value) || baseGetTag(value) != objectTag$1) { + return false; + } + var proto = getPrototype(value); + if (proto === null) { + return true; + } + var Ctor = hasOwnProperty$9.call(proto, 'constructor') && proto.constructor; + return typeof Ctor == 'function' && Ctor instanceof Ctor && + funcToString$2.call(Ctor) == objectCtorString; +} + +/** + * The base implementation of `_.slice` without an iteratee call guard. + * + * @private + * @param {Array} array The array to slice. + * @param {number} [start=0] The start position. + * @param {number} [end=array.length] The end position. + * @returns {Array} Returns the slice of `array`. + */ +function baseSlice(array, start, end) { + var index = -1, + length = array.length; + + if (start < 0) { + start = -start > length ? 0 : (length + start); + } + end = end > length ? length : end; + if (end < 0) { + end += length; + } + length = start > end ? 0 : ((end - start) >>> 0); + start >>>= 0; + + var result = Array(length); + while (++index < length) { + result[index] = array[index + start]; + } + return result; +} + +/** + * Casts `array` to a slice if it's needed. + * + * @private + * @param {Array} array The array to inspect. + * @param {number} start The start position. + * @param {number} [end=array.length] The end position. + * @returns {Array} Returns the cast slice. + */ +function castSlice(array, start, end) { + var length = array.length; + end = end === undefined ? length : end; + return (!start && end >= length) ? array : baseSlice(array, start, end); +} + +/** Used to compose unicode character classes. */ +var rsAstralRange = '\\ud800-\\udfff', + rsComboMarksRange = '\\u0300-\\u036f', + reComboHalfMarksRange = '\\ufe20-\\ufe2f', + rsComboSymbolsRange = '\\u20d0-\\u20ff', + rsComboRange = rsComboMarksRange + reComboHalfMarksRange + rsComboSymbolsRange, + rsVarRange = '\\ufe0e\\ufe0f'; + +/** Used to compose unicode capture groups. */ +var rsZWJ = '\\u200d'; + +/** Used to detect strings with [zero-width joiners or code points from the astral planes](http://eev.ee/blog/2015/09/12/dark-corners-of-unicode/). */ +var reHasUnicode = RegExp('[' + rsZWJ + rsAstralRange + rsComboRange + rsVarRange + ']'); + +/** + * Checks if `string` contains Unicode symbols. + * + * @private + * @param {string} string The string to inspect. + * @returns {boolean} Returns `true` if a symbol is found, else `false`. + */ +function hasUnicode(string) { + return reHasUnicode.test(string); +} + +/** + * Converts an ASCII `string` to an array. + * + * @private + * @param {string} string The string to convert. + * @returns {Array} Returns the converted array. + */ +function asciiToArray(string) { + return string.split(''); +} + +/** Used to compose unicode character classes. */ +var rsAstralRange$1 = '\\ud800-\\udfff', + rsComboMarksRange$1 = '\\u0300-\\u036f', + reComboHalfMarksRange$1 = '\\ufe20-\\ufe2f', + rsComboSymbolsRange$1 = '\\u20d0-\\u20ff', + rsComboRange$1 = rsComboMarksRange$1 + reComboHalfMarksRange$1 + rsComboSymbolsRange$1, + rsVarRange$1 = '\\ufe0e\\ufe0f'; + +/** Used to compose unicode capture groups. */ +var rsAstral = '[' + rsAstralRange$1 + ']', + rsCombo = '[' + rsComboRange$1 + ']', + rsFitz = '\\ud83c[\\udffb-\\udfff]', + rsModifier = '(?:' + rsCombo + '|' + rsFitz + ')', + rsNonAstral = '[^' + rsAstralRange$1 + ']', + rsRegional = '(?:\\ud83c[\\udde6-\\uddff]){2}', + rsSurrPair = '[\\ud800-\\udbff][\\udc00-\\udfff]', + rsZWJ$1 = '\\u200d'; + +/** Used to compose unicode regexes. */ +var reOptMod = rsModifier + '?', + rsOptVar = '[' + rsVarRange$1 + ']?', + rsOptJoin = '(?:' + rsZWJ$1 + '(?:' + [rsNonAstral, rsRegional, rsSurrPair].join('|') + ')' + rsOptVar + reOptMod + ')*', + rsSeq = rsOptVar + reOptMod + rsOptJoin, + rsSymbol = '(?:' + [rsNonAstral + rsCombo + '?', rsCombo, rsRegional, rsSurrPair, rsAstral].join('|') + ')'; + +/** Used to match [string symbols](https://mathiasbynens.be/notes/javascript-unicode). */ +var reUnicode = RegExp(rsFitz + '(?=' + rsFitz + ')|' + rsSymbol + rsSeq, 'g'); + +/** + * Converts a Unicode `string` to an array. + * + * @private + * @param {string} string The string to convert. + * @returns {Array} Returns the converted array. + */ +function unicodeToArray(string) { + return string.match(reUnicode) || []; +} + +/** + * Converts `string` to an array. + * + * @private + * @param {string} string The string to convert. + * @returns {Array} Returns the converted array. + */ +function stringToArray(string) { + return hasUnicode(string) + ? unicodeToArray(string) + : asciiToArray(string); +} + +/** + * Removes all key-value entries from the stack. + * + * @private + * @name clear + * @memberOf Stack + */ +function stackClear() { + this.__data__ = new ListCache; + this.size = 0; +} + +/** + * Removes `key` and its value from the stack. + * + * @private + * @name delete + * @memberOf Stack + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed, else `false`. + */ +function stackDelete(key) { + var data = this.__data__, + result = data['delete'](key); + + this.size = data.size; + return result; +} + +/** + * Gets the stack value for `key`. + * + * @private + * @name get + * @memberOf Stack + * @param {string} key The key of the value to get. + * @returns {*} Returns the entry value. + */ +function stackGet(key) { + return this.__data__.get(key); +} + +/** + * Checks if a stack value for `key` exists. + * + * @private + * @name has + * @memberOf Stack + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ +function stackHas(key) { + return this.__data__.has(key); +} + +/** Used as the size to enable large array optimizations. */ +var LARGE_ARRAY_SIZE = 200; + +/** + * Sets the stack `key` to `value`. + * + * @private + * @name set + * @memberOf Stack + * @param {string} key The key of the value to set. + * @param {*} value The value to set. + * @returns {Object} Returns the stack cache instance. + */ +function stackSet(key, value) { + var data = this.__data__; + if (data instanceof ListCache) { + var pairs = data.__data__; + if (!Map$1 || (pairs.length < LARGE_ARRAY_SIZE - 1)) { + pairs.push([key, value]); + this.size = ++data.size; + return this; + } + data = this.__data__ = new MapCache(pairs); + } + data.set(key, value); + this.size = data.size; + return this; +} + +/** + * Creates a stack cache object to store key-value pairs. + * + * @private + * @constructor + * @param {Array} [entries] The key-value pairs to cache. + */ +function Stack(entries) { + var data = this.__data__ = new ListCache(entries); + this.size = data.size; +} + +// Add methods to `Stack`. +Stack.prototype.clear = stackClear; +Stack.prototype['delete'] = stackDelete; +Stack.prototype.get = stackGet; +Stack.prototype.has = stackHas; +Stack.prototype.set = stackSet; + +/** Detect free variable `exports`. */ +var freeExports$2 = typeof exports == 'object' && exports && !exports.nodeType && exports; + +/** Detect free variable `module`. */ +var freeModule$2 = freeExports$2 && typeof module == 'object' && module && !module.nodeType && module; + +/** Detect the popular CommonJS extension `module.exports`. */ +var moduleExports$2 = freeModule$2 && freeModule$2.exports === freeExports$2; + +/** Built-in value references. */ +var Buffer$1 = moduleExports$2 ? root.Buffer : undefined, + allocUnsafe = Buffer$1 ? Buffer$1.allocUnsafe : undefined; + +/** + * Creates a clone of `buffer`. + * + * @private + * @param {Buffer} buffer The buffer to clone. + * @param {boolean} [isDeep] Specify a deep clone. + * @returns {Buffer} Returns the cloned buffer. + */ +function cloneBuffer(buffer, isDeep) { + if (isDeep) { + return buffer.slice(); + } + var length = buffer.length, + result = allocUnsafe ? allocUnsafe(length) : new buffer.constructor(length); + + buffer.copy(result); + return result; +} + +/** + * A specialized version of `_.filter` for arrays without support for + * iteratee shorthands. + * + * @private + * @param {Array} [array] The array to iterate over. + * @param {Function} predicate The function invoked per iteration. + * @returns {Array} Returns the new filtered array. + */ +function arrayFilter(array, predicate) { + var index = -1, + length = array == null ? 0 : array.length, + resIndex = 0, + result = []; + + while (++index < length) { + var value = array[index]; + if (predicate(value, index, array)) { + result[resIndex++] = value; + } + } + return result; +} + +/** + * This method returns a new empty array. + * + * @static + * @memberOf _ + * @since 4.13.0 + * @category Util + * @returns {Array} Returns the new empty array. + * @example + * + * var arrays = _.times(2, _.stubArray); + * + * console.log(arrays); + * // => [[], []] + * + * console.log(arrays[0] === arrays[1]); + * // => false + */ +function stubArray() { + return []; +} + +/** Used for built-in method references. */ +var objectProto$c = Object.prototype; + +/** Built-in value references. */ +var propertyIsEnumerable$1 = objectProto$c.propertyIsEnumerable; + +/* Built-in method references for those with the same name as other `lodash` methods. */ +var nativeGetSymbols = Object.getOwnPropertySymbols; + +/** + * Creates an array of the own enumerable symbols of `object`. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of symbols. + */ +var getSymbols = !nativeGetSymbols ? stubArray : function(object) { + if (object == null) { + return []; + } + object = Object(object); + return arrayFilter(nativeGetSymbols(object), function(symbol) { + return propertyIsEnumerable$1.call(object, symbol); + }); +}; + +/* Built-in method references for those with the same name as other `lodash` methods. */ +var nativeGetSymbols$1 = Object.getOwnPropertySymbols; + +/** + * Creates an array of the own and inherited enumerable symbols of `object`. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of symbols. + */ +var getSymbolsIn = !nativeGetSymbols$1 ? stubArray : function(object) { + var result = []; + while (object) { + arrayPush(result, getSymbols(object)); + object = getPrototype(object); + } + return result; +}; + +/** + * The base implementation of `getAllKeys` and `getAllKeysIn` which uses + * `keysFunc` and `symbolsFunc` to get the enumerable property names and + * symbols of `object`. + * + * @private + * @param {Object} object The object to query. + * @param {Function} keysFunc The function to get the keys of `object`. + * @param {Function} symbolsFunc The function to get the symbols of `object`. + * @returns {Array} Returns the array of property names and symbols. + */ +function baseGetAllKeys(object, keysFunc, symbolsFunc) { + var result = keysFunc(object); + return isArray(object) ? result : arrayPush(result, symbolsFunc(object)); +} + +/** + * Creates an array of own enumerable property names and symbols of `object`. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names and symbols. + */ +function getAllKeys(object) { + return baseGetAllKeys(object, keys, getSymbols); +} + +/** + * Creates an array of own and inherited enumerable property names and + * symbols of `object`. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names and symbols. + */ +function getAllKeysIn(object) { + return baseGetAllKeys(object, keysIn, getSymbolsIn); +} + +/* Built-in method references that are verified to be native. */ +var DataView = getNative(root, 'DataView'); + +/* Built-in method references that are verified to be native. */ +var Promise$1 = getNative(root, 'Promise'); + +/* Built-in method references that are verified to be native. */ +var Set$1 = getNative(root, 'Set'); + +/** `Object#toString` result references. */ +var mapTag$1 = '[object Map]', + objectTag$2 = '[object Object]', + promiseTag = '[object Promise]', + setTag$1 = '[object Set]', + weakMapTag$1 = '[object WeakMap]'; + +var dataViewTag$1 = '[object DataView]'; + +/** Used to detect maps, sets, and weakmaps. */ +var dataViewCtorString = toSource(DataView), + mapCtorString = toSource(Map$1), + promiseCtorString = toSource(Promise$1), + setCtorString = toSource(Set$1), + weakMapCtorString = toSource(WeakMap$1); + +/** + * Gets the `toStringTag` of `value`. + * + * @private + * @param {*} value The value to query. + * @returns {string} Returns the `toStringTag`. + */ +var getTag = baseGetTag; + +// Fallback for data views, maps, sets, and weak maps in IE 11 and promises in Node.js < 6. +if ((DataView && getTag(new DataView(new ArrayBuffer(1))) != dataViewTag$1) || + (Map$1 && getTag(new Map$1) != mapTag$1) || + (Promise$1 && getTag(Promise$1.resolve()) != promiseTag) || + (Set$1 && getTag(new Set$1) != setTag$1) || + (WeakMap$1 && getTag(new WeakMap$1) != weakMapTag$1)) { + getTag = function(value) { + var result = baseGetTag(value), + Ctor = result == objectTag$2 ? value.constructor : undefined, + ctorString = Ctor ? toSource(Ctor) : ''; + + if (ctorString) { + switch (ctorString) { + case dataViewCtorString: return dataViewTag$1; + case mapCtorString: return mapTag$1; + case promiseCtorString: return promiseTag; + case setCtorString: return setTag$1; + case weakMapCtorString: return weakMapTag$1; + } + } + return result; + }; +} + +var getTag$1 = getTag; + +/** Built-in value references. */ +var Uint8Array = root.Uint8Array; + +/** + * Creates a clone of `arrayBuffer`. + * + * @private + * @param {ArrayBuffer} arrayBuffer The array buffer to clone. + * @returns {ArrayBuffer} Returns the cloned array buffer. + */ +function cloneArrayBuffer(arrayBuffer) { + var result = new arrayBuffer.constructor(arrayBuffer.byteLength); + new Uint8Array(result).set(new Uint8Array(arrayBuffer)); + return result; +} + +/** + * Creates a clone of `typedArray`. + * + * @private + * @param {Object} typedArray The typed array to clone. + * @param {boolean} [isDeep] Specify a deep clone. + * @returns {Object} Returns the cloned typed array. + */ +function cloneTypedArray(typedArray, isDeep) { + var buffer = isDeep ? cloneArrayBuffer(typedArray.buffer) : typedArray.buffer; + return new typedArray.constructor(buffer, typedArray.byteOffset, typedArray.length); +} + +/** + * Initializes an object clone. + * + * @private + * @param {Object} object The object to clone. + * @returns {Object} Returns the initialized clone. + */ +function initCloneObject(object) { + return (typeof object.constructor == 'function' && !isPrototype(object)) + ? baseCreate(getPrototype(object)) + : {}; +} + +/** Used to stand-in for `undefined` hash values. */ +var HASH_UNDEFINED$2 = '__lodash_hash_undefined__'; + +/** + * Adds `value` to the array cache. + * + * @private + * @name add + * @memberOf SetCache + * @alias push + * @param {*} value The value to cache. + * @returns {Object} Returns the cache instance. + */ +function setCacheAdd(value) { + this.__data__.set(value, HASH_UNDEFINED$2); + return this; +} + +/** + * Checks if `value` is in the array cache. + * + * @private + * @name has + * @memberOf SetCache + * @param {*} value The value to search for. + * @returns {number} Returns `true` if `value` is found, else `false`. + */ +function setCacheHas(value) { + return this.__data__.has(value); +} + +/** + * + * Creates an array cache object to store unique values. + * + * @private + * @constructor + * @param {Array} [values] The values to cache. + */ +function SetCache(values) { + var index = -1, + length = values == null ? 0 : values.length; + + this.__data__ = new MapCache; + while (++index < length) { + this.add(values[index]); + } +} + +// Add methods to `SetCache`. +SetCache.prototype.add = SetCache.prototype.push = setCacheAdd; +SetCache.prototype.has = setCacheHas; + +/** + * A specialized version of `_.some` for arrays without support for iteratee + * shorthands. + * + * @private + * @param {Array} [array] The array to iterate over. + * @param {Function} predicate The function invoked per iteration. + * @returns {boolean} Returns `true` if any element passes the predicate check, + * else `false`. + */ +function arraySome(array, predicate) { + var index = -1, + length = array == null ? 0 : array.length; + + while (++index < length) { + if (predicate(array[index], index, array)) { + return true; + } + } + return false; +} + +/** + * Checks if a `cache` value for `key` exists. + * + * @private + * @param {Object} cache The cache to query. + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ +function cacheHas(cache, key) { + return cache.has(key); +} + +/** Used to compose bitmasks for value comparisons. */ +var COMPARE_PARTIAL_FLAG = 1, + COMPARE_UNORDERED_FLAG = 2; + +/** + * A specialized version of `baseIsEqualDeep` for arrays with support for + * partial deep comparisons. + * + * @private + * @param {Array} array The array to compare. + * @param {Array} other The other array to compare. + * @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details. + * @param {Function} customizer The function to customize comparisons. + * @param {Function} equalFunc The function to determine equivalents of values. + * @param {Object} stack Tracks traversed `array` and `other` objects. + * @returns {boolean} Returns `true` if the arrays are equivalent, else `false`. + */ +function equalArrays(array, other, bitmask, customizer, equalFunc, stack) { + var isPartial = bitmask & COMPARE_PARTIAL_FLAG, + arrLength = array.length, + othLength = other.length; + + if (arrLength != othLength && !(isPartial && othLength > arrLength)) { + return false; + } + // Assume cyclic values are equal. + var stacked = stack.get(array); + if (stacked && stack.get(other)) { + return stacked == other; + } + var index = -1, + result = true, + seen = (bitmask & COMPARE_UNORDERED_FLAG) ? new SetCache : undefined; + + stack.set(array, other); + stack.set(other, array); + + // Ignore non-index properties. + while (++index < arrLength) { + var arrValue = array[index], + othValue = other[index]; + + if (customizer) { + var compared = isPartial + ? customizer(othValue, arrValue, index, other, array, stack) + : customizer(arrValue, othValue, index, array, other, stack); + } + if (compared !== undefined) { + if (compared) { + continue; + } + result = false; + break; + } + // Recursively compare arrays (susceptible to call stack limits). + if (seen) { + if (!arraySome(other, function(othValue, othIndex) { + if (!cacheHas(seen, othIndex) && + (arrValue === othValue || equalFunc(arrValue, othValue, bitmask, customizer, stack))) { + return seen.push(othIndex); + } + })) { + result = false; + break; + } + } else if (!( + arrValue === othValue || + equalFunc(arrValue, othValue, bitmask, customizer, stack) + )) { + result = false; + break; + } + } + stack['delete'](array); + stack['delete'](other); + return result; +} + +/** + * Converts `map` to its key-value pairs. + * + * @private + * @param {Object} map The map to convert. + * @returns {Array} Returns the key-value pairs. + */ +function mapToArray(map) { + var index = -1, + result = Array(map.size); + + map.forEach(function(value, key) { + result[++index] = [key, value]; + }); + return result; +} + +/** + * Converts `set` to an array of its values. + * + * @private + * @param {Object} set The set to convert. + * @returns {Array} Returns the values. + */ +function setToArray(set) { + var index = -1, + result = Array(set.size); + + set.forEach(function(value) { + result[++index] = value; + }); + return result; +} + +/** Used to compose bitmasks for value comparisons. */ +var COMPARE_PARTIAL_FLAG$1 = 1, + COMPARE_UNORDERED_FLAG$1 = 2; + +/** `Object#toString` result references. */ +var boolTag$1 = '[object Boolean]', + dateTag$1 = '[object Date]', + errorTag$1 = '[object Error]', + mapTag$2 = '[object Map]', + numberTag$1 = '[object Number]', + regexpTag$1 = '[object RegExp]', + setTag$2 = '[object Set]', + stringTag$1 = '[object String]', + symbolTag$1 = '[object Symbol]'; + +var arrayBufferTag$1 = '[object ArrayBuffer]', + dataViewTag$2 = '[object DataView]'; + +/** Used to convert symbols to primitives and strings. */ +var symbolProto$1 = Symbol ? Symbol.prototype : undefined, + symbolValueOf = symbolProto$1 ? symbolProto$1.valueOf : undefined; + +/** + * A specialized version of `baseIsEqualDeep` for comparing objects of + * the same `toStringTag`. + * + * **Note:** This function only supports comparing values with tags of + * `Boolean`, `Date`, `Error`, `Number`, `RegExp`, or `String`. + * + * @private + * @param {Object} object The object to compare. + * @param {Object} other The other object to compare. + * @param {string} tag The `toStringTag` of the objects to compare. + * @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details. + * @param {Function} customizer The function to customize comparisons. + * @param {Function} equalFunc The function to determine equivalents of values. + * @param {Object} stack Tracks traversed `object` and `other` objects. + * @returns {boolean} Returns `true` if the objects are equivalent, else `false`. + */ +function equalByTag(object, other, tag, bitmask, customizer, equalFunc, stack) { + switch (tag) { + case dataViewTag$2: + if ((object.byteLength != other.byteLength) || + (object.byteOffset != other.byteOffset)) { + return false; + } + object = object.buffer; + other = other.buffer; + + case arrayBufferTag$1: + if ((object.byteLength != other.byteLength) || + !equalFunc(new Uint8Array(object), new Uint8Array(other))) { + return false; + } + return true; + + case boolTag$1: + case dateTag$1: + case numberTag$1: + // Coerce booleans to `1` or `0` and dates to milliseconds. + // Invalid dates are coerced to `NaN`. + return eq(+object, +other); + + case errorTag$1: + return object.name == other.name && object.message == other.message; + + case regexpTag$1: + case stringTag$1: + // Coerce regexes to strings and treat strings, primitives and objects, + // as equal. See http://www.ecma-international.org/ecma-262/7.0/#sec-regexp.prototype.tostring + // for more details. + return object == (other + ''); + + case mapTag$2: + var convert = mapToArray; + + case setTag$2: + var isPartial = bitmask & COMPARE_PARTIAL_FLAG$1; + convert || (convert = setToArray); + + if (object.size != other.size && !isPartial) { + return false; + } + // Assume cyclic values are equal. + var stacked = stack.get(object); + if (stacked) { + return stacked == other; + } + bitmask |= COMPARE_UNORDERED_FLAG$1; + + // Recursively compare objects (susceptible to call stack limits). + stack.set(object, other); + var result = equalArrays(convert(object), convert(other), bitmask, customizer, equalFunc, stack); + stack['delete'](object); + return result; + + case symbolTag$1: + if (symbolValueOf) { + return symbolValueOf.call(object) == symbolValueOf.call(other); + } + } + return false; +} + +/** Used to compose bitmasks for value comparisons. */ +var COMPARE_PARTIAL_FLAG$2 = 1; + +/** Used for built-in method references. */ +var objectProto$d = Object.prototype; + +/** Used to check objects for own properties. */ +var hasOwnProperty$a = objectProto$d.hasOwnProperty; + +/** + * A specialized version of `baseIsEqualDeep` for objects with support for + * partial deep comparisons. + * + * @private + * @param {Object} object The object to compare. + * @param {Object} other The other object to compare. + * @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details. + * @param {Function} customizer The function to customize comparisons. + * @param {Function} equalFunc The function to determine equivalents of values. + * @param {Object} stack Tracks traversed `object` and `other` objects. + * @returns {boolean} Returns `true` if the objects are equivalent, else `false`. + */ +function equalObjects(object, other, bitmask, customizer, equalFunc, stack) { + var isPartial = bitmask & COMPARE_PARTIAL_FLAG$2, + objProps = getAllKeys(object), + objLength = objProps.length, + othProps = getAllKeys(other), + othLength = othProps.length; + + if (objLength != othLength && !isPartial) { + return false; + } + var index = objLength; + while (index--) { + var key = objProps[index]; + if (!(isPartial ? key in other : hasOwnProperty$a.call(other, key))) { + return false; + } + } + // Assume cyclic values are equal. + var stacked = stack.get(object); + if (stacked && stack.get(other)) { + return stacked == other; + } + var result = true; + stack.set(object, other); + stack.set(other, object); + + var skipCtor = isPartial; + while (++index < objLength) { + key = objProps[index]; + var objValue = object[key], + othValue = other[key]; + + if (customizer) { + var compared = isPartial + ? customizer(othValue, objValue, key, other, object, stack) + : customizer(objValue, othValue, key, object, other, stack); + } + // Recursively compare objects (susceptible to call stack limits). + if (!(compared === undefined + ? (objValue === othValue || equalFunc(objValue, othValue, bitmask, customizer, stack)) + : compared + )) { + result = false; + break; + } + skipCtor || (skipCtor = key == 'constructor'); + } + if (result && !skipCtor) { + var objCtor = object.constructor, + othCtor = other.constructor; + + // Non `Object` object instances with different constructors are not equal. + if (objCtor != othCtor && + ('constructor' in object && 'constructor' in other) && + !(typeof objCtor == 'function' && objCtor instanceof objCtor && + typeof othCtor == 'function' && othCtor instanceof othCtor)) { + result = false; + } + } + stack['delete'](object); + stack['delete'](other); + return result; +} + +/** Used to compose bitmasks for value comparisons. */ +var COMPARE_PARTIAL_FLAG$3 = 1; + +/** `Object#toString` result references. */ +var argsTag$2 = '[object Arguments]', + arrayTag$1 = '[object Array]', + objectTag$3 = '[object Object]'; + +/** Used for built-in method references. */ +var objectProto$e = Object.prototype; + +/** Used to check objects for own properties. */ +var hasOwnProperty$b = objectProto$e.hasOwnProperty; + +/** + * A specialized version of `baseIsEqual` for arrays and objects which performs + * deep comparisons and tracks traversed objects enabling objects with circular + * references to be compared. + * + * @private + * @param {Object} object The object to compare. + * @param {Object} other The other object to compare. + * @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details. + * @param {Function} customizer The function to customize comparisons. + * @param {Function} equalFunc The function to determine equivalents of values. + * @param {Object} [stack] Tracks traversed `object` and `other` objects. + * @returns {boolean} Returns `true` if the objects are equivalent, else `false`. + */ +function baseIsEqualDeep(object, other, bitmask, customizer, equalFunc, stack) { + var objIsArr = isArray(object), + othIsArr = isArray(other), + objTag = objIsArr ? arrayTag$1 : getTag$1(object), + othTag = othIsArr ? arrayTag$1 : getTag$1(other); + + objTag = objTag == argsTag$2 ? objectTag$3 : objTag; + othTag = othTag == argsTag$2 ? objectTag$3 : othTag; + + var objIsObj = objTag == objectTag$3, + othIsObj = othTag == objectTag$3, + isSameTag = objTag == othTag; + + if (isSameTag && isBuffer(object)) { + if (!isBuffer(other)) { + return false; + } + objIsArr = true; + objIsObj = false; + } + if (isSameTag && !objIsObj) { + stack || (stack = new Stack); + return (objIsArr || isTypedArray(object)) + ? equalArrays(object, other, bitmask, customizer, equalFunc, stack) + : equalByTag(object, other, objTag, bitmask, customizer, equalFunc, stack); + } + if (!(bitmask & COMPARE_PARTIAL_FLAG$3)) { + var objIsWrapped = objIsObj && hasOwnProperty$b.call(object, '__wrapped__'), + othIsWrapped = othIsObj && hasOwnProperty$b.call(other, '__wrapped__'); + + if (objIsWrapped || othIsWrapped) { + var objUnwrapped = objIsWrapped ? object.value() : object, + othUnwrapped = othIsWrapped ? other.value() : other; + + stack || (stack = new Stack); + return equalFunc(objUnwrapped, othUnwrapped, bitmask, customizer, stack); + } + } + if (!isSameTag) { + return false; + } + stack || (stack = new Stack); + return equalObjects(object, other, bitmask, customizer, equalFunc, stack); +} + +/** + * The base implementation of `_.isEqual` which supports partial comparisons + * and tracks traversed objects. + * + * @private + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @param {boolean} bitmask The bitmask flags. + * 1 - Unordered comparison + * 2 - Partial comparison + * @param {Function} [customizer] The function to customize comparisons. + * @param {Object} [stack] Tracks traversed `value` and `other` objects. + * @returns {boolean} Returns `true` if the values are equivalent, else `false`. + */ +function baseIsEqual(value, other, bitmask, customizer, stack) { + if (value === other) { + return true; + } + if (value == null || other == null || (!isObjectLike(value) && !isObjectLike(other))) { + return value !== value && other !== other; + } + return baseIsEqualDeep(value, other, bitmask, customizer, baseIsEqual, stack); +} + +/** Used to compose bitmasks for value comparisons. */ +var COMPARE_PARTIAL_FLAG$4 = 1, + COMPARE_UNORDERED_FLAG$2 = 2; + +/** + * The base implementation of `_.isMatch` without support for iteratee shorthands. + * + * @private + * @param {Object} object The object to inspect. + * @param {Object} source The object of property values to match. + * @param {Array} matchData The property names, values, and compare flags to match. + * @param {Function} [customizer] The function to customize comparisons. + * @returns {boolean} Returns `true` if `object` is a match, else `false`. + */ +function baseIsMatch(object, source, matchData, customizer) { + var index = matchData.length, + length = index, + noCustomizer = !customizer; + + if (object == null) { + return !length; + } + object = Object(object); + while (index--) { + var data = matchData[index]; + if ((noCustomizer && data[2]) + ? data[1] !== object[data[0]] + : !(data[0] in object) + ) { + return false; + } + } + while (++index < length) { + data = matchData[index]; + var key = data[0], + objValue = object[key], + srcValue = data[1]; + + if (noCustomizer && data[2]) { + if (objValue === undefined && !(key in object)) { + return false; + } + } else { + var stack = new Stack; + if (customizer) { + var result = customizer(objValue, srcValue, key, object, source, stack); + } + if (!(result === undefined + ? baseIsEqual(srcValue, objValue, COMPARE_PARTIAL_FLAG$4 | COMPARE_UNORDERED_FLAG$2, customizer, stack) + : result + )) { + return false; + } + } + } + return true; +} + +/** + * Checks if `value` is suitable for strict equality comparisons, i.e. `===`. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` if suitable for strict + * equality comparisons, else `false`. + */ +function isStrictComparable(value) { + return value === value && !isObject(value); +} + +/** + * Gets the property names, values, and compare flags of `object`. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the match data of `object`. + */ +function getMatchData(object) { + var result = keys(object), + length = result.length; + + while (length--) { + var key = result[length], + value = object[key]; + + result[length] = [key, value, isStrictComparable(value)]; + } + return result; +} + +/** + * A specialized version of `matchesProperty` for source values suitable + * for strict equality comparisons, i.e. `===`. + * + * @private + * @param {string} key The key of the property to get. + * @param {*} srcValue The value to match. + * @returns {Function} Returns the new spec function. + */ +function matchesStrictComparable(key, srcValue) { + return function(object) { + if (object == null) { + return false; + } + return object[key] === srcValue && + (srcValue !== undefined || (key in Object(object))); + }; +} + +/** + * The base implementation of `_.matches` which doesn't clone `source`. + * + * @private + * @param {Object} source The object of property values to match. + * @returns {Function} Returns the new spec function. + */ +function baseMatches(source) { + var matchData = getMatchData(source); + if (matchData.length == 1 && matchData[0][2]) { + return matchesStrictComparable(matchData[0][0], matchData[0][1]); + } + return function(object) { + return object === source || baseIsMatch(object, source, matchData); + }; +} + +/** + * The base implementation of `_.hasIn` without support for deep paths. + * + * @private + * @param {Object} [object] The object to query. + * @param {Array|string} key The key to check. + * @returns {boolean} Returns `true` if `key` exists, else `false`. + */ +function baseHasIn(object, key) { + return object != null && key in Object(object); +} + +/** + * Checks if `path` exists on `object`. + * + * @private + * @param {Object} object The object to query. + * @param {Array|string} path The path to check. + * @param {Function} hasFunc The function to check properties. + * @returns {boolean} Returns `true` if `path` exists, else `false`. + */ +function hasPath(object, path, hasFunc) { + path = castPath(path, object); + + var index = -1, + length = path.length, + result = false; + + while (++index < length) { + var key = toKey(path[index]); + if (!(result = object != null && hasFunc(object, key))) { + break; + } + object = object[key]; + } + if (result || ++index != length) { + return result; + } + length = object == null ? 0 : object.length; + return !!length && isLength(length) && isIndex(key, length) && + (isArray(object) || isArguments(object)); +} + +/** + * Checks if `path` is a direct or inherited property of `object`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Object + * @param {Object} object The object to query. + * @param {Array|string} path The path to check. + * @returns {boolean} Returns `true` if `path` exists, else `false`. + * @example + * + * var object = _.create({ 'a': _.create({ 'b': 2 }) }); + * + * _.hasIn(object, 'a'); + * // => true + * + * _.hasIn(object, 'a.b'); + * // => true + * + * _.hasIn(object, ['a', 'b']); + * // => true + * + * _.hasIn(object, 'b'); + * // => false + */ +function hasIn(object, path) { + return object != null && hasPath(object, path, baseHasIn); +} + +/** Used to compose bitmasks for value comparisons. */ +var COMPARE_PARTIAL_FLAG$5 = 1, + COMPARE_UNORDERED_FLAG$3 = 2; + +/** + * The base implementation of `_.matchesProperty` which doesn't clone `srcValue`. + * + * @private + * @param {string} path The path of the property to get. + * @param {*} srcValue The value to match. + * @returns {Function} Returns the new spec function. + */ +function baseMatchesProperty(path, srcValue) { + if (isKey(path) && isStrictComparable(srcValue)) { + return matchesStrictComparable(toKey(path), srcValue); + } + return function(object) { + var objValue = get(object, path); + return (objValue === undefined && objValue === srcValue) + ? hasIn(object, path) + : baseIsEqual(srcValue, objValue, COMPARE_PARTIAL_FLAG$5 | COMPARE_UNORDERED_FLAG$3); + }; +} + +/** + * The base implementation of `_.property` without support for deep paths. + * + * @private + * @param {string} key The key of the property to get. + * @returns {Function} Returns the new accessor function. + */ +function baseProperty(key) { + return function(object) { + return object == null ? undefined : object[key]; + }; +} + +/** + * A specialized version of `baseProperty` which supports deep paths. + * + * @private + * @param {Array|string} path The path of the property to get. + * @returns {Function} Returns the new accessor function. + */ +function basePropertyDeep(path) { + return function(object) { + return baseGet(object, path); + }; +} + +/** + * Creates a function that returns the value at `path` of a given object. + * + * @static + * @memberOf _ + * @since 2.4.0 + * @category Util + * @param {Array|string} path The path of the property to get. + * @returns {Function} Returns the new accessor function. + * @example + * + * var objects = [ + * { 'a': { 'b': 2 } }, + * { 'a': { 'b': 1 } } + * ]; + * + * _.map(objects, _.property('a.b')); + * // => [2, 1] + * + * _.map(_.sortBy(objects, _.property(['a', 'b'])), 'a.b'); + * // => [1, 2] + */ +function property(path) { + return isKey(path) ? baseProperty(toKey(path)) : basePropertyDeep(path); +} + +/** + * The base implementation of `_.iteratee`. + * + * @private + * @param {*} [value=_.identity] The value to convert to an iteratee. + * @returns {Function} Returns the iteratee. + */ +function baseIteratee(value) { + // Don't store the `typeof` result in a variable to avoid a JIT bug in Safari 9. + // See https://bugs.webkit.org/show_bug.cgi?id=156034 for more details. + if (typeof value == 'function') { + return value; + } + if (value == null) { + return identity; + } + if (typeof value == 'object') { + return isArray(value) + ? baseMatchesProperty(value[0], value[1]) + : baseMatches(value); + } + return property(value); +} + +/** + * Creates a base function for methods like `_.forIn` and `_.forOwn`. + * + * @private + * @param {boolean} [fromRight] Specify iterating from right to left. + * @returns {Function} Returns the new base function. + */ +function createBaseFor(fromRight) { + return function(object, iteratee, keysFunc) { + var index = -1, + iterable = Object(object), + props = keysFunc(object), + length = props.length; + + while (length--) { + var key = props[fromRight ? length : ++index]; + if (iteratee(iterable[key], key, iterable) === false) { + break; + } + } + return object; + }; +} + +/** + * The base implementation of `baseForOwn` which iterates over `object` + * properties returned by `keysFunc` and invokes `iteratee` for each property. + * Iteratee functions may exit iteration early by explicitly returning `false`. + * + * @private + * @param {Object} object The object to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @param {Function} keysFunc The function to get the keys of `object`. + * @returns {Object} Returns `object`. + */ +var baseFor = createBaseFor(); + +/** + * The base implementation of `_.forOwn` without support for iteratee shorthands. + * + * @private + * @param {Object} object The object to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Object} Returns `object`. + */ +function baseForOwn(object, iteratee) { + return object && baseFor(object, iteratee, keys); +} + +/** + * This function is like `assignValue` except that it doesn't assign + * `undefined` values. + * + * @private + * @param {Object} object The object to modify. + * @param {string} key The key of the property to assign. + * @param {*} value The value to assign. + */ +function assignMergeValue(object, key, value) { + if ((value !== undefined && !eq(object[key], value)) || + (value === undefined && !(key in object))) { + baseAssignValue(object, key, value); + } +} + +/** + * This method is like `_.isArrayLike` except that it also checks if `value` + * is an object. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an array-like object, + * else `false`. + * @example + * + * _.isArrayLikeObject([1, 2, 3]); + * // => true + * + * _.isArrayLikeObject(document.body.children); + * // => true + * + * _.isArrayLikeObject('abc'); + * // => false + * + * _.isArrayLikeObject(_.noop); + * // => false + */ +function isArrayLikeObject(value) { + return isObjectLike(value) && isArrayLike(value); +} + +/** + * Gets the value at `key`, unless `key` is "__proto__" or "constructor". + * + * @private + * @param {Object} object The object to query. + * @param {string} key The key of the property to get. + * @returns {*} Returns the property value. + */ +function safeGet(object, key) { + if (key === 'constructor' && typeof object[key] === 'function') { + return; + } + + if (key == '__proto__') { + return; + } + + return object[key]; +} + +/** + * Converts `value` to a plain object flattening inherited enumerable string + * keyed properties of `value` to own properties of the plain object. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Lang + * @param {*} value The value to convert. + * @returns {Object} Returns the converted plain object. + * @example + * + * function Foo() { + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.assign({ 'a': 1 }, new Foo); + * // => { 'a': 1, 'b': 2 } + * + * _.assign({ 'a': 1 }, _.toPlainObject(new Foo)); + * // => { 'a': 1, 'b': 2, 'c': 3 } + */ +function toPlainObject(value) { + return copyObject(value, keysIn(value)); +} + +/** + * A specialized version of `baseMerge` for arrays and objects which performs + * deep merges and tracks traversed objects enabling objects with circular + * references to be merged. + * + * @private + * @param {Object} object The destination object. + * @param {Object} source The source object. + * @param {string} key The key of the value to merge. + * @param {number} srcIndex The index of `source`. + * @param {Function} mergeFunc The function to merge values. + * @param {Function} [customizer] The function to customize assigned values. + * @param {Object} [stack] Tracks traversed source values and their merged + * counterparts. + */ +function baseMergeDeep(object, source, key, srcIndex, mergeFunc, customizer, stack) { + var objValue = safeGet(object, key), + srcValue = safeGet(source, key), + stacked = stack.get(srcValue); + + if (stacked) { + assignMergeValue(object, key, stacked); + return; + } + var newValue = customizer + ? customizer(objValue, srcValue, (key + ''), object, source, stack) + : undefined; + + var isCommon = newValue === undefined; + + if (isCommon) { + var isArr = isArray(srcValue), + isBuff = !isArr && isBuffer(srcValue), + isTyped = !isArr && !isBuff && isTypedArray(srcValue); + + newValue = srcValue; + if (isArr || isBuff || isTyped) { + if (isArray(objValue)) { + newValue = objValue; + } + else if (isArrayLikeObject(objValue)) { + newValue = copyArray(objValue); + } + else if (isBuff) { + isCommon = false; + newValue = cloneBuffer(srcValue, true); + } + else if (isTyped) { + isCommon = false; + newValue = cloneTypedArray(srcValue, true); + } + else { + newValue = []; + } + } + else if (isPlainObject(srcValue) || isArguments(srcValue)) { + newValue = objValue; + if (isArguments(objValue)) { + newValue = toPlainObject(objValue); + } + else if (!isObject(objValue) || isFunction(objValue)) { + newValue = initCloneObject(srcValue); + } + } + else { + isCommon = false; + } + } + if (isCommon) { + // Recursively merge objects and arrays (susceptible to call stack limits). + stack.set(srcValue, newValue); + mergeFunc(newValue, srcValue, srcIndex, customizer, stack); + stack['delete'](srcValue); + } + assignMergeValue(object, key, newValue); +} + +/** + * The base implementation of `_.merge` without support for multiple sources. + * + * @private + * @param {Object} object The destination object. + * @param {Object} source The source object. + * @param {number} srcIndex The index of `source`. + * @param {Function} [customizer] The function to customize merged values. + * @param {Object} [stack] Tracks traversed source values and their merged + * counterparts. + */ +function baseMerge(object, source, srcIndex, customizer, stack) { + if (object === source) { + return; + } + baseFor(source, function(srcValue, key) { + stack || (stack = new Stack); + if (isObject(srcValue)) { + baseMergeDeep(object, source, key, srcIndex, baseMerge, customizer, stack); + } + else { + var newValue = customizer + ? customizer(safeGet(object, key), srcValue, (key + ''), object, source, stack) + : undefined; + + if (newValue === undefined) { + newValue = srcValue; + } + assignMergeValue(object, key, newValue); + } + }, keysIn); +} + +/** + * The base implementation of methods like `_.findKey` and `_.findLastKey`, + * without support for iteratee shorthands, which iterates over `collection` + * using `eachFunc`. + * + * @private + * @param {Array|Object} collection The collection to inspect. + * @param {Function} predicate The function invoked per iteration. + * @param {Function} eachFunc The function to iterate over `collection`. + * @returns {*} Returns the found element or its key, else `undefined`. + */ +function baseFindKey(collection, predicate, eachFunc) { + var result; + eachFunc(collection, function(value, key, collection) { + if (predicate(value, key, collection)) { + result = key; + return false; + } + }); + return result; +} + +/** + * This method is like `_.find` except that it returns the key of the first + * element `predicate` returns truthy for instead of the element itself. + * + * @static + * @memberOf _ + * @since 1.1.0 + * @category Object + * @param {Object} object The object to inspect. + * @param {Function} [predicate=_.identity] The function invoked per iteration. + * @returns {string|undefined} Returns the key of the matched element, + * else `undefined`. + * @example + * + * var users = { + * 'barney': { 'age': 36, 'active': true }, + * 'fred': { 'age': 40, 'active': false }, + * 'pebbles': { 'age': 1, 'active': true } + * }; + * + * _.findKey(users, function(o) { return o.age < 40; }); + * // => 'barney' (iteration order is not guaranteed) + * + * // The `_.matches` iteratee shorthand. + * _.findKey(users, { 'age': 1, 'active': true }); + * // => 'pebbles' + * + * // The `_.matchesProperty` iteratee shorthand. + * _.findKey(users, ['active', false]); + * // => 'fred' + * + * // The `_.property` iteratee shorthand. + * _.findKey(users, 'active'); + * // => 'barney' + */ +function findKey(object, predicate) { + return baseFindKey(object, baseIteratee(predicate), baseForOwn); +} + +/** `Object#toString` result references. */ +var stringTag$2 = '[object String]'; + +/** + * Checks if `value` is classified as a `String` primitive or object. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a string, else `false`. + * @example + * + * _.isString('abc'); + * // => true + * + * _.isString(1); + * // => false + */ +function isString(value) { + return typeof value == 'string' || + (!isArray(value) && isObjectLike(value) && baseGetTag(value) == stringTag$2); +} + +/** `Object#toString` result references. */ +var boolTag$2 = '[object Boolean]'; + +/** + * Checks if `value` is classified as a boolean primitive or object. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a boolean, else `false`. + * @example + * + * _.isBoolean(false); + * // => true + * + * _.isBoolean(null); + * // => false + */ +function isBoolean(value) { + return value === true || value === false || + (isObjectLike(value) && baseGetTag(value) == boolTag$2); +} + +/** + * Performs a deep comparison between two values to determine if they are + * equivalent. + * + * **Note:** This method supports comparing arrays, array buffers, booleans, + * date objects, error objects, maps, numbers, `Object` objects, regexes, + * sets, strings, symbols, and typed arrays. `Object` objects are compared + * by their own, not inherited, enumerable properties. Functions and DOM + * nodes are compared by strict equality, i.e. `===`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @returns {boolean} Returns `true` if the values are equivalent, else `false`. + * @example + * + * var object = { 'a': 1 }; + * var other = { 'a': 1 }; + * + * _.isEqual(object, other); + * // => true + * + * object === other; + * // => false + */ +function isEqual(value, other) { + return baseIsEqual(value, other); +} + +/** `Object#toString` result references. */ +var numberTag$2 = '[object Number]'; + +/** + * Checks if `value` is classified as a `Number` primitive or object. + * + * **Note:** To exclude `Infinity`, `-Infinity`, and `NaN`, which are + * classified as numbers, use the `_.isFinite` method. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a number, else `false`. + * @example + * + * _.isNumber(3); + * // => true + * + * _.isNumber(Number.MIN_VALUE); + * // => true + * + * _.isNumber(Infinity); + * // => true + * + * _.isNumber('3'); + * // => false + */ +function isNumber(value) { + return typeof value == 'number' || + (isObjectLike(value) && baseGetTag(value) == numberTag$2); +} + +/** + * Checks if `value` is `NaN`. + * + * **Note:** This method is based on + * [`Number.isNaN`](https://mdn.io/Number/isNaN) and is not the same as + * global [`isNaN`](https://mdn.io/isNaN) which returns `true` for + * `undefined` and other non-number values. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is `NaN`, else `false`. + * @example + * + * _.isNaN(NaN); + * // => true + * + * _.isNaN(new Number(NaN)); + * // => true + * + * isNaN(undefined); + * // => true + * + * _.isNaN(undefined); + * // => false + */ +function isNaN(value) { + // An `NaN` primitive is the only value that is not equal to itself. + // Perform the `toStringTag` check first to avoid errors with some + // ActiveX objects in IE. + return isNumber(value) && value != +value; +} + +/** + * Checks if `value` is `null`. + * + * @static + * @memberOf _ + * @since 0.1.0 + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is `null`, else `false`. + * @example + * + * _.isNull(null); + * // => true + * + * _.isNull(void 0); + * // => false + */ +function isNull(value) { + return value === null; +} + +/** + * Checks if `value` is `undefined`. + * + * @static + * @since 0.1.0 + * @memberOf _ + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is `undefined`, else `false`. + * @example + * + * _.isUndefined(void 0); + * // => true + * + * _.isUndefined(null); + * // => false + */ +function isUndefined(value) { + return value === undefined; +} + +/** + * The opposite of `_.mapValues`; this method creates an object with the + * same values as `object` and keys generated by running each own enumerable + * string keyed property of `object` thru `iteratee`. The iteratee is invoked + * with three arguments: (value, key, object). + * + * @static + * @memberOf _ + * @since 3.8.0 + * @category Object + * @param {Object} object The object to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @returns {Object} Returns the new mapped object. + * @see _.mapValues + * @example + * + * _.mapKeys({ 'a': 1, 'b': 2 }, function(value, key) { + * return key + value; + * }); + * // => { 'a1': 1, 'b2': 2 } + */ +function mapKeys(object, iteratee) { + var result = {}; + iteratee = baseIteratee(iteratee); + + baseForOwn(object, function(value, key, object) { + baseAssignValue(result, iteratee(value, key, object), value); + }); + return result; +} + +/** + * Creates an object with the same keys as `object` and values generated + * by running each own enumerable string keyed property of `object` thru + * `iteratee`. The iteratee is invoked with three arguments: + * (value, key, object). + * + * @static + * @memberOf _ + * @since 2.4.0 + * @category Object + * @param {Object} object The object to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @returns {Object} Returns the new mapped object. + * @see _.mapKeys + * @example + * + * var users = { + * 'fred': { 'user': 'fred', 'age': 40 }, + * 'pebbles': { 'user': 'pebbles', 'age': 1 } + * }; + * + * _.mapValues(users, function(o) { return o.age; }); + * // => { 'fred': 40, 'pebbles': 1 } (iteration order is not guaranteed) + * + * // The `_.property` iteratee shorthand. + * _.mapValues(users, 'age'); + * // => { 'fred': 40, 'pebbles': 1 } (iteration order is not guaranteed) + */ +function mapValues(object, iteratee) { + var result = {}; + iteratee = baseIteratee(iteratee); + + baseForOwn(object, function(value, key, object) { + baseAssignValue(result, key, iteratee(value, key, object)); + }); + return result; +} + +/** + * This method is like `_.assign` except that it recursively merges own and + * inherited enumerable string keyed properties of source objects into the + * destination object. Source properties that resolve to `undefined` are + * skipped if a destination value exists. Array and plain object properties + * are merged recursively. Other objects and value types are overridden by + * assignment. Source objects are applied from left to right. Subsequent + * sources overwrite property assignments of previous sources. + * + * **Note:** This method mutates `object`. + * + * @static + * @memberOf _ + * @since 0.5.0 + * @category Object + * @param {Object} object The destination object. + * @param {...Object} [sources] The source objects. + * @returns {Object} Returns `object`. + * @example + * + * var object = { + * 'a': [{ 'b': 2 }, { 'd': 4 }] + * }; + * + * var other = { + * 'a': [{ 'c': 3 }, { 'e': 5 }] + * }; + * + * _.merge(object, other); + * // => { 'a': [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] } + */ +var merge = createAssigner(function(object, source, srcIndex) { + baseMerge(object, source, srcIndex); +}); + +/** Error message constants. */ +var FUNC_ERROR_TEXT$1 = 'Expected a function'; + +/** + * Creates a function that negates the result of the predicate `func`. The + * `func` predicate is invoked with the `this` binding and arguments of the + * created function. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category Function + * @param {Function} predicate The predicate to negate. + * @returns {Function} Returns the new negated function. + * @example + * + * function isEven(n) { + * return n % 2 == 0; + * } + * + * _.filter([1, 2, 3, 4, 5, 6], _.negate(isEven)); + * // => [1, 3, 5] + */ +function negate(predicate) { + if (typeof predicate != 'function') { + throw new TypeError(FUNC_ERROR_TEXT$1); + } + return function() { + var args = arguments; + switch (args.length) { + case 0: return !predicate.call(this); + case 1: return !predicate.call(this, args[0]); + case 2: return !predicate.call(this, args[0], args[1]); + case 3: return !predicate.call(this, args[0], args[1], args[2]); + } + return !predicate.apply(this, args); + }; +} + +/** + * The base implementation of `_.set`. + * + * @private + * @param {Object} object The object to modify. + * @param {Array|string} path The path of the property to set. + * @param {*} value The value to set. + * @param {Function} [customizer] The function to customize path creation. + * @returns {Object} Returns `object`. + */ +function baseSet(object, path, value, customizer) { + if (!isObject(object)) { + return object; + } + path = castPath(path, object); + + var index = -1, + length = path.length, + lastIndex = length - 1, + nested = object; + + while (nested != null && ++index < length) { + var key = toKey(path[index]), + newValue = value; + + if (index != lastIndex) { + var objValue = nested[key]; + newValue = customizer ? customizer(objValue, key, nested) : undefined; + if (newValue === undefined) { + newValue = isObject(objValue) + ? objValue + : (isIndex(path[index + 1]) ? [] : {}); + } + } + assignValue(nested, key, newValue); + nested = nested[key]; + } + return object; +} + +/** + * The base implementation of `_.pickBy` without support for iteratee shorthands. + * + * @private + * @param {Object} object The source object. + * @param {string[]} paths The property paths to pick. + * @param {Function} predicate The function invoked per property. + * @returns {Object} Returns the new object. + */ +function basePickBy(object, paths, predicate) { + var index = -1, + length = paths.length, + result = {}; + + while (++index < length) { + var path = paths[index], + value = baseGet(object, path); + + if (predicate(value, path)) { + baseSet(result, castPath(path, object), value); + } + } + return result; +} + +/** + * Creates an object composed of the `object` properties `predicate` returns + * truthy for. The predicate is invoked with two arguments: (value, key). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Object + * @param {Object} object The source object. + * @param {Function} [predicate=_.identity] The function invoked per property. + * @returns {Object} Returns the new object. + * @example + * + * var object = { 'a': 1, 'b': '2', 'c': 3 }; + * + * _.pickBy(object, _.isNumber); + * // => { 'a': 1, 'c': 3 } + */ +function pickBy(object, predicate) { + if (object == null) { + return {}; + } + var props = arrayMap(getAllKeysIn(object), function(prop) { + return [prop]; + }); + predicate = baseIteratee(predicate); + return basePickBy(object, props, function(value, path) { + return predicate(value, path[0]); + }); +} + +/** + * The opposite of `_.pickBy`; this method creates an object composed of + * the own and inherited enumerable string keyed properties of `object` that + * `predicate` doesn't return truthy for. The predicate is invoked with two + * arguments: (value, key). + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Object + * @param {Object} object The source object. + * @param {Function} [predicate=_.identity] The function invoked per property. + * @returns {Object} Returns the new object. + * @example + * + * var object = { 'a': 1, 'b': '2', 'c': 3 }; + * + * _.omitBy(object, _.isNumber); + * // => { 'b': '2' } + */ +function omitBy(object, predicate) { + return pickBy(object, negate(baseIteratee(predicate))); +} + +/** + * Used by `_.trim` and `_.trimEnd` to get the index of the last string symbol + * that is not found in the character symbols. + * + * @private + * @param {Array} strSymbols The string symbols to inspect. + * @param {Array} chrSymbols The character symbols to find. + * @returns {number} Returns the index of the last unmatched string symbol. + */ +function charsEndIndex(strSymbols, chrSymbols) { + var index = strSymbols.length; + + while (index-- && baseIndexOf(chrSymbols, strSymbols[index], 0) > -1) {} + return index; +} + +/** + * Used by `_.trim` and `_.trimStart` to get the index of the first string symbol + * that is not found in the character symbols. + * + * @private + * @param {Array} strSymbols The string symbols to inspect. + * @param {Array} chrSymbols The character symbols to find. + * @returns {number} Returns the index of the first unmatched string symbol. + */ +function charsStartIndex(strSymbols, chrSymbols) { + var index = -1, + length = strSymbols.length; + + while (++index < length && baseIndexOf(chrSymbols, strSymbols[index], 0) > -1) {} + return index; +} + +/** Used to match leading and trailing whitespace. */ +var reTrim = /^\s+|\s+$/g; + +/** + * Removes leading and trailing whitespace or specified characters from `string`. + * + * @static + * @memberOf _ + * @since 3.0.0 + * @category String + * @param {string} [string=''] The string to trim. + * @param {string} [chars=whitespace] The characters to trim. + * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. + * @returns {string} Returns the trimmed string. + * @example + * + * _.trim(' abc '); + * // => 'abc' + * + * _.trim('-_-abc-_-', '_-'); + * // => 'abc' + * + * _.map([' foo ', ' bar '], _.trim); + * // => ['foo', 'bar'] + */ +function trim(string, chars, guard) { + string = toString(string); + if (string && (guard || chars === undefined)) { + return string.replace(reTrim, ''); + } + if (!string || !(chars = baseToString(chars))) { + return string; + } + var strSymbols = stringToArray(string), + chrSymbols = stringToArray(chars), + start = charsStartIndex(strSymbols, chrSymbols), + end = charsEndIndex(strSymbols, chrSymbols) + 1; + + return castSlice(strSymbols, start, end).join(''); +} + +/** + * Check several parameter that there is something in the param + * @param {*} param input + * @return {boolean} + */ + +function notEmpty (a) { + if (isArray(a)) { + return true; + } + return a !== undefined && a !== null && trim(a) !== ''; +} + +// validator numbers +/** + * @2015-05-04 found a problem if the value is a number like string + * it will pass, so add a check if it's string before we pass to next + * @param {number} value expected value + * @return {boolean} true if OK + */ +var checkIsNumber = function(value) { + return isString(value) ? false : !isNaN( parseFloat(value) ) +}; + +// validate string type +/** + * @param {string} value expected value + * @return {boolean} true if OK + */ +var checkIsString = function(value) { + return (trim(value) !== '') ? isString(value) : false; +}; + +// check for boolean +/** + * @param {boolean} value expected + * @return {boolean} true if OK + */ +var checkIsBoolean = function(value) { + return isBoolean(value); +}; + +// validate any thing only check if there is something +/** + * @param {*} value the value + * @param {boolean} [checkNull=true] strict check if there is null value + * @return {boolean} true is OK + */ +var checkIsAny = function(value, checkNull) { + if ( checkNull === void 0 ) checkNull = true; + + if (!isUndefined(value) && value !== '' && trim(value) !== '') { + if (checkNull === false || (checkNull === true && !isNull(value))) { + return true; + } + } + return false; +}; + +// Good practice rule - No magic number + +var ARGS_NOT_ARRAY_ERR = "args is not an array! You might want to do: ES6 Array.from(arguments) or ES5 Array.prototype.slice.call(arguments)"; +var PARAMS_NOT_ARRAY_ERR = "params is not an array! Did something gone wrong when you generate the contract.json?"; +var EXCEPTION_CASE_ERR = 'Could not understand your arguments and parameter structure!'; +// @TODO the jsdoc return array. and we should also allow array syntax +var DEFAULT_TYPE$1 = DEFAULT_TYPE; +var ARRAY_TYPE_LFT$1 = ARRAY_TYPE_LFT; +var ARRAY_TYPE_RGT$1 = ARRAY_TYPE_RGT; + +var TYPE_KEY$1 = TYPE_KEY; +var OPTIONAL_KEY$1 = OPTIONAL_KEY; +var ENUM_KEY$1 = ENUM_KEY; +var ARGS_KEY$1 = ARGS_KEY; +var CHECKER_KEY$1 = CHECKER_KEY; +var ALIAS_KEY$1 = ALIAS_KEY; + +var ARRAY_TYPE$1 = ARRAY_TYPE; +var OBJECT_TYPE$1 = OBJECT_TYPE; +var STRING_TYPE$1 = STRING_TYPE; +var BOOLEAN_TYPE$1 = BOOLEAN_TYPE; +var NUMBER_TYPE$1 = NUMBER_TYPE; +var KEY_WORD$1 = KEY_WORD; +var OR_SEPERATOR$1 = OR_SEPERATOR; + +// not actually in use +// export const NUMBER_TYPES = JSONQL_CONSTANTS.NUMBER_TYPES; + +// primitive types + +/** + * this is a wrapper method to call different one based on their type + * @param {string} type to check + * @return {function} a function to handle the type + */ +var combineFn = function(type) { + switch (type) { + case NUMBER_TYPE$1: + return checkIsNumber; + case STRING_TYPE$1: + return checkIsString; + case BOOLEAN_TYPE$1: + return checkIsBoolean; + default: + return checkIsAny; + } +}; + +// validate array type + +/** + * @param {array} value expected + * @param {string} [type=''] pass the type if we encounter array. then we need to check the value as well + * @return {boolean} true if OK + */ +var checkIsArray = function(value, type) { + if ( type === void 0 ) type=''; + + if (isArray(value)) { + if (type === '' || trim(type)==='') { + return true; + } + // we test it in reverse + // @TODO if the type is an array (OR) then what? + // we need to take into account this could be an array + var c = value.filter(function (v) { return !combineFn(type)(v); }); + return !(c.length > 0) + } + return false; +}; + +/** + * check if it matches the array. pattern + * @param {string} type + * @return {boolean|array} false means NO, always return array + */ +var isArrayLike$1 = function(type) { + // @TODO could that have something like array<> instead of array.<>? missing the dot? + // because type script is Array without the dot + if (type.indexOf(ARRAY_TYPE_LFT$1) > -1 && type.indexOf(ARRAY_TYPE_RGT$1) > -1) { + var _type = type.replace(ARRAY_TYPE_LFT$1, '').replace(ARRAY_TYPE_RGT$1, ''); + if (_type.indexOf(OR_SEPERATOR$1)) { + return _type.split(OR_SEPERATOR$1) + } + return [_type] + } + return false; +}; + +/** + * we might encounter something like array. then we need to take it apart + * @param {object} p the prepared object for processing + * @param {string|array} type the type came from + * @return {boolean} for the filter to operate on + */ +var arrayTypeHandler = function(p, type) { + var arg = p.arg; + // need a special case to handle the OR type + // we need to test the args instead of the type(s) + if (type.length > 1) { + return !arg.filter(function (v) { return ( + !(type.length > type.filter(function (t) { return !combineFn(t)(v); }).length) + ); }).length; + } + // type is array so this will be or! + return type.length > type.filter(function (t) { return !checkIsArray(arg, t); }).length; +}; + +// validate object type +/** + * @TODO if provide with the keys then we need to check if the key:value type as well + * @param {object} value expected + * @param {array} [keys=null] if it has the keys array to compare as well + * @return {boolean} true if OK + */ +var checkIsObject = function(value, keys) { + if ( keys === void 0 ) keys=null; + + if (isPlainObject(value)) { + if (!keys) { + return true; + } + if (checkIsArray(keys)) { + // please note we DON'T care if some is optional + // plese refer to the contract.json for the keys + return !keys.filter(function (key) { + var _value = value[key.name]; + return !(key.type.length > key.type.filter(function (type) { + var tmp; + if (!isUndefined(_value)) { + if ((tmp = isArrayLike$1(type)) !== false) { + return !arrayTypeHandler({arg: _value}, tmp) + // return tmp.filter(t => !checkIsArray(_value, t)).length; + // @TODO there might be an object within an object with keys as well :S + } + return !combineFn(type)(_value) + } + return true; + }).length) + }).length; + } + } + return false; +}; + +/** + * fold this into it's own function to handler different object type + * @param {object} p the prepared object for process + * @return {boolean} + */ +var objectTypeHandler = function(p) { + var arg = p.arg; + var param = p.param; + var _args = [arg]; + if (Array.isArray(param.keys) && param.keys.length) { + _args.push(param.keys); + } + // just simple check + return checkIsObject.apply(null, _args) +}; + +// move the index.js code here that make more sense to find where things are + +// import debug from 'debug' +// const debugFn = debug('jsonql-params-validator:validator') +// also export this for use in other places + +/** + * We need to handle those optional parameter without a default value + * @param {object} params from contract.json + * @return {boolean} for filter operation false is actually OK + */ +var optionalHandler = function( params ) { + var arg = params.arg; + var param = params.param; + if (notEmpty(arg)) { + // debug('call optional handler', arg, params); + // loop through the type in param + return !(param.type.length > param.type.filter(function (type) { return validateHandler(type, params); } + ).length) + } + return false; +}; + +/** + * actually picking the validator + * @param {*} type for checking + * @param {*} value for checking + * @return {boolean} true on OK + */ +var validateHandler = function(type, value) { + var tmp; + switch (true) { + case type === OBJECT_TYPE$1: + // debugFn('call OBJECT_TYPE') + return !objectTypeHandler(value) + case type === ARRAY_TYPE$1: + // debugFn('call ARRAY_TYPE') + return !checkIsArray(value.arg) + // @TODO when the type is not present, it always fall through here + // so we need to find a way to actually pre-check the type first + // AKA check the contract.json map before running here + case (tmp = isArrayLike$1(type)) !== false: + // debugFn('call ARRAY_LIKE: %O', value) + return !arrayTypeHandler(value, tmp) + default: + return !combineFn(type)(value.arg) + } +}; + +/** + * it get too longer to fit in one line so break it out from the fn below + * @param {*} arg value + * @param {object} param config + * @return {*} value or apply default value + */ +var getOptionalValue = function(arg, param) { + if (!isUndefined(arg)) { + return arg; + } + return (param.optional === true && !isUndefined(param.defaultvalue) ? param.defaultvalue : null) +}; + +/** + * padding the arguments with defaultValue if the arguments did not provide the value + * this will be the name export + * @param {array} args normalized arguments + * @param {array} params from contract.json + * @return {array} merge the two together + */ +var normalizeArgs = function(args, params) { + // first we should check if this call require a validation at all + // there will be situation where the function doesn't need args and params + if (!checkIsArray(params)) { + // debugFn('params value', params) + throw new JsonqlError(PARAMS_NOT_ARRAY_ERR) + } + if (params.length === 0) { + return []; + } + if (!checkIsArray(args)) { + throw new JsonqlError(ARGS_NOT_ARRAY_ERR) + } + // debugFn(args, params); + // fall through switch + switch(true) { + case args.length == params.length: // standard + return args.map(function (arg, i) { return ( + { + arg: arg, + index: i, + param: params[i] + } + ); }); + case params[0].variable === true: // using spread syntax + var type = params[0].type; + return args.map(function (arg, i) { return ( + { + arg: arg, + index: i, // keep the index for reference + param: params[i] || { type: type, name: '_' } + } + ); }); + // with optional defaultValue parameters + case args.length < params.length: + return params.map(function (param, i) { return ( + { + param: param, + index: i, + arg: getOptionalValue(args[i], param), + optional: param.optional || false + } + ); }); + // this one pass more than it should have anything after the args.length will be cast as any type + case args.length > params.length && params.length === 1: + // this happens when we have those array. type + var tmp, _type = [ DEFAULT_TYPE$1 ]; + // we only looking at the first one, this might be a @BUG! + if ((tmp = isArrayLike$1(params[0].type[0])) !== false) { + _type = tmp; + } + // if not then we fall back to the following + return args.map(function (arg, i) { return ( + { + arg: arg, + index: i, + param: params[i] || { type: _type, name: '_' } + } + ); }); + // @TODO find out if there is more cases not cover + default: // this should never happen + // debugFn('args', args) + // debugFn('params', params) + // this is unknown therefore we just throw it! + throw new JsonqlError(EXCEPTION_CASE_ERR, { args: args, params: params }) + } +}; + +// what we want is after the validaton we also get the normalized result +// which is with the optional property if the argument didn't provide it +/** + * process the array of params back to their arguments + * @param {array} result the params result + * @return {array} arguments + */ +var processReturn = function (result) { return result.map(function (r) { return r.arg; }); }; + +/** + * validator main interface + * @param {array} args the arguments pass to the method call + * @param {array} params from the contract for that method + * @param {boolean} [withResul=false] if true then this will return the normalize result as well + * @return {array} empty array on success, or failed parameter and reasons + */ +var validateSync = function(args, params, withResult) { + var obj; + + if ( withResult === void 0 ) withResult = false; + var cleanArgs = normalizeArgs(args, params); + var checkResult = cleanArgs.filter(function (p) { + if (p.param.optional === true) { + return optionalHandler(p) + } + // because array of types means OR so if one pass means pass + return !(p.param.type.length > p.param.type.filter( + function (type) { return validateHandler(type, p); } + ).length) + }); + // using the same convention we been using all this time + return !withResult ? checkResult : ( obj = {}, obj[ERROR_KEY] = checkResult, obj[DATA_KEY] = processReturn(cleanArgs), obj ) +}; + +/** + * A wrapper method that return promise + * @param {array} args arguments + * @param {array} params from contract.json + * @param {boolean} [withResul=false] if true then this will return the normalize result as well + * @return {object} promise.then or catch + */ +var validateAsync = function(args, params, withResult) { + if ( withResult === void 0 ) withResult = false; + + return new Promise(function (resolver, rejecter) { + var result = validateSync(args, params, withResult); + if (withResult) { + return result[ERROR_KEY].length ? rejecter(result[ERROR_KEY]) + : resolver(result[DATA_KEY]) + } + // the different is just in the then or catch phrase + return result.length ? rejecter(result) : resolver([]) + }) +}; + +/** + * @param {array} arr Array for check + * @param {*} value target + * @return {boolean} true on successs + */ +var isInArray = function(arr, value) { + return !!arr.filter(function (a) { return a === value; }).length; +}; + +/** + * @param {object} obj for search + * @param {string} key target + * @return {boolean} true on success + */ +var checkKeyInObject = function(obj, key) { + var keys = Object.keys(obj); + return isInArray(keys, key) +}; + +// import debug from 'debug'; +// const debugFn = debug('jsonql-params-validator:options:prepare') + +// just not to make my head hurt +var isEmpty = function (value) { return !notEmpty(value); }; + +/** + * Map the alias to their key then grab their value over + * @param {object} config the user supplied config + * @param {object} appProps the default option map + * @return {object} the config keys replaced with the appProps key by the ALIAS + */ +function mapAliasConfigKeys(config, appProps) { + // need to do two steps + // 1. take key with alias key + var aliasMap = omitBy(appProps, function (value, k) { return !value[ALIAS_KEY$1]; } ); + if (isEqual(aliasMap, {})) { + return config; + } + return mapKeys(config, function (v, key) { return findKey(aliasMap, function (o) { return o.alias === key; }) || key; }) +} + +/** + * We only want to run the valdiation against the config (user supplied) value + * but keep the defaultOptions untouch + * @param {object} config configuraton supplied by user + * @param {object} appProps the default options map + * @return {object} the pristine values that will add back to the final output + */ +function preservePristineValues(config, appProps) { + // @BUG this will filter out those that is alias key + // we need to first map the alias keys back to their full key + var _config = mapAliasConfigKeys(config, appProps); + // take the default value out + var pristineValues = mapValues( + omitBy(appProps, function (value, key) { return checkKeyInObject(_config, key); }), + function (value) { return value.args; } + ); + // for testing the value + var checkAgainstAppProps = omitBy(appProps, function (value, key) { return !checkKeyInObject(_config, key); }); + // output + return { + pristineValues: pristineValues, + checkAgainstAppProps: checkAgainstAppProps, + config: _config // passing this correct values back + } +} + +/** + * This will take the value that is ONLY need to check + * @param {object} config that one + * @param {object} props map for creating checking + * @return {object} put that arg into the args + */ +function processConfigAction(config, props) { + // debugFn('processConfigAction', props) + // v.1.2.0 add checking if its mark optional and the value is empty then pass + return mapValues(props, function (value, key) { + var obj, obj$1; + + return ( + isUndefined(config[key]) || (value[OPTIONAL_KEY$1] === true && isEmpty(config[key])) + ? merge({}, value, ( obj = {}, obj[KEY_WORD$1] = true, obj )) + : ( obj$1 = {}, obj$1[ARGS_KEY$1] = config[key], obj$1[TYPE_KEY$1] = value[TYPE_KEY$1], obj$1[OPTIONAL_KEY$1] = value[OPTIONAL_KEY$1] || false, obj$1[ENUM_KEY$1] = value[ENUM_KEY$1] || false, obj$1[CHECKER_KEY$1] = value[CHECKER_KEY$1] || false, obj$1 ) + ); + } + ) +} + +/** + * Quick transform + * @TODO we should only validate those that is pass from the config + * and pass through those values that is from the defaultOptions + * @param {object} opts that one + * @param {object} appProps mutation configuration options + * @return {object} put that arg into the args + */ +function prepareArgsForValidation(opts, appProps) { + var ref = preservePristineValues(opts, appProps); + var config = ref.config; + var pristineValues = ref.pristineValues; + var checkAgainstAppProps = ref.checkAgainstAppProps; + // output + return [ + processConfigAction(config, checkAgainstAppProps), + pristineValues + ] +} + +// breaking the whole thing up to see what cause the multiple calls issue + +// import debug from 'debug'; +// const debugFn = debug('jsonql-params-validator:options:validation') + +/** + * just make sure it returns an array to use + * @param {*} arg input + * @return {array} output + */ +var toArray = function (arg) { return checkIsArray(arg) ? arg : [arg]; }; + +/** + * DIY in array + * @param {array} arr to check against + * @param {*} value to check + * @return {boolean} true on OK + */ +var inArray = function (arr, value) { return ( + !!arr.filter(function (v) { return v === value; }).length +); }; + +/** + * break out to make the code easier to read + * @param {object} value to process + * @param {function} cb the validateSync + * @return {array} empty on success + */ +function validateHandler$1(value, cb) { + var obj; + + // cb is the validateSync methods + var args = [ + [ value[ARGS_KEY$1] ], + [( obj = {}, obj[TYPE_KEY$1] = toArray(value[TYPE_KEY$1]), obj[OPTIONAL_KEY$1] = value[OPTIONAL_KEY$1], obj )] + ]; + // debugFn('validateHandler', args) + return Reflect.apply(cb, null, args) +} + +/** + * Check against the enum value if it's provided + * @param {*} value to check + * @param {*} enumv to check against if it's not false + * @return {boolean} true on OK + */ +var enumHandler = function (value, enumv) { + if (checkIsArray(enumv)) { + return inArray(enumv, value) + } + return true; +}; + +/** + * Allow passing a function to check the value + * There might be a problem here if the function is incorrect + * and that will makes it hard to debug what is going on inside + * @TODO there could be a few feature add to this one under different circumstance + * @param {*} value to check + * @param {function} checker for checking + */ +var checkerHandler = function (value, checker) { + try { + return isFunction(checker) ? checker.apply(null, [value]) : false; + } catch (e) { + return false; + } +}; + +/** + * Taken out from the runValidaton this only validate the required values + * @param {array} args from the config2argsAction + * @param {function} cb validateSync + * @return {array} of configuration values + */ +function runValidationAction(cb) { + return function (value, key) { + // debugFn('runValidationAction', key, value) + if (value[KEY_WORD$1]) { + return value[ARGS_KEY$1] + } + var check = validateHandler$1(value, cb); + if (check.length) { + // debugFn('runValidationAction', key, value) + throw new JsonqlTypeError(key, check) + } + if (value[ENUM_KEY$1] !== false && !enumHandler(value[ARGS_KEY$1], value[ENUM_KEY$1])) { + throw new JsonqlEnumError(key) + } + if (value[CHECKER_KEY$1] !== false && !checkerHandler(value[ARGS_KEY$1], value[CHECKER_KEY$1])) { + throw new JsonqlCheckerError(key) + } + return value[ARGS_KEY$1] + } +} + +/** + * @param {object} args from the config2argsAction + * @param {function} cb validateSync + * @return {object} of configuration values + */ +function runValidation(args, cb) { + var argsForValidate = args[0]; + var pristineValues = args[1]; + // turn the thing into an array and see what happen here + // debugFn('_args', argsForValidate) + var result = mapValues(argsForValidate, runValidationAction(cb)); + return merge(result, pristineValues) +} + +// this is port back from the client to share across all projects + +// import debug from 'debug' +// const debugFn = debug('jsonql-params-validator:check-options-async') + +/** + * Quick transform + * @param {object} config that one + * @param {object} appProps mutation configuration options + * @return {object} put that arg into the args + */ +var configToArgs = function (config, appProps) { + return Promise.resolve( + prepareArgsForValidation(config, appProps) + ) +}; + +/** + * @param {object} config user provide configuration option + * @param {object} appProps mutation configuration options + * @param {object} constProps the immutable configuration options + * @param {function} cb the validateSync method + * @return {object} Promise resolve merge config object + */ +function checkOptionsAsync(config, appProps, constProps, cb) { + if ( config === void 0 ) config = {}; + + return configToArgs(config, appProps) + .then(function (args1) { + // debugFn('args', args1) + return runValidation(args1, cb) + }) + // next if every thing good then pass to final merging + .then(function (args2) { return merge({}, args2, constProps); }) +} + +// create function to construct the config entry so we don't need to keep building object +// import debug from 'debug'; +// const debugFn = debug('jsonql-params-validator:construct-config'); +/** + * @param {*} args value + * @param {string} type for value + * @param {boolean} [optional=false] + * @param {boolean|array} [enumv=false] + * @param {boolean|function} [checker=false] + * @return {object} config entry + */ +function constructConfigFn(args, type, optional, enumv, checker, alias) { + if ( optional === void 0 ) optional=false; + if ( enumv === void 0 ) enumv=false; + if ( checker === void 0 ) checker=false; + if ( alias === void 0 ) alias=false; + + var base = {}; + base[ARGS_KEY] = args; + base[TYPE_KEY] = type; + if (optional === true) { + base[OPTIONAL_KEY] = true; + } + if (checkIsArray(enumv)) { + base[ENUM_KEY] = enumv; + } + if (isFunction(checker)) { + base[CHECKER_KEY] = checker; + } + if (isString(alias)) { + base[ALIAS_KEY] = alias; + } + return base; +} + +// export also create wrapper methods + +// import debug from 'debug'; +// const debugFn = debug('jsonql-params-validator:options:index'); + +/** + * This has a different interface + * @param {*} value to supply + * @param {string|array} type for checking + * @param {object} params to map against the config check + * @param {array} params.enumv NOT enum + * @param {boolean} params.optional false then nothing + * @param {function} params.checker need more work on this one later + * @param {string} params.alias mostly for cmd + */ +var createConfig = function (value, type, params) { + if ( params === void 0 ) params = {}; + + // Note the enumv not ENUM + // const { enumv, optional, checker, alias } = params; + // let args = [value, type, optional, enumv, checker, alias]; + var o = params[OPTIONAL_KEY]; + var e = params[ENUM_KEY]; + var c = params[CHECKER_KEY]; + var a = params[ALIAS_KEY]; + return constructConfigFn.apply(null, [value, type, o, e, c, a]) +}; + +/** + * We recreate the method here to avoid the circlar import + * @param {object} config user supply configuration + * @param {object} appProps mutation options + * @param {object} [constantProps={}] optional: immutation options + * @return {object} all checked configuration + */ +var checkConfigAsync = function(validateSync) { + return function(config, appProps, constantProps) { + if ( constantProps === void 0 ) constantProps= {}; + + return checkOptionsAsync(config, appProps, constantProps, validateSync) + } +}; + +// since this need to use everywhere might as well include in the validator + +function checkIsContract(contract) { + return checkIsObject(contract) + && ( + checkKeyInObject(contract, QUERY_NAME) + || checkKeyInObject(contract, MUTATION_NAME) + || checkKeyInObject(contract, SOCKET_NAME) + ) +} + +// craete several helper function to construct / extract the payload + +/** + * Get name from the payload (ported back from jsonql-koa) + * @param {*} payload to extract from + * @return {string} name + */ +function getNameFromPayload(payload) { + return Object.keys(payload)[0] +} + +/** + * @param {string} resolverName name of function + * @param {array} [args=[]] from the ...args + * @param {boolean} [jsonp = false] add v1.3.0 to koa + * @return {object} formatted argument + */ +function createQuery(resolverName, args, jsonp) { + var obj; + + if ( args === void 0 ) args = []; + if ( jsonp === void 0 ) jsonp = false; + if (checkIsString(resolverName) && checkIsArray(args)) { + var payload = {}; + payload[QUERY_ARG_NAME] = args; + if (jsonp === true) { + return payload; + } + return ( obj = {}, obj[resolverName] = payload, obj ) + } + throw new JsonqlValidationError("[createQuery] expect resolverName to be string and args to be array!", { resolverName: resolverName, args: args }) +} + +// string version of the above +function createQueryStr(resolverName, args, jsonp) { + if ( args === void 0 ) args = []; + if ( jsonp === void 0 ) jsonp = false; + + return JSON.stringify(createQuery(resolverName, args, jsonp)) +} + +// export +var isString$1 = checkIsString; +var isArray$1 = checkIsArray; +var validateSync$1 = validateSync; +var validateAsync$1 = validateAsync; + +var createConfig$1 = createConfig; + +var checkConfigAsync$1 = checkConfigAsync(validateSync); + +var isKeyInObject = checkKeyInObject; + +var isContract = checkIsContract; +var createQueryStr$1 = createQueryStr; +var getNameFromPayload$1 = getNameFromPayload; + +/** + * Try to normalize it to use between browser and node + * @param {string} name for the debug output + * @return {function} debug + */ +var getDebug = function (name) { + if (debug$2) { + return debug$2('jsonql-ws-client').extend(name) + } + return function () { + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + console.info.apply(null, [name].concat(args)); + } +}; +try { + if (window && window.localStorage) { + localStorage.setItem('DEBUG', 'jsonql-ws-client*'); + } +} catch(e) {} + +// since both the ws and io version are +var debugFn = getDebug('create-nsp-client'); +/** + * wrapper method to create a nsp without login + * @param {string|boolean} namespace namespace url could be false + * @param {object} opts configuration + * @return {object} ws client instance + */ +var nspClient = function (namespace, opts) { + var wssPath = opts.wssPath; + var wsOptions = opts.wsOptions; + var hostname = opts.hostname; + var url = namespace ? [hostname, namespace].join('/') : wssPath; + return opts.nspClient(url, wsOptions) +}; + +/** + * wrapper method to create a nsp with token auth + * @param {string} namespace namespace url + * @param {object} opts configuration + * @return {object} ws client instance + */ +var nspAuthClient = function (namespace, opts) { + var wssPath = opts.wssPath; + var token = opts.token; + var wsOptions = opts.wsOptions; + var hostname = opts.hostname; + var url = namespace ? [hostname, namespace].join('/') : wssPath; + return opts.nspAuthClient(url, token, wsOptions) +}; + +// constants + +var SOCKET_IO = JS_WS_SOCKET_IO_NAME; +var WS = JS_WS_NAME; + +var AVAILABLE_SERVERS = [SOCKET_IO, WS]; + +var SOCKET_NOT_DEFINE_ERR = 'socket is not define in the contract file!'; + +var MISSING_PROP_ERR = 'Missing property in contract!'; + +var EMIT_EVT = EMIT_REPLY_TYPE; + +var UNKNOWN_RESULT = 'UKNNOWN RESULT!'; + +var MY_NAMESPACE = 'myNamespace'; + +/** + * Got to make sure the connection order otherwise + * it will hang + * @param {object} nspSet contract + * @param {string} publicNamespace like the name said + * @return {array} namespaces in order + */ +function getNamespaceInOrder(nspSet, publicNamespace) { + var names = []; // need to make sure the order! + for (var namespace in nspSet) { + if (namespace === publicNamespace) { + names[1] = namespace; + } else { + names[0] = namespace; + } + } + return names; +} + +var obj, obj$1; +var debug = getDebug('check-options'); + +var fixWss = function (url, serverType) { + // ws only allow ws:// path + if (serverType===WS) { + return url.replace('http://', 'ws://') + } + return url; +}; + +var getHostName = function () { return ( + [window.location.protocol, window.location.host].join('//') +); }; + +var constProps = { + // this will be the switcher! + nspClient: null, + nspAuthClient: null, + // contructed path + wssPath: '' +}; + +var defaultOptions = { + loginHandlerName: createConfig$1(ISSUER_NAME, [STRING_TYPE]), + logoutHandlerName: createConfig$1(LOGOUT_NAME, [STRING_TYPE]), + // we will use this for determine the socket.io client type as well + useJwt: createConfig$1(false, [BOOLEAN_TYPE, STRING_TYPE]), + hostname: createConfig$1(false, [STRING_TYPE]), + namespace: createConfig$1(JSONQL_PATH, [STRING_TYPE]), + wsOptions: createConfig$1({transports: ['websocket'], 'force new connection' : true}, [OBJECT_TYPE]), + serverType: createConfig$1(SOCKET_IO, [STRING_TYPE], ( obj = {}, obj[ENUM_KEY] = AVAILABLE_SERVERS, obj )), + // we require the contract already generated and pass here + contract: createConfig$1({}, [OBJECT_TYPE], ( obj$1 = {}, obj$1[CHECKER_KEY] = isContract, obj$1 )), + enableAuth: createConfig$1(false, [BOOLEAN_TYPE]), + token: createConfig$1(false, [STRING_TYPE]) +}; +// export +function checkOptions(config) { + return checkConfigAsync$1(config, defaultOptions, constProps) + .then(function (opts) { + if (!opts.hostname) { + opts.hostname = getHostName(); + } + // @TODO the contract now will supply the namespace information + // and we need to use that to group the namespace call + opts.wssPath = fixWss([opts.hostname, opts.namespace].join('/'), opts.serverType); + + debug('opts', opts); + return opts; + }) +} + +var NB_EVENT_SERVICE_PRIVATE_STORE = new WeakMap(); +var NB_EVENT_SERVICE_PRIVATE_LAZY = new WeakMap(); + +/** + * generate a 32bit hash based on the function.toString() + * _from http://stackoverflow.com/questions/7616461/generate-a-hash-_from-string-in-javascript-jquery + * @param {string} s the converted to string function + * @return {string} the hashed function string + */ +function hashCode(s) { + return s.split("").reduce(function(a,b){a=((a<<5)-a)+b.charCodeAt(0);return a&a},0) +} + +// making all the functionality on it's own +// import { WatchClass } from './watch' + +var SuspendClass = function SuspendClass() { + // suspend, release and queue + this.__suspend__ = null; + this.queueStore = new Set(); + /* + this.watch('suspend', function(value, prop, oldValue) { + this.logger(`${prop} set from ${oldValue} to ${value}`) + // it means it set the suspend = true then release it + if (oldValue === true && value === false) { + // we want this happen after the return happens + setTimeout(() => { + this.release() + }, 1) + } + return value; // we need to return the value to store it + }) + */ +}; + +var prototypeAccessors = { $suspend: { configurable: true },$queues: { configurable: true } }; + +/** + * setter to set the suspend and check if it's boolean value + * @param {boolean} value to trigger + */ +prototypeAccessors.$suspend.set = function (value) { + var this$1 = this; + + if (typeof value === 'boolean') { + var lastValue = this.__suspend__; + this.__suspend__ = value; + this.logger('($suspend)', ("Change from " + lastValue + " --> " + value)); + if (lastValue === true && value === false) { + setTimeout(function () { + this$1.release(); + }, 1); + } + } else { + throw new Error("$suspend only accept Boolean value!") + } +}; + +/** + * queuing call up when it's in suspend mode + * @param {any} value + * @return {Boolean} true when added or false when it's not + */ +SuspendClass.prototype.$queue = function $queue () { + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + if (this.__suspend__ === true) { + this.logger('($queue)', 'added to $queue', args); + // there shouldn't be any duplicate ... + this.queueStore.add(args); + } + return this.__suspend__; +}; + +/** + * a getter to get all the store queue + * @return {array} Set turn into Array before return + */ +prototypeAccessors.$queues.get = function () { + var size = this.queueStore.size; + this.logger('($queues)', ("size: " + size)); + if (size > 0) { + return Array.from(this.queueStore) + } + return [] +}; + +/** + * Release the queue + * @return {int} size if any + */ +SuspendClass.prototype.release = function release () { + var this$1 = this; + + var size = this.queueStore.size; + this.logger('(release)', ("Release was called " + size)); + if (size > 0) { + var queue = Array.from(this.queueStore); + this.queueStore.clear(); + this.logger('queue', queue); + queue.forEach(function (args) { + this$1.logger(args); + Reflect.apply(this$1.$trigger, this$1, args); + }); + this.logger(("Release size " + (this.queueStore.size))); + } +}; + +Object.defineProperties( SuspendClass.prototype, prototypeAccessors ); + +// break up the main file because its getting way too long + +var NbEventServiceBase = /*@__PURE__*/(function (SuspendClass) { + function NbEventServiceBase(config) { + if ( config === void 0 ) config = {}; + + SuspendClass.call(this); + if (config.logger && typeof config.logger === 'function') { + this.logger = config.logger; + } + this.keep = config.keep; + // for the $done setter + this.result = config.keep ? [] : null; + // we need to init the store first otherwise it could be a lot of checking later + this.normalStore = new Map(); + this.lazyStore = new Map(); + } + + if ( SuspendClass ) NbEventServiceBase.__proto__ = SuspendClass; + NbEventServiceBase.prototype = Object.create( SuspendClass && SuspendClass.prototype ); + NbEventServiceBase.prototype.constructor = NbEventServiceBase; + + var prototypeAccessors = { normalStore: { configurable: true },lazyStore: { configurable: true } }; + + /** + * validate the event name(s) + * @param {string[]} evt event name + * @return {boolean} true when OK + */ + NbEventServiceBase.prototype.validateEvt = function validateEvt () { + var this$1 = this; + var evt = [], len = arguments.length; + while ( len-- ) evt[ len ] = arguments[ len ]; + + evt.forEach(function (e) { + if (typeof e !== 'string') { + this$1.logger('(validateEvt)', e); + throw new Error("event name must be string type!") + } + }); + return true; + }; + + /** + * Simple quick check on the two main parameters + * @param {string} evt event name + * @param {function} callback function to call + * @return {boolean} true when OK + */ + NbEventServiceBase.prototype.validate = function validate (evt, callback) { + if (this.validateEvt(evt)) { + if (typeof callback === 'function') { + return true; + } + } + throw new Error("callback required to be function type!") + }; + + /** + * Check if this type is correct or not added in V1.5.0 + * @param {string} type for checking + * @return {boolean} true on OK + */ + NbEventServiceBase.prototype.validateType = function validateType (type) { + var types = ['on', 'only', 'once', 'onlyOnce']; + return !!types.filter(function (t) { return type === t; }).length; + }; + + /** + * Run the callback + * @param {function} callback function to execute + * @param {array} payload for callback + * @param {object} ctx context or null + * @return {void} the result store in $done + */ + NbEventServiceBase.prototype.run = function run (callback, payload, ctx) { + this.logger('(run)', callback, payload, ctx); + this.$done = Reflect.apply(callback, ctx, this.toArray(payload)); + }; + + /** + * Take the content out and remove it from store id by the name + * @param {string} evt event name + * @param {string} [storeName = lazyStore] name of store + * @return {object|boolean} content or false on not found + */ + NbEventServiceBase.prototype.takeFromStore = function takeFromStore (evt, storeName) { + if ( storeName === void 0 ) storeName = 'lazyStore'; + + var store = this[storeName]; // it could be empty at this point + if (store) { + this.logger('(takeFromStore)', storeName, store); + if (store.has(evt)) { + var content = store.get(evt); + this.logger('(takeFromStore)', ("has " + evt), content); + store.delete(evt); + return content; + } + return false; + } + throw new Error((storeName + " is not supported!")) + }; + + /** + * The add to store step is similar so make it generic for resuse + * @param {object} store which store to use + * @param {string} evt event name + * @param {spread} args because the lazy store and normal store store different things + * @return {array} store and the size of the store + */ + NbEventServiceBase.prototype.addToStore = function addToStore (store, evt) { + var args = [], len = arguments.length - 2; + while ( len-- > 0 ) args[ len ] = arguments[ len + 2 ]; + + var fnSet; + if (store.has(evt)) { + this.logger('(addToStore)', (evt + " existed")); + fnSet = store.get(evt); + } else { + this.logger('(addToStore)', ("create new Set for " + evt)); + // this is new + fnSet = new Set(); + } + // lazy only store 2 items - this is not the case in V1.6.0 anymore + // we need to check the first parameter is string or not + if (args.length > 2) { + if (Array.isArray(args[0])) { // lazy store + // check if this type of this event already register in the lazy store + var t = args[2]; + if (!this.checkTypeInLazyStore(evt, t)) { + fnSet.add(args); + } + } else { + if (!this.checkContentExist(args, fnSet)) { + this.logger('(addToStore)', "insert new", args); + fnSet.add(args); + } + } + } else { // add straight to lazy store + fnSet.add(args); + } + store.set(evt, fnSet); + return [store, fnSet.size] + }; + + /** + * @param {array} args for compare + * @param {object} fnSet A Set to search from + * @return {boolean} true on exist + */ + NbEventServiceBase.prototype.checkContentExist = function checkContentExist (args, fnSet) { + var list = Array.from(fnSet); + return !!list.filter(function (l) { + var hash = l[0]; + if (hash === args[0]) { + return true; + } + return false; + }).length; + }; + + /** + * get the existing type to make sure no mix type add to the same store + * @param {string} evtName event name + * @param {string} type the type to check + * @return {boolean} true you can add, false then you can't add this type + */ + NbEventServiceBase.prototype.checkTypeInStore = function checkTypeInStore (evtName, type) { + this.validateEvt(evtName, type); + var all = this.$get(evtName, true); + if (all === false) { + // pristine it means you can add + return true; + } + // it should only have ONE type in ONE event store + return !all.filter(function (list) { + var t = list[3]; + return type !== t; + }).length; + }; + + /** + * This is checking just the lazy store because the structure is different + * therefore we need to use a new method to check it + */ + NbEventServiceBase.prototype.checkTypeInLazyStore = function checkTypeInLazyStore (evtName, type) { + this.validateEvt(evtName, type); + var store = this.lazyStore.get(evtName); + this.logger('(checkTypeInLazyStore)', store); + if (store) { + return !!Array + .from(store) + .filter(function (l) { + var t = l[2]; + return t !== type; + }).length + } + return false; + }; + + /** + * wrapper to re-use the addToStore, + * V1.3.0 add extra check to see if this type can add to this evt + * @param {string} evt event name + * @param {string} type on or once + * @param {function} callback function + * @param {object} context the context the function execute in or null + * @return {number} size of the store + */ + NbEventServiceBase.prototype.addToNormalStore = function addToNormalStore (evt, type, callback, context) { + if ( context === void 0 ) context = null; + + this.logger('(addToNormalStore)', evt, type, 'try to add to normal store'); + // @TODO we need to check the existing store for the type first! + if (this.checkTypeInStore(evt, type)) { + this.logger('(addToNormalStore)', (type + " can add to " + evt + " normal store")); + var key = this.hashFnToKey(callback); + var args = [this.normalStore, evt, key, callback, context, type]; + var ref = Reflect.apply(this.addToStore, this, args); + var _store = ref[0]; + var size = ref[1]; + this.normalStore = _store; + return size; + } + return false; + }; + + /** + * Add to lazy store this get calls when the callback is not register yet + * so we only get a payload object or even nothing + * @param {string} evt event name + * @param {array} payload of arguments or empty if there is none + * @param {object} [context=null] the context the callback execute in + * @param {string} [type=false] register a type so no other type can add to this evt + * @return {number} size of the store + */ + NbEventServiceBase.prototype.addToLazyStore = function addToLazyStore (evt, payload, context, type) { + if ( payload === void 0 ) payload = []; + if ( context === void 0 ) context = null; + if ( type === void 0 ) type = false; + + // this is add in V1.6.0 + // when there is type then we will need to check if this already added in lazy store + // and no other type can add to this lazy store + var args = [this.lazyStore, evt, this.toArray(payload), context]; + if (type) { + args.push(type); + } + var ref = Reflect.apply(this.addToStore, this, args); + var _store = ref[0]; + var size = ref[1]; + this.lazyStore = _store; + return size; + }; + + /** + * make sure we store the argument correctly + * @param {*} arg could be array + * @return {array} make sured + */ + NbEventServiceBase.prototype.toArray = function toArray (arg) { + return Array.isArray(arg) ? arg : [arg]; + }; + + /** + * setter to store the Set in private + * @param {object} obj a Set + */ + prototypeAccessors.normalStore.set = function (obj) { + NB_EVENT_SERVICE_PRIVATE_STORE.set(this, obj); + }; + + /** + * @return {object} Set object + */ + prototypeAccessors.normalStore.get = function () { + return NB_EVENT_SERVICE_PRIVATE_STORE.get(this) + }; + + /** + * setter to store the Set in lazy store + * @param {object} obj a Set + */ + prototypeAccessors.lazyStore.set = function (obj) { + NB_EVENT_SERVICE_PRIVATE_LAZY.set(this , obj); + }; + + /** + * @return {object} the lazy store Set + */ + prototypeAccessors.lazyStore.get = function () { + return NB_EVENT_SERVICE_PRIVATE_LAZY.get(this) + }; + + /** + * generate a hashKey to identify the function call + * The build-in store some how could store the same values! + * @param {function} fn the converted to string function + * @return {string} hashKey + */ + NbEventServiceBase.prototype.hashFnToKey = function hashFnToKey (fn) { + return hashCode(fn.toString()) + ''; + }; + + Object.defineProperties( NbEventServiceBase.prototype, prototypeAccessors ); + + return NbEventServiceBase; +}(SuspendClass)); + +// The top level +// export +var EventService = /*@__PURE__*/(function (NbStoreService) { + function EventService(config) { + if ( config === void 0 ) config = {}; + + NbStoreService.call(this, config); + } + + if ( NbStoreService ) EventService.__proto__ = NbStoreService; + EventService.prototype = Object.create( NbStoreService && NbStoreService.prototype ); + EventService.prototype.constructor = EventService; + + var prototypeAccessors = { $done: { configurable: true } }; + + /** + * logger function for overwrite + */ + EventService.prototype.logger = function logger () {}; + + ////////////////////////// + // PUBLIC METHODS // + ////////////////////////// + + /** + * Register your evt handler, note we don't check the type here, + * we expect you to be sensible and know what you are doing. + * @param {string} evt name of event + * @param {function} callback bind method --> if it's array or not + * @param {object} [context=null] to execute this call in + * @return {number} the size of the store + */ + EventService.prototype.$on = function $on (evt , callback , context) { + var this$1 = this; + if ( context === void 0 ) context = null; + + var type = 'on'; + this.validate(evt, callback); + // first need to check if this evt is in lazy store + var lazyStoreContent = this.takeFromStore(evt); + // this is normal register first then call later + if (lazyStoreContent === false) { + this.logger('($on)', (evt + " callback is not in lazy store")); + // @TODO we need to check if there was other listener to this + // event and are they the same type then we could solve that + // register the different type to the same event name + + return this.addToNormalStore(evt, type, callback, context) + } + this.logger('($on)', (evt + " found in lazy store")); + // this is when they call $trigger before register this callback + var size = 0; + lazyStoreContent.forEach(function (content) { + var payload = content[0]; + var ctx = content[1]; + var t = content[2]; + if (t && t !== type) { + throw new Error(("You are trying to register an event already been taken by other type: " + t)) + } + this$1.run(callback, payload, context || ctx); + size += this$1.addToNormalStore(evt, type, callback, context || ctx); + }); + return size; + }; + + /** + * once only registered it once, there is no overwrite option here + * @NOTE change in v1.3.0 $once can add multiple listeners + * but once the event fired, it will remove this event (see $only) + * @param {string} evt name + * @param {function} callback to execute + * @param {object} [context=null] the handler execute in + * @return {boolean} result + */ + EventService.prototype.$once = function $once (evt , callback , context) { + if ( context === void 0 ) context = null; + + this.validate(evt, callback); + var type = 'once'; + var lazyStoreContent = this.takeFromStore(evt); + // this is normal register before call $trigger + var nStore = this.normalStore; + if (lazyStoreContent === false) { + this.logger('($once)', (evt + " not in the lazy store")); + // v1.3.0 $once now allow to add multiple listeners + return this.addToNormalStore(evt, type, callback, context) + } else { + // now this is the tricky bit + // there is a potential bug here that cause by the developer + // if they call $trigger first, the lazy won't know it's a once call + // so if in the middle they register any call with the same evt name + // then this $once call will be fucked - add this to the documentation + this.logger('($once)', lazyStoreContent); + var list = Array.from(lazyStoreContent); + // should never have more than 1 + var ref = list[0]; + var payload = ref[0]; + var ctx = ref[1]; + var t = ref[2]; + if (t && t !== type) { + throw new Error(("You are trying to register an event already been taken by other type: " + t)) + } + this.run(callback, payload, context || ctx); + // remove this evt from store + this.$off(evt); + } + }; + + /** + * This one event can only bind one callbackback + * @param {string} evt event name + * @param {function} callback event handler + * @param {object} [context=null] the context the event handler execute in + * @return {boolean} true bind for first time, false already existed + */ + EventService.prototype.$only = function $only (evt, callback, context) { + var this$1 = this; + if ( context === void 0 ) context = null; + + this.validate(evt, callback); + var type = 'only'; + var added = false; + var lazyStoreContent = this.takeFromStore(evt); + // this is normal register before call $trigger + var nStore = this.normalStore; + if (!nStore.has(evt)) { + this.logger("($only)", (evt + " add to store")); + added = this.addToNormalStore(evt, type, callback, context); + } + if (lazyStoreContent !== false) { + // there are data store in lazy store + this.logger('($only)', (evt + " found data in lazy store to execute")); + var list = Array.from(lazyStoreContent); + // $only allow to trigger this multiple time on the single handler + list.forEach( function (l) { + var payload = l[0]; + var ctx = l[1]; + var t = l[2]; + if (t && t !== type) { + throw new Error(("You are trying to register an event already been taken by other type: " + t)) + } + this$1.run(callback, payload, context || ctx); + }); + } + return added; + }; + + /** + * $only + $once this is because I found a very subtile bug when we pass a + * resolver, rejecter - and it never fire because that's OLD adeed in v1.4.0 + * @param {string} evt event name + * @param {function} callback to call later + * @param {object} [context=null] exeucte context + * @return {void} + */ + EventService.prototype.$onlyOnce = function $onlyOnce (evt, callback, context) { + if ( context === void 0 ) context = null; + + this.validate(evt, callback); + var type = 'onlyOnce'; + var added = false; + var lazyStoreContent = this.takeFromStore(evt); + // this is normal register before call $trigger + var nStore = this.normalStore; + if (!nStore.has(evt)) { + this.logger("($onlyOnce)", (evt + " add to store")); + added = this.addToNormalStore(evt, type, callback, context); + } + if (lazyStoreContent !== false) { + // there are data store in lazy store + this.logger('($onlyOnce)', lazyStoreContent); + var list = Array.from(lazyStoreContent); + // should never have more than 1 + var ref = list[0]; + var payload = ref[0]; + var ctx = ref[1]; + var t = ref[2]; + if (t && t !== 'onlyOnce') { + throw new Error(("You are trying to register an event already been taken by other type: " + t)) + } + this.run(callback, payload, context || ctx); + // remove this evt from store + this.$off(evt); + } + return added; + }; + + /** + * This is a shorthand of $off + $on added in V1.5.0 + * @param {string} evt event name + * @param {function} callback to exeucte + * @param {object} [context = null] or pass a string as type + * @param {string} [type=on] what type of method to replace + * @return {} + */ + EventService.prototype.$replace = function $replace (evt, callback, context, type) { + if ( context === void 0 ) context = null; + if ( type === void 0 ) type = 'on'; + + if (this.validateType(type)) { + this.$off(evt); + var method = this['$' + type]; + return Reflect.apply(method, this, [evt, callback, context]) + } + throw new Error((type + " is not supported!")) + }; + + /** + * trigger the event + * @param {string} evt name NOT allow array anymore! + * @param {mixed} [payload = []] pass to fn + * @param {object|string} [context = null] overwrite what stored + * @param {string} [type=false] if pass this then we need to add type to store too + * @return {number} if it has been execute how many times + */ + EventService.prototype.$trigger = function $trigger (evt , payload , context, type) { + if ( payload === void 0 ) payload = []; + if ( context === void 0 ) context = null; + if ( type === void 0 ) type = false; + + this.validateEvt(evt); + var found = 0; + // first check the normal store + var nStore = this.normalStore; + this.logger('($trigger)', 'normalStore', nStore); + if (nStore.has(evt)) { + // @1.8.0 to add the suspend queue + var added = this.$queue(evt, payload, context, type); + this.logger('($trigger)', evt, 'found; add to queue: ', added); + if (added === true) { + return false; // not executed + } + var nSet = Array.from(nStore.get(evt)); + var ctn = nSet.length; + var hasOnce = false; + for (var i=0; i < ctn; ++i) { + ++found; + // this.logger('found', found) + var ref = nSet[i]; + var _ = ref[0]; + var callback = ref[1]; + var ctx = ref[2]; + var type$1 = ref[3]; + this.run(callback, payload, context || ctx); + if (type$1 === 'once' || type$1 === 'onlyOnce') { + hasOnce = true; + } + } + if (hasOnce) { + nStore.delete(evt); + } + return found; + } + // now this is not register yet + this.addToLazyStore(evt, payload, context, type); + return found; + }; + + /** + * this is an alias to the $trigger + * @NOTE breaking change in V1.6.0 we swap the parameter around + * @param {string} evt event name + * @param {*} params pass to the callback + * @param {string} type of call + * @param {object} context what context callback execute in + * @return {*} from $trigger + */ + EventService.prototype.$call = function $call (evt, params, type, context) { + if ( type === void 0 ) type = false; + if ( context === void 0 ) context = null; + + var args = [evt, params]; + args.push(context, type); + return Reflect.apply(this.$trigger, this, args) + }; + + /** + * remove the evt from all the stores + * @param {string} evt name + * @return {boolean} true actually delete something + */ + EventService.prototype.$off = function $off (evt) { + this.validateEvt(evt); + var stores = [ this.lazyStore, this.normalStore ]; + var found = false; + stores.forEach(function (store) { + if (store.has(evt)) { + found = true; + store.delete(evt); + } + }); + return found; + }; + + /** + * return all the listener from the event + * @param {string} evtName event name + * @param {boolean} [full=false] if true then return the entire content + * @return {array|boolean} listerner(s) or false when not found + */ + EventService.prototype.$get = function $get (evt, full) { + if ( full === void 0 ) full = false; + + this.validateEvt(evt); + var store = this.normalStore; + if (store.has(evt)) { + return Array + .from(store.get(evt)) + .map( function (l) { + if (full) { + return l; + } + var key = l[0]; + var callback = l[1]; + return callback; + }) + } + return false; + }; + + /** + * store the return result from the run + * @param {*} value whatever return from callback + */ + prototypeAccessors.$done.set = function (value) { + this.logger('($done)', 'value: ', value); + if (this.keep) { + this.result.push(value); + } else { + this.result = value; + } + }; + + /** + * @TODO is there any real use with the keep prop? + * getter for $done + * @return {*} whatever last store result + */ + prototypeAccessors.$done.get = function () { + if (this.keep) { + this.logger('(get $done)', this.result); + return this.result[this.result.length - 1] + } + return this.result; + }; + + Object.defineProperties( EventService.prototype, prototypeAccessors ); + + return EventService; +}(NbEventServiceBase)); + +// default + +// create a clone version so we know which one we actually is using +var JsonqlWsEvt = /*@__PURE__*/(function (NBEventService) { + function JsonqlWsEvt() { + NBEventService.call(this, {logger: getDebug('nb-event-service')}); + } + + if ( NBEventService ) JsonqlWsEvt.__proto__ = NBEventService; + JsonqlWsEvt.prototype = Object.create( NBEventService && NBEventService.prototype ); + JsonqlWsEvt.prototype.constructor = JsonqlWsEvt; + + var prototypeAccessors = { name: { configurable: true } }; + + prototypeAccessors.name.get = function () { + return 'jsonql-ws-client' + }; + + Object.defineProperties( JsonqlWsEvt.prototype, prototypeAccessors ); + + return JsonqlWsEvt; +}(EventService)); + +// This is ported back from ws-server and it will get use in the server / client side + +function extractSocketPart(contract) { + if (isKeyInObject(contract, 'socket')) { + return contract.socket; + } + return contract; +} + +/** + * @BUG we should check the socket part instead of expect the downstream to read the menu! + * We only need this when the enableAuth is true otherwise there is only one namespace + * @param {object} contract the socket part of the contract file + * @return {object} 1. remap the contract using the namespace --> resolvers + * 2. the size of the object (1 all private, 2 mixed public with private) + * 3. which namespace is public + */ +function groupByNamespace(contract) { + var socket = extractSocketPart(contract); + + var nspSet = {}; + var size = 0; + var publicNamespace; + for (var resolverName in socket) { + var params = socket[resolverName]; + var namespace = params.namespace; + if (namespace) { + if (!nspSet[namespace]) { + ++size; + nspSet[namespace] = {}; + } + nspSet[namespace][resolverName] = params; + if (!publicNamespace) { + if (params.public) { + publicNamespace = namespace; + } + } + } + } + return { size: size, nspSet: nspSet, publicNamespace: publicNamespace } +} + +// This is ported back from ws-client +// the idea if from https://decembersoft.com/posts/promises-in-serial-with-array-reduce/ +/** + * previously we already make sure the order of the namespaces + * and attach the auth client to it + * @param {array} promises array of unresolved promises + * @return {object} promise resolved with the array of promises resolved results + */ +function chainPromises(promises) { + return promises.reduce(function (promiseChain, currentTask) { return ( + promiseChain.then(function (chainResults) { return ( + currentTask.then(function (currentResult) { return ( + chainResults.concat( [currentResult]) + ); }) + ); }) + ); }, Promise.resolve([])) +} + +/** + * The code was extracted from: + * https://github.com/davidchambers/Base64.js + */ + +var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + +function InvalidCharacterError(message) { + this.message = message; +} + +InvalidCharacterError.prototype = new Error(); +InvalidCharacterError.prototype.name = 'InvalidCharacterError'; + +function polyfill (input) { + var str = String(input).replace(/=+$/, ''); + if (str.length % 4 == 1) { + throw new InvalidCharacterError("'atob' failed: The string to be decoded is not correctly encoded."); + } + for ( + // initialize result and counters + var bc = 0, bs, buffer, idx = 0, output = ''; + // get next character + buffer = str.charAt(idx++); + // character found in table? initialize bit storage and add its ascii value; + ~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer, + // and if not first of each 4 characters, + // convert the first 8 bits to one ascii character + bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0 + ) { + // try to find character in table (0-63, not found => -1) + buffer = chars.indexOf(buffer); + } + return output; +} + + +var atob = typeof window !== 'undefined' && window.atob && window.atob.bind(window) || polyfill; + +function InvalidTokenError(message) { + this.message = message; +} + +InvalidTokenError.prototype = new Error(); +InvalidTokenError.prototype.name = 'InvalidTokenError'; + +var obj$2, obj$1$1, obj$2$1, obj$3, obj$4, obj$5, obj$6, obj$7, obj$8; + +var appProps = { + algorithm: createConfig$1(HSA_ALGO, [STRING_TYPE]), + expiresIn: createConfig$1(false, [BOOLEAN_TYPE, NUMBER_TYPE, STRING_TYPE], ( obj$2 = {}, obj$2[ALIAS_KEY] = 'exp', obj$2[OPTIONAL_KEY] = true, obj$2 )), + notBefore: createConfig$1(false, [BOOLEAN_TYPE, NUMBER_TYPE, STRING_TYPE], ( obj$1$1 = {}, obj$1$1[ALIAS_KEY] = 'nbf', obj$1$1[OPTIONAL_KEY] = true, obj$1$1 )), + audience: createConfig$1(false, [BOOLEAN_TYPE, STRING_TYPE], ( obj$2$1 = {}, obj$2$1[ALIAS_KEY] = 'iss', obj$2$1[OPTIONAL_KEY] = true, obj$2$1 )), + subject: createConfig$1(false, [BOOLEAN_TYPE, STRING_TYPE], ( obj$3 = {}, obj$3[ALIAS_KEY] = 'sub', obj$3[OPTIONAL_KEY] = true, obj$3 )), + issuer: createConfig$1(false, [BOOLEAN_TYPE, STRING_TYPE], ( obj$4 = {}, obj$4[ALIAS_KEY] = 'iss', obj$4[OPTIONAL_KEY] = true, obj$4 )), + noTimestamp: createConfig$1(false, [BOOLEAN_TYPE], ( obj$5 = {}, obj$5[OPTIONAL_KEY] = true, obj$5 )), + header: createConfig$1(false, [BOOLEAN_TYPE, STRING_TYPE], ( obj$6 = {}, obj$6[OPTIONAL_KEY] = true, obj$6 )), + keyid: createConfig$1(false, [BOOLEAN_TYPE, STRING_TYPE], ( obj$7 = {}, obj$7[OPTIONAL_KEY] = true, obj$7 )), + mutatePayload: createConfig$1(false, [BOOLEAN_TYPE], ( obj$8 = {}, obj$8[OPTIONAL_KEY] = true, obj$8 )) +}; + +// ws client using native WebSocket + +function getWS() { + switch(true) { + case (typeof WebSocket !== 'undefined'): + return WebSocket; + case (typeof MozWebSocket !== 'undefined'): + return MozWebSocket; + // case (typeof global !== 'undefined'): + // return global.WebSocket || global.MozWebSocket; + case (typeof window !== 'undefined'): + return window.WebSocket || window.MozWebSocket; + // case (typeof self !== 'undefined'): + // return self.WebSocket || self.MozWebSocket; + default: + throw new JsonqlValidationError('WebSocket is NOT SUPPORTED!') + } +} + +var WS$1 = getWS(); + +// mapping the resolver to their respective nsp +var debug$1 = getDebug('process-contract'); + +/** + * Just make sure the object contain what we are looking for + * @param {object} opts configuration from checkOptions + * @return {object} the target content + */ +var getResolverList = function (contract) { + if (contract) { + var socket = contract.socket; + if (socket) { + return socket; + } + } + throw new JsonqlResolverNotFoundError(MISSING_PROP_ERR) +}; + +/** + * process the contract first + * @param {object} opts configuration + * @return {object} sorted list + */ +function processContract(opts) { + var obj; + + var contract = opts.contract; + var enableAuth = opts.enableAuth; + if (enableAuth) { + return groupByNamespace(contract) + } + return { + nspSet: ( obj = {}, obj[JSONQL_PATH] = getResolverList(contract), obj ), + publicNamespace: JSONQL_PATH, + size: 1 // this prop is pretty meaningless now + } +} + +// export the util methods + +var toArray$1 = function (arg) { return isArray$1(arg) ? arg : [arg]; }; + +/** + * very simple tool to create the event name + * @param {string} [...args] spread + * @return {string} join by _ + */ +var createEvt = function () { + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + return args.join('_'); +}; + +/** + * Unbind the event + * @param {object} ee EventEmitter + * @param {string} namespace + * @return {void} + */ +var clearMainEmitEvt = function (ee, namespace) { + var nsps = isArray$1(namespace) ? namespace : [namespace]; + nsps.forEach(function (n) { + ee.$off(createEvt(n, EMIT_EVT)); + }); +}; + +/** + * @param {*} args arguments to send + *@return {object} formatted payload + */ +var formatPayload = function (args) { + var obj; + + return ( + ( obj = {}, obj[QUERY_ARG_NAME] = args, obj ) +); +}; + +/** + * @param {object} nsps namespace as key + * @param {string} type of server + */ +var disconnect = function (nsps, type) { + if ( type === void 0 ) type = JS_WS_SOCKET_IO_NAME; + + try { + var method = type === JS_WS_SOCKET_IO_NAME ? 'disconnect' : 'terminate'; + for (var namespace in nsps) { + var nsp = nsps[namespace]; + if (nsp && nsp[method]) { + Reflect.apply(nsp[method], null, []); + } + } + } catch(e) { + // socket.io throw a this.destroy of undefined? + console.error('disconnect', e); + } +}; + +/** + * trigger errors on all the namespace onError handler + * @param {object} ee Event Emitter + * @param {array} namespaces nsps string + * @param {string} message optional + * @return {void} + */ +function triggerNamespacesOnError(ee, namespaces, message) { + namespaces.forEach( function (namespace) { + ee.$call(createEvt(namespace, ERROR_PROP_NAME), [{ message: message, namespace: namespace }]); + }); +} + +// @TODO port what is in the ws-main-handler +var debugFn$1 = getDebug('client-event-handler'); + +/** + * A fake ee handler + * @param {string} namespace nsp + * @param {object} ee EventEmitter + * @return {void} + */ +var notLoginWsHandler = function (namespace, ee) { + ee.$only( + createEvt(namespace, EMIT_EVT), + function(resolverName, args) { + debugFn$1('noLoginHandler hijack the ws call', namespace, resolverName, args); + var error = { + message: NOT_LOGIN_ERR_MSG + }; + // It should just throw error here and should not call the result + // because that's channel for handling normal event not the fake one + ee.$call(createEvt(namespace, resolverName, ERROR_PROP_NAME), [error]); + // also trigger the result handler, but wrap inside the error key + ee.$call(createEvt(namespace, resolverName, RESULT_PROP_NAME), [{ error: error }]); + } + ); +}; + +/** + * centralize all the comm in one place + * @param {object} opts configuration + * @param {array} namespaces namespace(s) + * @param {object} ee Event Emitter instance + * @param {function} bindWsHandler binding the ee to ws + * @param {array} namespaces array of namespace available + * @param {object} nsps namespaced nsp + * @return {void} nothing + */ +function clientEventHandler(opts, nspMap, ee, bindWsHandler, namespaces, nsps) { + // loop + // @BUG for io this has to be in order the one with auth need to get call first + // The order of login is very import we need to run a waterfall here to make sure + // one is execute then the other + namespaces.forEach(function (namespace) { + if (nsps[namespace]) { + debugFn$1('call bindWsHandler', namespace); + var args = [namespace, nsps[namespace], ee]; + if (opts.serverType === SOCKET_IO) { + var nspSet = nspMap.nspSet; + args.push(nspSet[namespace]); + args.push(opts); + } + Reflect.apply(bindWsHandler, null, args); + } else { + // a dummy placeholder + notLoginWsHandler(namespace, ee); + } + }); + // this will be available regardless enableAuth + // because the server can log the client out + ee.$on(LOGOUT_EVENT_NAME, function() { + debugFn$1('LOGOUT_EVENT_NAME'); + // disconnect(nsps, opts.serverType) + // we need to issue error to all the namespace onError handler + triggerNamespacesOnError(ee, namespaces, LOGOUT_EVENT_NAME); + // rebind all of the handler to the fake one + namespaces.forEach( function (namespace) { + clearMainEmitEvt(ee, namespace); + // clear out the nsp + nsps[namespace] = false; + // add a NOT LOGIN error if call + notLoginWsHandler(namespace, ee); + }); + }); +} + +// take the ws reply data for use +var debugFn$2 = getDebug('extract-ws-payload'); + +var keys$1 = [ WS_REPLY_TYPE, WS_EVT_NAME, WS_DATA_NAME ]; + +/** + * @param {object} payload should be string when reply but could be transformed + * @return {boolean} true is OK + */ +var isWsReply = function (payload) { + var data = payload.data; + if (data) { + var result = keys$1.filter(function (key) { return isKeyInObject(data, key); }); + return (result.length === keys$1.length) ? data : false; + } + return false; +}; + +/** + * @param {object} payload This is the entire ws Event Object + * @return {object} false on failed + */ +var extractWsPayload = function (payload) { + var data = payload.data; + var json = isString$1(data) ? JSON.parse(data) : data; + // debugFn('extractWsPayload', json) + var fdata; + if ((fdata = isWsReply(json)) !== false) { + return { + resolverName: fdata[WS_EVT_NAME], + data: fdata[WS_DATA_NAME], + type: fdata[WS_REPLY_TYPE] + }; + } + throw new JsonqlError('payload can not be decoded', payload) +}; + +// the WebSocket main handler +var debugFn$3 = getDebug('ws-main-handler'); + +/** + * under extremely circumstances we might not even have a resolverName, then + * we issue a global error for the developer to catch it + * @param {object} ee event emitter + * @param {string} namespace nsp + * @param {string} resolverName resolver + * @param {object} json decoded payload or error object + */ +var errorTypeHandler = function (ee, namespace, resolverName, json) { + var evt = [namespace]; + if (resolverName) { + debugFn$3(("a global error on " + namespace)); + evt.push(resolverName); + } + evt.push(ERROR_PROP_NAME); + var evtName = Reflect.apply(createEvt, null, evt); + // test if there is a data field + var payload = json.data || json; + ee.$trigger(evtName, [payload]); +}; + +/** + * Binding the even to socket normally + * @param {string} namespace + * @param {object} ws the nsp + * @param {object} ee EventEmitter + * @return {object} promise resolve after the onopen event + */ +function wsMainHandlerAction(namespace, ws, ee) { + // send + ws.onopen = function() { + // we just call the onReady + ee.$call(READY_PROP_NAME, namespace); + // add listener + ee.$only( + createEvt(namespace, EMIT_EVT), + function(resolverName, args) { + debugFn$3('calling server', resolverName, args); + ws.send( + createQueryStr$1(resolverName, args) + ); + } + ); + }; + + // reply + ws.onmessage = function(payload) { + try { + var json = extractWsPayload(payload); + debugFn$3('Hear from server', json); + var resolverName = json.resolverName; + var type = json.type; + switch (type) { + case EMIT_REPLY_TYPE: + var r = ee.$trigger(createEvt(namespace, resolverName, MESSAGE_PROP_NAME), [json]); + debugFn$3(MESSAGE_PROP_NAME, r); + break; + case ACKNOWLEDGE_REPLY_TYPE: + debugFn$3(RESULT_PROP_NAME, json); + var x = ee.$trigger(createEvt(namespace, resolverName, RESULT_PROP_NAME), [json]); + debugFn$3('onResult add to event?', x); + break; + case ERROR_TYPE: + // this is handled error and we won't throw it + // we need to extract the error from json + errorTypeHandler(ee, namespace, resolverName, json); + break; + // @TODO there should be an error type instead of roll into the other two types? TBC + default: + // if this happen then we should throw it and halt the operation all together + debugFn$3('Unhandled event!', json); + errorTypeHandler(ee, namespace, resolverName, json); + // let error = {error: {'message': 'Unhandled event!', type}}; + // ee.$trigger(createEvt(namespace, resolverName, RESULT_PROP_NAME), [error]) + } + } catch(e) { + errorTypeHandler(ee, namespace, false, e); + } + }; + // when the server close the connection + ws.onclose = function() { + debugFn$3('ws.onclose'); + // @TODO what to do with this + // ee.$trigger(LOGOUT_EVENT_NAME, [namespace]) + }; + // listen to the LOGOUT_EVENT_NAME + ee.$on(LOGOUT_EVENT_NAME, function close() { + try { + debugFn$3('terminate ws connection'); + ws.terminate(); + } catch(e) { + debugFn$3('terminate ws error', e); + } + }); +} + +// make this another standalone module +var debugFn$4 = getDebug('ws-create-client'); + +/** + * Because the nsps can be throw away so it doesn't matter the scope + * this will get reuse again + * @param {object} opts configuration + * @param {object} nspMap from contract + * @param {string|null} token whether we have the token at run time + * @return {object} nsps namespace with namespace as key + */ +var createNsps = function(opts, nspMap, token) { + var nspSet = nspMap.nspSet; + var publicNamespace = nspMap.publicNamespace; + var login = false; + var namespaces = []; + var nsps = {}; + // first we need to binding all the events handler + if (opts.enableAuth && opts.useJwt) { + login = true; // just saying we need to listen to login event + namespaces = getNamespaceInOrder(nspSet, publicNamespace); + nsps = namespaces.map(function (namespace, i) { + var obj, obj$1, obj$2; + + if (i === 0) { + if (token) { + opts.token = token; + return ( obj = {}, obj[namespace] = nspAuthClient(namespace, opts), obj ) + } + return ( obj$1 = {}, obj$1[namespace] = false, obj$1 ) + } + return ( obj$2 = {}, obj$2[namespace] = nspClient(namespace, opts), obj$2 ) + }).reduce(function (first, next) { return Object.assign(first, next); }, {}); + } else { + var namespace = getNameFromPayload$1(nspSet); + namespaces.push(namespace); + // standard without login + // the stock version should not have a namespace + nsps[namespace] = nspClient(false, opts); + } + // return + return { nsps: nsps, namespaces: namespaces, login: login } +}; + +/** + * create a ws client + * @param {object} opts configuration + * @param {object} nspMap namespace with resolvers + * @param {object} ee EventEmitter to pass through + * @return {object} what comes in what goes out + */ +function createClient(opts, nspMap, ee) { + // arguments that don't change + var args = [opts, nspMap, ee, wsMainHandlerAction]; + // now create the nsps + var ref = createNsps(opts, nspMap, opts.token); + var nsps = ref.nsps; + var namespaces = ref.namespaces; + var login = ref.login; + // binding the listeners - and it will listen to LOGOUT event + // to unbind itself, and the above call will bind it again + Reflect.apply(clientEventHandler, null, args.concat([namespaces, nsps])); + // setup listener + if (login) { + ee.$only(LOGIN_EVENT_NAME, function(token) { + disconnect(nsps, JS_WS_NAME); + // @TODO should we trigger error on this one? + // triggerNamespacesOnError(ee, namespaces, LOGIN_EVENT_NAME) + clearMainEmitEvt(ee, namespaces); + debugFn$4('LOGIN_EVENT_NAME', token); + var newNsps = createNsps(opts, nspMap, token); + // rebind it + Reflect.apply( + clientEventHandler, + null, + args.concat([newNsps.namespaces, newNsps.nsps]) + ); + }); + } + // return what input + return { opts: opts, nspMap: nspMap, ee: ee } +} + +// we only need to export one interface from now on + +var debugFn$5 = getDebug('io-main-handler'); + +/** + * @param {object} ee Event Emitter + * @param {string} namespace namespace of this nsp + * @param {string} resolverName resolver to handle this call + * @return {function} capture the result + */ +var resultHandler = function (ee, namespace, resolverName, evt) { + if ( evt === void 0 ) evt = RESULT_PROP_NAME; + + return function (result) { + ee.$trigger(createEvt(namespace, resolverName, evt), [result]); + } +}; + +/** + * @param {object} nspSet resolver list + * @param {object} nsp nsp instance + * @param {object} ee Event Emitter + * @param {string} namespace name of this nsp + * @return {void} + */ +var createResolverListener = function (nspSet, nsp, ee, namespace) { + for (var resolverName in nspSet) { + nsp.on( + resolverName, + resultHandler(ee, namespace, resolverName, MESSAGE_PROP_NAME) + ); + } +}; + +/** + * @param {object} nsp instance + * @param {object} ee Event Emitter + * @param {string} namespace name of this nsp + * @return {void} + */ +var mainEventHandler = function (nsp, ee, namespace) { + ee.$only( + createEvt(namespace, EMIT_EVT), + function resolverEmitHandler(resolverName, args) { + debugFn$5('mainEventHandler', resolverName, args); + nsp.emit( + resolverName, + formatPayload(args), + resultHandler(ee, namespace, resolverName) + ); + } + ); +}; + +/** + * it makes no different at this point if we know its connection establish or not + * We should actually know this before hand before we call here + * @param {string} namespace of this client + * @param {object} socket this is the resolved nsp connection object + * @param {object} ee Event Emitter + * @param {object} nspSet the list of resolvers + * @param {object} opts configuration + */ +function ioMainHandler(namespace, socket, ee, nspSet, opts) { + // the main listener for all the client resolvers + mainEventHandler(socket, ee, namespace); + // it doesn't make much different between inside the connect or not + // loop through to create the listeners + createResolverListener(nspSet, socket, ee, namespace); + //@TODO next we need to add a ERROR handler + // The server side is not implementing a global ERROR call yet + // and the result or message error will be handle individually by their callback + // listen to the server close event + socket.on('disconnect', function disconnect() { + debugFn$5('io.disconnect'); + // TBC what to do with this + // ee.$trigger(LOGOUT_EVENT_NAME, [namespace]) + }); + // listen to the logout event + ee.$on(LOGOUT_EVENT_NAME, function logoutHandler() { + try { + debugFn$5('terminate ws connection'); + socket.close(); + } catch(e) { + debugFn$5('terminate ws error', e); + } + }); + // the last one to fire + ee.$trigger(READY_PROP_NAME, namespace); +} + +// this will create the socket.io client +var debugFn$6 = getDebug('io-create-client'); + +// just to make it less ugly +var mapNsps = function (nsps, namespaces) { return nsps + .map(function (nsp, i) { + var obj; + + return (( obj = {}, obj[namespaces[i]] = nsp, obj )); + }) + .reduce(function (last, next) { return Object.assign(last,next); }, {}); }; + +/** + * This one will run the create nsps in sequence and make sure + * the auth one connect before we call the others + * @param {object} opts configuration + * @param {object} nspMap contract map + * @param {string} token validation + * @return {object} promise resolve with namespaces, nsps in same order array + */ +var createAuthNsps = function(opts, nspMap, token, namespaces) { + var publicNamespace = nspMap.publicNamespace; + opts.token = token; + var p1 = function () { return nspAuthClient(namespaces[0], opts); }; + var p2 = function () { return nspClient(namespaces[1], opts); }; + return chainPromises([p1(), p2()]) + .then(function (nsps) { return ({ + nsps: mapNsps(nsps, namespaces), + namespaces: namespaces, + login: false + }); }) +}; + +/** + * Because the nsps can be throw away so it doesn't matter the scope + * this will get reuse again + * @param {object} opts configuration + * @param {object} nspMap from contract + * @param {string|null} token whether we have the token at run time + * @return {object} nsps namespace with namespace as key + */ +var createNsps$1 = function(opts, nspMap, token) { + var nspSet = nspMap.nspSet; + var publicNamespace = nspMap.publicNamespace; + var login = false; + // first we need to binding all the events handler + if (opts.enableAuth && opts.useJwt) { + var namespaces = getNamespaceInOrder(nspSet, publicNamespace); + debugFn$6('namespaces', namespaces); + login = opts.useJwt; // just saying we need to listen to login event + if (token) { + debugFn$6('call createAuthNsps'); + return createAuthNsps(opts, nspMap, token, namespaces) + } + debugFn$6('init with a placeholder'); + return nspClient(publicNamespace, opts) + .then(function (nsp) { + var obj; + + return ({ + nsps: ( obj = {}, obj[ publicNamespace ] = nsp, obj[ namespaces[0] ] = false, obj ), + namespaces: namespaces, + login: login + }); + }) + } + // standard without login + // the stock version should not have a namespace + return nspClient(false, opts) + .then(function (nsp) { + var obj; + + return ({ + nsps: ( obj = {}, obj[publicNamespace] = nsp, obj ), + namespaces: [publicNamespace], + login: login + }); + }) +}; + + + +/** + * This is just copy of the ws version we need to figure + * out how to deal with the roundtrip login later + * @param {object} opts configuration + * @param {object} nspMap namespace with resolvers + * @param {object} ee EventEmitter to pass through + * @return {object} what comes in what goes out + */ +function createClient$1(opts, nspMap, ee) { + // arguments don't change + var args = [opts, nspMap, ee, ioMainHandler]; + return createNsps$1(opts, nspMap, opts.token) + .then( function (ref) { + var nsps = ref.nsps; + var namespaces = ref.namespaces; + var login = ref.login; + + // binding the listeners - and it will listen to LOGOUT event + // to unbind itself, and the above call will bind it again + Reflect.apply(clientEventHandler, null, args.concat([namespaces, nsps])); + if (login) { + ee.$only(LOGIN_EVENT_NAME, function(token) { + // here we should disconnect all the previous nsps + disconnect(nsps); + // first trigger a LOGOUT event to unbind ee to ws + // ee.$trigger(LOGOUT_EVENT_NAME) // <-- this seems to cause a lot of problems + clearMainEmitEvt(ee, namespaces); + debugFn$6('LOGIN_EVENT_NAME'); + createNsps$1(opts, nspMap, token) + .then(function (newNsps) { + // rebind it + Reflect.apply( + clientEventHandler, + null, + args.concat([newNsps.namespaces, newNsps.nsps]) + ); + }); + }); + } + // return this will also works because the outter call are in promise chain + return { opts: opts, nspMap: nspMap, ee: ee } + }) +} + +/** + * get the create client instance function + * @param {string} type of client + * @return {function} the actual methods + * @public + */ +function createSocketClient(opts, nspMap, ee) { + switch (opts.serverType) { + case SOCKET_IO: + return createClient$1(opts, nspMap, ee) + case WS: + return createClient(opts, nspMap, ee) + default: + throw new JsonqlError(SOCKET_NOT_DEFINE_ERR) + } +} + +// generator resolvers +var EMIT_EVT$1 = EMIT_EVT; +var UNKNOWN_RESULT$1 = UNKNOWN_RESULT; +var MY_NAMESPACE$1 = MY_NAMESPACE; +var debugFn$7 = getDebug('generator'); + +/** + * prepare the methods + * @param {object} opts configuration + * @param {object} nspMap resolvers index by their namespace + * @param {object} ee EventEmitter + * @return {object} of resolvers + * @public + */ +function generator(opts, nspMap, ee) { + var obj = {}; + var nspSet = nspMap.nspSet; + for (var namespace in nspSet) { + var list = nspSet[namespace]; + for (var resolverName in list) { + var params = list[resolverName]; + var fn = createResolver(ee, namespace, resolverName, params); + obj[resolverName] = setupResolver(namespace, resolverName, params, fn, ee); + } + } + // add error handler + createNamespaceErrorHandler(obj, ee, nspSet); + // add onReady handler + createOnReadyhandler(obj, ee); + // Auth related methods + createAuthMethods(obj, ee, opts); + // this is a helper method for the developer to know the namespace inside + obj.getNsp = function () { + return Object.keys(nspSet) + }; + // output + return obj; +} + +/** + * create the actual function to send message to server + * @param {object} ee EventEmitter instance + * @param {string} namespace this resolver end point + * @param {string} resolverName name of resolver as event name + * @param {object} params from contract + * @return {function} resolver + */ +function createResolver(ee, namespace, resolverName, params) { + // note we pass the new withResult=true option + return function() { + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + return validateAsync$1(args, params.params, true) + .then( function (_args) { return actionCall(ee, namespace, resolverName, _args); } ) + .catch(finalCatch) + } +} + +/** + * just wrapper + * @param {object} ee EventEmitter + * @param {string} namespace where this belongs + * @param {string} resolverName resolver + * @param {array} args arguments + * @return {void} nothing + */ +function actionCall(ee, namespace, resolverName, args) { + if ( args === void 0 ) args = []; + + debugFn$7(("actionCall: " + namespace + " " + resolverName), args); + ee.$trigger(createEvt(namespace, EMIT_EVT$1), [ + resolverName, + toArray$1(args) + ]); +} + +/** + * break out to use in different places to handle the return from server + * @param {object} data from server + * @param {function} resolver from promise + * @param {function} rejecter from promise + * @return {void} nothing + */ +function respondHandler(data, resolver, rejecter) { + if (isKeyInObject(data, 'error')) { + debugFn$7('rejecter called', data.error); + rejecter(data.error); + } else if (isKeyInObject(data, 'data')) { + debugFn$7('resolver called', data.data); + resolver(data.data); + } else { + debugFn$7('UNKNOWN_RESULT', data); + rejecter({message: UNKNOWN_RESULT$1, error: data}); + } +} + +/** + * Add extra property to the resolver + * @param {string} namespace where this belongs + * @param {string} resolverName name as event name + * @param {object} params from contract + * @param {function} fn resolver function + * @param {object} ee EventEmitter + * @return {function} resolver + */ +var setupResolver = function (namespace, resolverName, params, fn, ee) { + // also need to setup a getter to get back the namespace of this resolver + if (Object.getOwnPropertyDescriptor(fn, MY_NAMESPACE$1) === undefined) { + Object.defineProperty(fn, MY_NAMESPACE$1, { + value: namespace, + writeable: false + }); + } + // onResult handler + if (Object.getOwnPropertyDescriptor(fn, RESULT_PROP_NAME) === undefined) { + Object.defineProperty(fn, RESULT_PROP_NAME, { + set: function(resultCallback) { + if (typeof resultCallback === 'function') { + ee.$only( + createEvt(namespace, resolverName, RESULT_PROP_NAME), + function resultHandler(result) { + respondHandler(result, resultCallback, function (error) { + ee.$trigger(createEvt(namespace, resolverName, ERROR_PROP_NAME), error); + }); + } + ); + } + }, + get: function() { + return null; + } + }); + } + // we do need to add the send prop back because it's the only way to deal with + // bi-directional data stream + if (Object.getOwnPropertyDescriptor(fn, MESSAGE_PROP_NAME) === undefined) { + Object.defineProperty(fn, MESSAGE_PROP_NAME, { + set: function(messageCallback) { + // we expect this to be a function + if (typeof messageCallback === 'function') { + // did that add to the callback + var onMessageCallback = function (args) { + respondHandler(args, messageCallback, function (error) { + ee.$trigger(createEvt(namespace, resolverName, ERROR_PROP_NAME), error); + }); + }; + // register the handler for this message event + ee.$only(createEvt(namespace, resolverName, MESSAGE_PROP_NAME), onMessageCallback); + } + }, + get: function() { + return null; // just return nothing + } + }); + } + // add an ERROR_PROP_NAME handler + if (Object.getOwnPropertyDescriptor(fn, ERROR_PROP_NAME) === undefined) { + Object.defineProperty(fn, ERROR_PROP_NAME, { + set: function(resolverErrorHandler) { + if (typeof resolverErrorHandler === 'function') { + // please note ERROR_PROP_NAME can add multiple listners + ee.$only(createEvt(namespace, resolverName, ERROR_PROP_NAME), resolverErrorHandler); + } + }, + get: function() { + return null; + } + }); + } + // pairing with the server vesrion SEND_MSG_PROP_NAME + if (Object.getOwnPropertyDescriptor(fn, SEND_MSG_PROP_NAME) === undefined) { + Object.defineProperty(fn, SEND_MSG_PROP_NAME, { + set: function(messagePayload) { + var result = validateSync$1(toArray$1(messagePayload), params.params, true); + // here is the different we don't throw erro instead we trigger an + // onError + if (result[ERROR_KEY] && result[ERROR_KEY].length) { + ee.$call( + createEvt(namespace, resolverName, ERROR_PROP_NAME), + [JsonqlValidationError(resolverName, result[ERROR_KEY])] + ); + } else { + // there is no return only an action call + actionCall(ee, namespace, resolverName, result[DATA_KEY]); + } + }, + get: function() { + return null; // just return nothing + } + }); + } + return fn; +}; + +/** + * The problem is the namespace can have more than one + * and we only have on onError message + * @param {object} obj the client itself + * @param {object} ee Event Emitter + * @param {object} nspSet namespace keys + * @return {void} + */ +var createNamespaceErrorHandler = function (obj, ee, nspSet) { + // using the onError as name + // @TODO we should follow the convention earlier + // make this a setter for the obj itself + if (Object.getOwnPropertyDescriptor(obj, ERROR_PROP_NAME) === undefined) { + Object.defineProperty(obj, ERROR_PROP_NAME, { + set: function(namespaceErrorHandler) { + if (typeof namespaceErrorHandler === 'function') { + // please note ERROR_PROP_NAME can add multiple listners + for (var namespace in nspSet) { + // this one is very tricky, we need to make sure the trigger is calling + // with the namespace as well as the error + ee.$on(createEvt(namespace, ERROR_PROP_NAME), namespaceErrorHandler); + } + } + }, + get: function() { + return null; + } + }); + } +}; + +/** + * This event will fire when the socket.io.on('connection') and ws.onopen + * @param {object} obj the client itself + * @param {object} ee Event Emitter + * @param {object} nspSet namespace keys + * @return {void} + */ +var createOnReadyhandler = function (obj, ee, nspSet) { + if (Object.getOwnPropertyDescriptor(obj, READY_PROP_NAME) === undefined) { + Object.defineProperty(obj, READY_PROP_NAME, { + set: function(onReadyCallback) { + if (typeof onReadyCallback === 'function') { + // reduce it down to just one flat level + var result = ee.$on(READY_PROP_NAME, onReadyCallback); + } + }, + get: function() { + return null; + } + }); + } +}; + +/** + * Create auth related methods + * @param {object} obj the client itself + * @param {object} ee Event Emitter + * @param {object} opts configuration + * @return {void} + */ +var createAuthMethods = function (obj, ee, opts) { + if (opts.enableAuth) { + // create an additonal login handler + // we require the token + obj[opts.loginHandlerName] = function (token) { + debugFn$7(opts.loginHandlerName, token); + if (token && isString$1(token)) { + return ee.$trigger(LOGIN_EVENT_NAME, [token]) + } + throw new JsonqlValidationError(opts.loginHandlerName) + }; + // logout event handler + obj[opts.logoutHandlerName] = function () { + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + ee.$trigger(LOGOUT_EVENT_NAME, args); + }; + } +}; + +// main api to get the ws-client + +/** + * The main interface to create the wsClient for use + * @param {function} clientGenerator this is an internal way to generate node or browser client + * @return {function} wsClient + * @public + */ +function main(clientGenerator) { + /** + * @param {object} config configuration + * @param {object} [eventEmitter=false] this will be the bridge between clients + * @return {object} wsClient + */ + var wsClient = function (config, eventEmitter) { + if ( eventEmitter === void 0 ) eventEmitter = false; + + return checkOptions(config) + .then(function (opts) { return ({ + opts: opts, + nspMap: processContract(opts), + ee: eventEmitter || new JsonqlWsEvt() + }); } + ) + .then(clientGenerator) + .then( + function (ref) { + var opts = ref.opts; + var nspMap = ref.nspMap; + var ee = ref.ee; + + return createSocketClient(opts, nspMap, ee); + } + ) + .then( + function (ref) { + var opts = ref.opts; + var nspMap = ref.nspMap; + var ee = ref.ee; + + return generator(opts, nspMap, ee); + } + ) + .catch(function (err) { + console.error('jsonql-ws-client init error', err); + }) + }; + // use the Object.addProperty trick + Object.defineProperty(wsClient, 'CLIENT_TYPE_INFO', { + value: 'version: 1.0.0-beta.2 module: cjs', + writable: false + }); + return wsClient; +} + +module.exports = main; diff --git a/packages/ws-base/src/client/utils/check-options.js b/packages/ws-base/src/client/utils/check-options.js new file mode 100644 index 0000000000000000000000000000000000000000..045a2a8858f0f278d567c86124cf5cc9e0242056 --- /dev/null +++ b/packages/ws-base/src/client/utils/check-options.js @@ -0,0 +1,66 @@ +// create options +import { createConfig, checkConfigAsync, isContract, isNotEmpty } from 'jsonql-params-validator' +import { JsonqlValidationError, JsonqlCheckerError } from 'jsonql-errors' +import { + STRING_TYPE, + BOOLEAN_TYPE, + OBJECT_TYPE, + ENUM_KEY, + CHECKER_KEY, + JSONQL_PATH, + ISSUER_NAME, + LOGOUT_NAME +} from 'jsonql-constants' +import { SOCKET_IO, WS, AVAILABLE_SERVERS } from './constants' +import getDebug from './get-debug' +const debug = getDebug('check-options') + +const fixWss = (url, serverType) => { + // ws only allow ws:// path + if (serverType===WS) { + return url.replace('http://', 'ws://') + } + return url; +} + +const getHostName = () => ( + [window.location.protocol, window.location.host].join('//') +) + +const constProps = { + // this will be the switcher! + nspClient: null, + nspAuthClient: null, + // contructed path + wssPath: '' +} + +const defaultOptions = { + loginHandlerName: createConfig(ISSUER_NAME, [STRING_TYPE]), + logoutHandlerName: createConfig(LOGOUT_NAME, [STRING_TYPE]), + // we will use this for determine the socket.io client type as well + useJwt: createConfig(false, [BOOLEAN_TYPE, STRING_TYPE]), + hostname: createConfig(false, [STRING_TYPE]), + namespace: createConfig(JSONQL_PATH, [STRING_TYPE]), + wsOptions: createConfig({transports: ['websocket'], 'force new connection' : true}, [OBJECT_TYPE]), + serverType: createConfig(SOCKET_IO, [STRING_TYPE], {[ENUM_KEY]: AVAILABLE_SERVERS}), + // we require the contract already generated and pass here + contract: createConfig({}, [OBJECT_TYPE], {[CHECKER_KEY]: isContract}), + enableAuth: createConfig(false, [BOOLEAN_TYPE]), + token: createConfig(false, [STRING_TYPE]) +} +// export +export default function checkOptions(config) { + return checkConfigAsync(config, defaultOptions, constProps) + .then(opts => { + if (!opts.hostname) { + opts.hostname = getHostName() + } + // @TODO the contract now will supply the namespace information + // and we need to use that to group the namespace call + opts.wssPath = fixWss([opts.hostname, opts.namespace].join('/'), opts.serverType) + + debug('opts', opts) + return opts; + }) +} diff --git a/packages/ws-base/src/client/utils/client-generator.js b/packages/ws-base/src/client/utils/client-generator.js new file mode 100644 index 0000000000000000000000000000000000000000..05ccacb897f59d4dc62bef99c1cabb99981d1950 --- /dev/null +++ b/packages/ws-base/src/client/utils/client-generator.js @@ -0,0 +1,48 @@ +// generate the web socket connect client for browser +/* +all these has moved to other standalone modules +import { + socketIoRoundtripLogin, + socketIoClientAsync, + socketIoHandshakeLogin, + wsAuthClient, + wsClient +} from 'jsonql-jwt' +*/ +import { isString } from 'jsonql-params-validator' +import { JsonqlError } from 'jsonql-errors' +import { SOCKET_IO, WS } from './constants' +// import { IO_ROUNDTRIP_LOGIN, IO_HANDSHAKE_LOGIN } from 'jsonql-constants' +import getDebug from './get-debug' +const debug = getDebug('client-generator') + +/** + * create the web socket client + * @param {object} payload passing + * @return {object} just mutate it then pass it on + */ +export default function clientGenerator({opts, nspMap, ee}) { + switch (opts.serverType) { + case SOCKET_IO: + opts.nspClient = (...args) => ( + Reflect.apply(socketIoClientAsync, null, [io, ...args]) + ) + if (isString(opts.useJwt)) { + opts.nspAuthClient = (...args) => ( + Reflect.apply(socketIoRoundtripLogin, null, [io, ...args]) + ) + } else { + opts.nspAuthClient = (...args) => ( + Reflect.apply(socketIoHandshakeLogin, null, [io, ...args]) + ) + } + break; + case WS: + opts.nspClient = wsClient; + opts.nspAuthClient = wsAuthClient; + break; + default: + throw new JsonqlError(`Unknown serverType: ${opts.serverType}`) + } + return {opts, nspMap, ee} +} diff --git a/packages/ws-base/src/client/utils/constants.js b/packages/ws-base/src/client/utils/constants.js new file mode 100644 index 0000000000000000000000000000000000000000..928a430444d7902b9b759bc525a3cf2ed6bac2c7 --- /dev/null +++ b/packages/ws-base/src/client/utils/constants.js @@ -0,0 +1,49 @@ +// constants + +import { + EMIT_REPLY_TYPE, + JS_WS_SOCKET_IO_NAME, + JS_WS_NAME, + MESSAGE_PROP_NAME, + RESULT_PROP_NAME +} from 'jsonql-constants' + +const SOCKET_IO = JS_WS_SOCKET_IO_NAME; +const WS = JS_WS_NAME; + +const AVAILABLE_SERVERS = [SOCKET_IO, WS] + +const SOCKET_NOT_DEFINE_ERR = 'socket is not define in the contract file!'; + +const SERVER_NOT_SUPPORT_ERR = 'is not supported server name!'; + +const MISSING_PROP_ERR = 'Missing property in contract!'; + +const UNKNOWN_CLIENT_ERR = 'Unknown client type!'; + +const EMIT_EVT = EMIT_REPLY_TYPE; + +const NAMESPACE_KEY = 'namespaceMap'; + +const UNKNOWN_RESULT = 'UKNNOWN RESULT!'; + +const NOT_ALLOW_OP = 'This operation is not allow!'; + +const MY_NAMESPACE = 'myNamespace' + +export { + SOCKET_IO, + WS, + AVAILABLE_SERVERS, + SOCKET_NOT_DEFINE_ERR, + SERVER_NOT_SUPPORT_ERR, + MISSING_PROP_ERR, + UNKNOWN_CLIENT_ERR, + EMIT_EVT, + MESSAGE_PROP_NAME, + RESULT_PROP_NAME, + NAMESPACE_KEY, + UNKNOWN_RESULT, + NOT_ALLOW_OP, + MY_NAMESPACE +} diff --git a/packages/ws-base/src/client/utils/create-nsp-client.js b/packages/ws-base/src/client/utils/create-nsp-client.js new file mode 100644 index 0000000000000000000000000000000000000000..9b656813a2e476dff98c2c366687e2be932ed884 --- /dev/null +++ b/packages/ws-base/src/client/utils/create-nsp-client.js @@ -0,0 +1,34 @@ +// since both the ws and io version are +// pre-defined in the client-generator +// and this one will have the same parameters +// and the callback is identical +import getDebug from './get-debug' +const debugFn = getDebug('create-nsp-client') +/** + * wrapper method to create a nsp without login + * @param {string|boolean} namespace namespace url could be false + * @param {object} opts configuration + * @return {object} ws client instance + */ +const nspClient = (namespace, opts) => { + const { wssPath, wsOptions, hostname } = opts; + const url = namespace ? [hostname, namespace].join('/') : wssPath; + return opts.nspClient(url, wsOptions) +} + +/** + * wrapper method to create a nsp with token auth + * @param {string} namespace namespace url + * @param {object} opts configuration + * @return {object} ws client instance + */ +const nspAuthClient = (namespace, opts) => { + const { wssPath, token, wsOptions, hostname } = opts; + const url = namespace ? [hostname, namespace].join('/') : wssPath; + return opts.nspAuthClient(url, token, wsOptions) +} + +export { + nspClient, + nspAuthClient +} diff --git a/packages/ws-base/src/client/utils/ee.js b/packages/ws-base/src/client/utils/ee.js new file mode 100644 index 0000000000000000000000000000000000000000..dcf926206251efebe143720983fb1c5cfa5fc0a5 --- /dev/null +++ b/packages/ws-base/src/client/utils/ee.js @@ -0,0 +1,14 @@ +import getDebug from './get-debug' +// this will generate a event emitter and will be use everywhere +import NBEventService from 'nb-event-service' +// create a clone version so we know which one we actually is using +export default class JsonqlWsEvt extends NBEventService { + + constructor() { + super({logger: getDebug('nb-event-service')}) + } + + get name() { + return'jsonql-ws-client' + } +} diff --git a/packages/ws-base/src/client/utils/events.js b/packages/ws-base/src/client/utils/events.js new file mode 100644 index 0000000000000000000000000000000000000000..29906ef83b72761a8b6fe7a5186d1663a65a9ae6 --- /dev/null +++ b/packages/ws-base/src/client/utils/events.js @@ -0,0 +1,36 @@ +// moved from the index.js +import { JS_WS_SOCKET_IO_NAME } from 'jsonql-constants' +import { EMIT_EVT } from './constants' +/** + * Unbind the event + * @param {object} ee EventEmitter + * @param {string} namespace + * @return {void} + */ +export const clearMainEmitEvt = (ee, namespace) => { + let nsps = isArray(namespace) ? namespace : [namespace] + nsps.forEach(n => { + ee.$off(createEvt(n, EMIT_EVT)) + }) +} + +/** + * exeucte a disconnect call for different client + * @param {object} nsps namespace as key + * @param {string} type of server + */ +export const disconnect = (nsps, type = JS_WS_SOCKET_IO_NAME) => { + try { + // @TODO what happen to others? + const method = type === JS_WS_SOCKET_IO_NAME ? 'disconnect' : 'terminate'; + for (let namespace in nsps) { + let nsp = nsps[namespace] + if (nsp && nsp[method]) { + Reflect.apply(nsp[method], null, []) + } + } + } catch(e) { + // socket.io throw a this.destroy of undefined? + console.error('disconnect', e) + } +} diff --git a/packages/ws-base/src/client/utils/get-debug.js b/packages/ws-base/src/client/utils/get-debug.js new file mode 100644 index 0000000000000000000000000000000000000000..d6615dcf8d3f47f655499a91e4fa3b6192440abb --- /dev/null +++ b/packages/ws-base/src/client/utils/get-debug.js @@ -0,0 +1,22 @@ +// not using the jsonql-utils version +import debug from 'debug' +/** + * Try to normalize it to use between browser and node + * @param {string} name for the debug output + * @return {function} debug + */ +const getDebug = name => { + if (debug) { + return debug('jsonql-ws-client').extend(name) + } + return (...args) => { + console.info.apply(null, [name].concat(args)); + } +} +try { + if (window && window.localStorage) { + localStorage.setItem('DEBUG', 'jsonql-ws-client*'); + } +} catch(e) {} +// export it +export default getDebug; diff --git a/packages/ws-base/src/client/utils/get-namespace-in-order.js b/packages/ws-base/src/client/utils/get-namespace-in-order.js new file mode 100644 index 0000000000000000000000000000000000000000..823a6256e2b7e2ab03210a1cda31fb10239a51be --- /dev/null +++ b/packages/ws-base/src/client/utils/get-namespace-in-order.js @@ -0,0 +1,20 @@ + + +/** + * Got to make sure the connection order otherwise + * it will hang + * @param {object} nspSet contract + * @param {string} publicNamespace like the name said + * @return {array} namespaces in order + */ +export default function getNamespaceInOrder(nspSet, publicNamespace) { + let names = []; // need to make sure the order! + for (let namespace in nspSet) { + if (namespace === publicNamespace) { + names[1] = namespace; + } else { + names[0] = namespace; + } + } + return names; +} diff --git a/packages/ws-base/src/client/utils/index.js b/packages/ws-base/src/client/utils/index.js new file mode 100644 index 0000000000000000000000000000000000000000..39e0378fcc324bdfa33d6c3a6d4497c3dded4d0e --- /dev/null +++ b/packages/ws-base/src/client/utils/index.js @@ -0,0 +1,26 @@ +// export the util methods +import { isArray } from 'jsonql-params-validator' +import { toArray, createEvt, formatPayload } from 'jsonql-utils' + +import ee from './ee' +import getDebug from './get-debug' +import * as constants from './constants' +import checkOptions from './check-options' +import processContract from './process-contract' +import getNamespaceInOrder from './get-namespace-in-order' +import { clearMainEmitEvt, disconnect } from './events' + +// export +export { + getNamespaceInOrder, + createEvt, + clearMainEmitEvt, + checkOptions, + ee, + constants, + getDebug, + processContract, + toArray, + formatPayload, + disconnect +} diff --git a/packages/ws-base/src/client/utils/process-contract.js b/packages/ws-base/src/client/utils/process-contract.js new file mode 100644 index 0000000000000000000000000000000000000000..7ec6a8554d01e10caa714b6d0eebb0d4d0b0a4e7 --- /dev/null +++ b/packages/ws-base/src/client/utils/process-contract.js @@ -0,0 +1,41 @@ +// mapping the resolver to their respective nsp + +import { JSONQL_PATH } from 'jsonql-constants' +import { groupByNamespace } from 'jsonql-jwt' +import { JsonqlResolverNotFoundError } from 'jsonql-errors' + +import { MISSING_PROP_ERR } from './constants' +import getDebug from './get-debug' +const debug = getDebug('process-contract') + +/** + * Just make sure the object contain what we are looking for + * @param {object} opts configuration from checkOptions + * @return {object} the target content + */ +const getResolverList = contract => { + if (contract) { + const { socket } = contract; + if (socket) { + return socket; + } + } + throw new JsonqlResolverNotFoundError(MISSING_PROP_ERR) +} + +/** + * process the contract first + * @param {object} opts configuration + * @return {object} sorted list + */ +export default function processContract(opts) { + const { contract, enableAuth } = opts; + if (enableAuth) { + return groupByNamespace(contract) + } + return { + nspSet: { [JSONQL_PATH]: getResolverList(contract) }, + publicNamespace: JSONQL_PATH, + size: 1 // this prop is pretty meaningless now + } +} diff --git a/packages/ws-base/src/client/utils/trigger-namespaces-on-error.js b/packages/ws-base/src/client/utils/trigger-namespaces-on-error.js new file mode 100644 index 0000000000000000000000000000000000000000..59409c8162602f2f507413873e96707f65512cea --- /dev/null +++ b/packages/ws-base/src/client/utils/trigger-namespaces-on-error.js @@ -0,0 +1,15 @@ + +import { ERROR_PROP_NAME } from 'jsonql-constants' +import { createEvt } from './index' +/** + * trigger errors on all the namespace onError handler + * @param {object} ee Event Emitter + * @param {array} namespaces nsps string + * @param {string} message optional + * @return {void} + */ +export default function triggerNamespacesOnError(ee, namespaces, message) { + namespaces.forEach( namespace => { + ee.$call(createEvt(namespace, ERROR_PROP_NAME), [{ message, namespace }]) + }) +} diff --git a/packages/ws-base/src/post-install.js b/packages/ws-base/src/post-install.js new file mode 100644 index 0000000000000000000000000000000000000000..5618e78d80b6949c0da389ae19254afcdadd1015 --- /dev/null +++ b/packages/ws-base/src/post-install.js @@ -0,0 +1,2 @@ +// This will just a show little nice message after the installation +// Just to state the main documentation website and little advertise about our companies diff --git a/packages/ws-base/src/server/README.md b/packages/ws-base/src/server/README.md new file mode 100644 index 0000000000000000000000000000000000000000..b554c7a1f9377a2bb3a375123291f30647dc31cb --- /dev/null +++ b/packages/ws-base/src/server/README.md @@ -0,0 +1,81 @@ +# jsonql-ws-server + +> Setup WebSocket / Socket.io server for the jsonql to run on the same host, automatic generate public / private channel using contract + +This module will create socket connection with [jsonql-koa](https://www.npmjs.com/package/jsonql-koa) +under the same host. + +Current it supports + +- [socket.io](https://socket.io/) +- [ws](https://github.com/websockets/ws) + + +## Installation + +```sh +$ npm i jsonql-ws-server +``` + +## Example + +Due to the architect of Koa, this can not simply create as a middleware. Therefore this need to init after +the Koa app has been inited. Also this require the `http.createServer` to pass the server + +**To start with socket.io** + +```js +const jsonqlWsServer = require('jsonql-ws-server') +const Koa = require('koa') +const http = require('http') +// bunch of init +const server = http.createServer(app.callback()) + +const io = jsonqlWsServer({ + serverType: 'socket.io', + options: {}, + server +}) +``` +~~Once this init is done, you will get a `global.WEB_SOCKET_SERVER` with two properties inside:~~ + +```js + +// !!! THIS HAS CHANGED, await for updated version of README !!! + +{ + "serverType": "socket.io", + "server": io +} + +``` + +Now by default if you didn't pass the `namespace` in the options. It will always create one for you using the + +```js +const { JSONQL_PATH } = require('jsonql-constants') +``` + +Then you can use it like so + +```js +// THIS HAS CHANGED AWAITING for updated version of README + +const socket = io.server[JSONQL_PATH] + +socket.on('connection', function(ctx) { + // do your thing +}); + +``` + +But in reality you don't really use it like that, because [jsonql-ws-client](https://www.npmjs.com/package/jsonql-ws-client) will use that return and construct the +usable client for you, hence the return of the `serverType` key. + +--- + +MIT + +[Joel Chu](https://joelchu.com) + +[NEWBRAN LTD](https://newbran.ch) / [to1source CN](https://to1source.cn) (c) 2019 diff --git a/packages/ws-base/src/server/check-options/index.js b/packages/ws-base/src/server/check-options/index.js new file mode 100644 index 0000000000000000000000000000000000000000..0e7a3439ea96085e0f00ad42b90f6322779dfcf5 --- /dev/null +++ b/packages/ws-base/src/server/check-options/index.js @@ -0,0 +1,97 @@ +// there is very limited options there +const { join } = require('path') +const fsx = require('fs-extra') +const getContract = require('../share/get-contract') + +const { + createConfig, + checkConfigAsync, + isContract, + isNotEmpty, + isString +} = require('jsonql-params-validator') +const { + HSA_ALGO, + ENUM_KEY, + PUBLIC_KEY, + PRIVATE_KEY, + STRING_TYPE, + BOOLEAN_TYPE, + OBJECT_TYPE, + NUMBER_TYPE, + CHECKER_KEY, + PEM_EXT, + PUBLIC_KEY_NAME, + PRIVATE_KEY_NAME, + DEFAULT_CONTRACT_FILE_NAME +} = require('jsonql-constants') +const { + SOCKET_IO, + AVAILABLE_SERVERS, + SECRET_MISSING_ERR +} = require('../share/constants') + +const HANDSHAKE_TYPE = 'handshake'; +const ROUNDTRIP_TYPE = 'roundtrip'; + +const { JsonqlValidationError } = require('jsonql-errors') + +// base options +const defaultOptions = { + // @TODO this will be moving out shortly after the test done + // RS256 this will need to figure out how to distribute the key + algorithm: createConfig(HSA_ALGO, [STRING_TYPE]), + authTimeout: createConfig(15000, [NUMBER_TYPE]), + + serverType: createConfig(SOCKET_IO, [STRING_TYPE], {[ENUM_KEY]: AVAILABLE_SERVERS}), + // we require the contract already generated and pass here + contract: createConfig({}, [OBJECT_TYPE]), + enableAuth: createConfig(false, [BOOLEAN_TYPE]), + // this option now is only for passing the key + // this cause a bug because this option is always BOOLEAN and STRING TYPE! + useJwt: createConfig(false, [STRING_TYPE, BOOLEAN_TYPE]), // need to double check this + // we don't actually use this two + contractDir: createConfig('', [STRING_TYPE]), + resolverDir: createConfig('', [STRING_TYPE]), + // this is for construct the namespace + publicMethodDir: createConfig(PUBLIC_KEY, [STRING_TYPE]), + // just try this with string type first + privateMethodDir: createConfig(PRIVATE_KEY, [STRING_TYPE, BOOLEAN_TYPE]), + // we only want the keys directory then we read it back + keysDir: createConfig(false, [STRING_TYPE]), + socketIoAuthType: createConfig(false, [STRING_TYPE], { + [ENUM_KEY]: [HANDSHAKE_TYPE, ROUNDTRIP_TYPE] + }) +} + +const constProps = { + contract: false, + publicKey: false, + privateKey: false, + secret: false, + publicNamespace: PUBLIC_KEY, + privateNamespace: PRIVATE_KEY +} + +/** + * @param {object} config user supply + * @return {object} promise resolve the opts + */ +module.exports = function(config) { + return checkConfigAsync(config, defaultOptions, constProps) + .then(getContract) + // processing the key + .then(opts => { + if (opts.enableAuth === true) { + if (isString(opts.useJwt)) { + opts.secret = opts.useJwt; + } else if (opts.keysDir) { + opts.publicKey = fsx.readFileSync(join(opts.keysDir, [PUBLIC_KEY_NAME, PEM_EXT].join('.'))) + opts.privateKey = fsx.readFileSync(join(opts.keysDir, [PRIVATE_KEY_NAME, PEM_EXT].join('.'))) + } else { + throw new JsonqlValidationError(SECRET_MISSING_ERR) + } + } + return opts; + }) +} diff --git a/packages/ws-base/src/server/generator.js b/packages/ws-base/src/server/generator.js new file mode 100644 index 0000000000000000000000000000000000000000..5e204c0d665187e38799b85d94e0f0eefeedb96e --- /dev/null +++ b/packages/ws-base/src/server/generator.js @@ -0,0 +1,28 @@ +// this will take the contract json file +// search for the socket and use the enableAuth to determine how to +// attach each method into the nsp object +const { + SOCKET_IO, + WS, + SOCKET_NOT_DEFINE_ERR +} = require('./share/constants') +const { JsonqlError } = require('jsonql-errors') +const { socketIoSetup } = require('./socket-io') +const { wsSetup } = require('./ws') + +/** + * Here will add the methods found from contract add to the io object + * @param {object} opts configuration + * @param {object} nsp the ws server instance + * @return {void} nothing to return + */ +module.exports = function(opts, nsp) { + // the authentication run during the setup + switch (opts.serverType) { + case SOCKET_IO: + return socketIoSetup(opts, nsp) + case WS: + return wsSetup(opts, nsp) + } + throw new JsonqlError(SOCKET_NOT_DEFINE_ERR) +} diff --git a/packages/ws-base/src/server/index.js b/packages/ws-base/src/server/index.js new file mode 100644 index 0000000000000000000000000000000000000000..07ac860d8e5d3789e5b2abd231c7ed13df58a155 --- /dev/null +++ b/packages/ws-base/src/server/index.js @@ -0,0 +1,35 @@ +// re-export here +const { socketIoCreateServer } = require('./socket-io') +const { wsCreateServer } = require('./ws') +const generator = require('./generator') +const { + SOCKET_IO, + WS, + SERVER_NOT_SUPPORT_ERR +} = require('./share/constants') +const checkOptions = require('./check-options') +const { getDebug } = require('./share/helpers') +const debug = getDebug('lib-index') + +/** + * @param {string} name of the server + * @return {function} for constructing the server + */ +const getServer = name => { + debug('getServer', name) + switch (name) { + case WS: + return wsCreateServer; + case SOCKET_IO: + return socketIoCreateServer; + default: + throw new Error(`${name} ${SERVER_NOT_SUPPORT_ERR}`) + } +} + +// re-export +module.exports = { + getServer, + checkOptions, + generator +}; diff --git a/packages/ws-base/src/server/share/add-handler-property.js b/packages/ws-base/src/server/share/add-handler-property.js new file mode 100644 index 0000000000000000000000000000000000000000..44b11706cc46c2c6a45e5e60725a7d87cc769486 --- /dev/null +++ b/packages/ws-base/src/server/share/add-handler-property.js @@ -0,0 +1,25 @@ +// we should be using a generic method to add handler property +// at the moment the server only has a `send` method +// and there should be a onMessage method for them to received +// what the client send back instead of relying on them to +// construct listener using the raw socket object + +/** + * @param {function} fn the resolver + * @param {string} name the name of the property + * @param {function} setter for set + * @param {function} getter for get + * @param {object} [options={}] extra options for defineProperty + * @return {function} the resolver itself + */ +function addHandlerProperty(fn, name, setter, getter , options = {}) { + if (Object.getOwnPropertyDescriptor(fn, name) === undefined) { + Object.defineProperty(fn, name, Object.assign({ + set: setter, + get: getter + }, options)) + } + return fn; +} +// export +module.exports = addHandlerProperty; diff --git a/packages/ws-base/src/server/share/add-ws-property.js b/packages/ws-base/src/server/share/add-ws-property.js new file mode 100644 index 0000000000000000000000000000000000000000..81b911302b94eec3d737351d74c867003093ba6a --- /dev/null +++ b/packages/ws-base/src/server/share/add-ws-property.js @@ -0,0 +1,19 @@ + +/** + * this is a generic method that add the socket property + * to the resolver function for other use + * @param {function} fn the resolver + * @param {object} socket the socket.io socket or nsp + * @return {function} fn the resolver + new property from ctx + */ +const addWsProperty = (fn , socket) => { + if (Object.getOwnPropertyDescriptor(fn, 'socket') === undefined) { + Object.defineProperty(fn, 'socket', { + value: socket, + writeable: false + }) + } + return fn; +} +// export +module.exports = addWsProperty; diff --git a/packages/ws-base/src/server/share/constants.js b/packages/ws-base/src/server/share/constants.js new file mode 100644 index 0000000000000000000000000000000000000000..e86fac16309532acf94e4938584be11c7f0554f8 --- /dev/null +++ b/packages/ws-base/src/server/share/constants.js @@ -0,0 +1,26 @@ + +const SOCKET_IO = 'socket.io'; +const WS = 'ws'; + +const AVAILABLE_SERVERS = [SOCKET_IO, WS]; + +const SOCKET_NOT_DEFINE_ERR = 'socket is not define in the contract file!'; + +const SERVER_NOT_SUPPORT_ERR = 'is not supported server name!'; + +const SECRET_MISSING_ERR = 'Secret is required!'; + +const MODULE_NAME = 'jsonql-ws-server'; + +const CONTRACT_NOT_FOUND_ERR = `No contract presented!`; + +module.exports = { + SOCKET_IO, + WS, + AVAILABLE_SERVERS, + SOCKET_NOT_DEFINE_ERR, + SERVER_NOT_SUPPORT_ERR, + SECRET_MISSING_ERR, + MODULE_NAME, + CONTRACT_NOT_FOUND_ERR +}; diff --git a/packages/ws-base/src/server/share/get-contract.js b/packages/ws-base/src/server/share/get-contract.js new file mode 100644 index 0000000000000000000000000000000000000000..02c0b6b40e979947144e350741a602c36079cac2 --- /dev/null +++ b/packages/ws-base/src/server/share/get-contract.js @@ -0,0 +1,22 @@ +// if they didn't pass the contract then we need to grab it from the contractDir +const { join } = require('path') +const fsx = require('fs-extra') +const { DEFAULT_CONTRACT_FILE_NAME } = require('jsonql-constants') +const { isContract } = require('jsonql-params-validator') +const { JsonqlError } = require('jsonql-errors') +const { CONTRACT_NOT_FOUND_ERR } = require('./constants') +/** + * @param {object} config configuration + * @return {object} config with the contract + */ +module.exports = function(config) { + if (config.contract && isContract(config.contract)) { + return config; + } + let c = fsx.readJsonSync(join(config.contractDir, DEFAULT_CONTRACT_FILE_NAME)) + if (!isContract(c)) { + throw new JsonqlError(CONTRACT_NOT_FOUND_ERR ) + } + config.contract = c; + return config; +} diff --git a/packages/ws-base/src/server/share/helpers.js b/packages/ws-base/src/server/share/helpers.js new file mode 100644 index 0000000000000000000000000000000000000000..05d84083b8c3c69d33317e88fd21639fcbc78bbd --- /dev/null +++ b/packages/ws-base/src/server/share/helpers.js @@ -0,0 +1,125 @@ +// util methods +const { trim } = require('lodash') +const debug = require('debug') +// jsonql libraries +const { + JSONQL_PATH, + WS_REPLY_TYPE, + WS_EVT_NAME, + WS_DATA_NAME +} = require('jsonql-constants') +const { + isString, + isKeyInObject +} = require('jsonql-params-validator') +const { + JsonqlError, + clientErrorsHandler +} = require('jsonql-errors') + +const { MODULE_NAME } = require('./constants') + +// create debug +const getDebug = name => debug(MODULE_NAME).extend(name) + +const _debug = getDebug('helpers') + +// import { getDebug } from '../get-debug'; +const keys = [ WS_REPLY_TYPE, WS_EVT_NAME, WS_DATA_NAME ] +// const debug = getDebug('is-ws-reply'); +/** + * @param {string|object} payload should be string when reply but could be transformed + * @return {boolean} true is OK + */ +const isWsReply = payload => { + const json = isString(payload) ? JSON.parse(payload) : payload; + const { data } = json; + if (data) { + let result = keys.filter(key => isKeyInObject(data, key)) + return (result.length === keys.length) ? data : false; + } + return false; +} + +/** + * @param {string|object} data received data + * @return {object} false on failed + */ +const extractWsPayload = payload => { + const json = isString(payload) ? JSON.parse(payload) : payload; + // if there is error then this will throw + clientErrorsHandler(json); + // now handle the data + let _data; + if ((_data = isWsReply(json)) !== false) { + return { + data: _data[WS_DATA_NAME], + resolverName: _data[WS_EVT_NAME], + type: _data[WS_REPLY_TYPE] + } + } + throw new JsonqlError('payload can not decoded', payload) +} + +/** + * We are going to completely change this + * 1. there will only be max two namespace + * 2. when it's normal we will have the stock path as namespace + * 3. when enableAuth then we will have two one is jsonql/public + private + * @param {object} config options + * @return {array} of namespace(s) + */ +const getNamespace = function(config) { + const base = JSONQL_PATH; + if (config.enableAuth) { + // the public come first + return [ + [ base , config.publicNamespace].join('/'), + [ base , config.privateNamespace].join('/') + ] + } + return [ base ] +} + +/** + * From underscore.string library + * @BUG there is a bug here with the non-standard name + * @param {string} str string + * @return {string} dasherize string + */ +const dasherize = str => trim(str) + .replace(/([A-Z])/g, '-$1') + .replace(/[-_\s]+/g, '-') + .toLowerCase() + +/** + * create the error message + * @param {object} e error + * @param {boolean} s true toString + * @return {object} formatted reply + */ +const packError = (e, s = false) => { + const payload = { + error: { + className: e.className || 'JsonqlServerError', + message: e.message || 'NO MESSAGE', + detail: e.detail || e + } + } + return s ? JSON.stringify(payload) : payload; +} + +// just an empty method for addProperty getter +const nil = function() { + return false; +} + +// export +module.exports = { + getDebug, + getNamespace, + dasherize, + packError, + extractWsPayload, + nil +} diff --git a/packages/ws-base/src/server/share/resolve-method.js b/packages/ws-base/src/server/share/resolve-method.js new file mode 100644 index 0000000000000000000000000000000000000000..a6a1ad568045643d3aa95ca3f8d02424d622879f --- /dev/null +++ b/packages/ws-base/src/server/share/resolve-method.js @@ -0,0 +1,181 @@ +// search for the resolver location +const fs = require('fs') +const { join } = require('path') +const { isUndefined } = require('lodash') +const { + JsonqlAuthorisationError, + JsonqlResolverNotFoundError, + JsonqlResolverAppError, + JsonqlValidationError, + JsonqlError, + finalCatch +} = require('jsonql-errors') +const { + SOCKET_NAME, + DEFAULT_RESOLVER_IMPORT_FILE_NAME, + MODULE_TYPE +} = require('jsonql-constants') +const { validateSync } = require('jsonql-params-validator') +const { provideUserdata } = require('jsonql-jwt') +const { dasherize } = require('./helpers') +// the addProperty methods +const socketIoAddProperty = require('../socket-io/add-property') +const wsAddProperty = require('../ws/add-property') + +const { SOCKET_IO, WS } = require('./constants') + +const debug = require('debug')('jsonql-ws-server:resolve-method') +/** + * @param {string} name resolverName + * @param {string} type resolverType + * @param {object} opts configuration + * @return {function} resolver 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 { + let _fn; + switch(serverType) { + case SOCKET_IO: + _fn = socketIoAddProperty(fn, resolverName, ws) + break; + case WS: + _fn = wsAddProperty(fn, resolverName, ws) + break; + default: + throw new JsonqlError(`No such serverType: ${serverType} for ${resolverName}`) + } + // group the provide userdata together + return isUndefined(userdata) ? _fn : provideUserdata(_fn, userdata) +} + +/** + * similiar to the one in Koa-middleware without the ctx + * @param {string} resolverName name to call + * @param {array} args arguments + * @param {object} params extracted params + * @param {object} opts for search later + * @param {object} userdata userdata + * @param {object} ws io for socket.io + * @param {object|boolean} userdata false when there is none userdata + * @return {mixed} depends on the contract + */ +const resolveMethod = function(resolverName, args, params, opts, ws, userdata) { + const fn = getResolver(resolverName, params, opts) + const tfn = addProperty(opts.serverType, fn, resolverName, ws, userdata) + try { + return Reflect.apply(tfn, null, args) + } catch(e) { + throw new JsonqlResolverAppError(resolverName, e) + } +} + +// we only need to export one method +module.exports = resolveMethod diff --git a/packages/ws-base/src/server/socket-io/add-property.js b/packages/ws-base/src/server/socket-io/add-property.js new file mode 100644 index 0000000000000000000000000000000000000000..2f7bca2f7e0d76a41278f30598d2eeadf5560605 --- /dev/null +++ b/packages/ws-base/src/server/socket-io/add-property.js @@ -0,0 +1,24 @@ +// because different ws server has their own structure +// therefore different addProperty will have different implementation +const { SEND_MSG_PROP_NAME } = require('jsonql-constants') +const addHandlerProperty = require('../share/add-handler-property') +const { nil } = require('../share/helpers') +/** + * Add property to the function + * @TODO we could extend this idea further to Koa for the userdata + * + * @param {function} fn the resolver + * @param {object} ctx the koa-socket-2 context + * @param {string} resolverName the name of the resolver + * @return {function} fn the resolver + new property from ctx + */ +const addProperty = (fn , resolverName, nsp) => { + let resolver; + resolver = addHandlerProperty(fn, SEND_MSG_PROP_NAME, function(prop) { + // we wrap this here to make sure the format is correct + nsp.emit(resolverName, {data: prop}) + }, nil) + return resolver; +} + +module.exports = addProperty diff --git a/packages/ws-base/src/server/socket-io/index.js b/packages/ws-base/src/server/socket-io/index.js new file mode 100644 index 0000000000000000000000000000000000000000..28f7bb53825fb15329b69892b67182a7baeb4541 --- /dev/null +++ b/packages/ws-base/src/server/socket-io/index.js @@ -0,0 +1,8 @@ +const socketIoCreateServer = require('./socket-io-create-server'); +const socketIoSetup = require('./socket-io-setup'); + + +module.exports = { + socketIoCreateServer, + socketIoSetup +}; diff --git a/packages/ws-base/src/server/socket-io/socket-io-create-server.js b/packages/ws-base/src/server/socket-io/socket-io-create-server.js new file mode 100644 index 0000000000000000000000000000000000000000..bc0029b76516df476ae402ad629a9198210271a6 --- /dev/null +++ b/packages/ws-base/src/server/socket-io/socket-io-create-server.js @@ -0,0 +1,39 @@ +// socket.io server setup +const socketIo = require('socket.io') +const { socketIoHandshakeAuth } = require('jsonql-jwt') +const { RSA_ALGO } = require('jsonql-constants') + +const { getNamespace, getDebug } = require('../share/helpers') +const debug = getDebug('create-socket.io') + + +/** + * @TODO need to check how to support the com style verification + * @param {object} config options + * @param {object} server the http server + * @return {object} ws server instance with namespace as key + */ +module.exports = function(config, server) { + // init the io instance + const io = socketIo(server) + const namespaces = getNamespace(config) + let added = false; + // always use a namespace + return namespaces.map((namespace, i) => { + // this is different from WebSocket start with a slash + let nsp = io.of('/' + namespace) + if (config.enableAuth && config.publicKey && i > 0) { + // if they use the config.secret then + // we init the socketIoJwtAuth later + nsp = socketIoHandshakeAuth(nsp, { + timeout: config.authTimeout, + secret: config.publicKey, + algorithms: RSA_ALGO + }) + debug(`create nsp using socketIoHandshakeAuth`) + } + // return the key + return { [namespace]: nsp } + }) + .reduce((last, next) => Object.assign(last, next), {}); +} diff --git a/packages/ws-base/src/server/socket-io/socket-io-setup.js b/packages/ws-base/src/server/socket-io/socket-io-setup.js new file mode 100644 index 0000000000000000000000000000000000000000..4eb6cd70274b468036d07b1f906ae6e6a49c0ff8 --- /dev/null +++ b/packages/ws-base/src/server/socket-io/socket-io-setup.js @@ -0,0 +1,161 @@ +// socket.io setup event handler +const { finalCatch } = require('jsonql-errors') +const { isNotEmpty, validateAsync } = require('jsonql-params-validator') +const { LOGOUT_EVT_NAME, ACKNOWLEDGE_REPLY_TYPE } = require('jsonql-constants') +const { socketIoGetUserdata, groupByNamespace, socketIoJwtAuth } = require('jsonql-jwt') +const resolveMethod = require('../share/resolve-method') +const addWsProperty = require('../share/add-ws-property') +const { getDebug, packError } = require('../share/helpers') + +const debug = getDebug('socket-io-setup') + +/** + * @param {object} socket io socket connection instance + * @param {object} nsp the namespace instance + * @return {void} nothing + */ +const logoutHandler = (socket, nsp) => { + socket.on(LOGOUT_EVT_NAME, () => { + debug(LOGOUT_EVT_NAME) + // @TODO need to test this + nsp.disconnect() + }) +} + +/** + * We no longer use koa-socket-2 because it's hard to implement login + * @param {object} socket The nsp socket instance + * @param {object} data pass from client + * @param {string} fnName name of function as event name + * @param {object} params from contract + * @param {object} opts configuration + * @param {function} cb callback from socket.io + * @param {object} userdata could be undefined + * @return {void} noting + */ +const fnHandler = (socket, data, fnName, params, opts, cb, userdata = false) => { + return validateAsync(data.args, params.params, true) + .then(args => { + debug('call resolveMethod', fnName, args) + // @TODO figure out how to create rooms etc + return resolveMethod( + fnName, + args, + params, + opts, + socket, + userdata + ) + }) + .then(result => { + debug('result', result) + // decide if we need to call the cb or not here + if (isNotEmpty(result)) { + cb({data: result}) + } + }) + .catch(err => { + debug('catch error', err) + cb(packError(err)) + }) +} + +/** + * @param {object} nsp namespace instance + * @param {object} methodsInNsp map of contract + * @param {object} opts configuration + * @return {void} noting + */ +const socketIoAuthHandler = (nsp, methodsInNsp, opts) => { + // @TODO + let socketIoSetupConfig = {secret: opts.secret} + debug('using socketIoJwtAuth (roundtrip)', socketIoSetupConfig) + // setup auth + socketIoJwtAuth(nsp, socketIoSetupConfig) + .then(socket => { + let userdata = socketIoGetUserdata(socket) + debug('authenticated', userdata) + for (let fnName in methodsInNsp) { + let params = methodsInNsp[fnName] + // actual handler + socket.on(fnName, (data, cb) => { + let args = [socket, data, fnName, params, opts, cb, userdata] + Reflect.apply(fnHandler, null, args) + }) + } + logoutHandler(socket, nsp) + }) + .catch(err => { + console.error('jwt auth error', err) + }) +} + +/** + * Break out from socketIoSetup than add the auth layer + * @param {object} nsp the socket.io namespace instance + * @param {string} namespace what it said + * @param {object} methodsInNsp as event name + * @param {object} opts configuration + * @param {string|boolean} publicNamespace + * @return {void} nothing + */ +const nspHandler = (nsp, namespace, methodsInNsp, opts, publicNamespace = false) => { + let userdata; + if (opts.enableAuth && opts.secret && namespace !== publicNamespace) { + return socketIoAuthHandler(nsp, methodsInNsp, opts) + } + nsp.on('connection', socket => { + debug('public available nsp connected') + for (let fnName in methodsInNsp) { + let params = methodsInNsp[fnName] + // actual handler + socket.on(fnName, (data, cb) => { + debug('Handling event', fnName) + let args = [socket, data, fnName, params, opts, cb] + Reflect.apply(fnHandler, null, args) + }) + } + // special case + logoutHandler(socket, nsp) + }) +} + +/** + * Just to determine which map we should use + * @param {*} nspSet could be undefined if it's not enableAuth + * @param {string} namespace nsp + * @param {object} socketFns from contract.socket + * @return {object} map to the resolvers + */ +const getMethodsInNsp = (nspSet, namespace, socketFns) => ( + nspSet && nspSet[namespace] ? nspSet[namespace] : socketFns +) + +/** + * @param {object} opts configuration + * @param {object} nspObj key namespace value io + * @return {object} nspObj just return it + */ +const socketIoSetup = (opts, nspObj) => { + let nspInfo = {}; + const socketFns = opts.contract.socket; + if (opts.enableAuth) { + nspInfo = groupByNamespace(socketFns) + } + let { publicNamespace, nspSet } = nspInfo; + // because this is attach to the Koa.app itself + // its a bit different in the handling + for (let namespace in nspObj) { + let nsp = nspObj[namespace] + let methodsInNsp = getMethodsInNsp(nspSet, namespace, socketFns) + let args = [nsp, namespace, methodsInNsp, opts] + if (publicNamespace) { + args.push(publicNamespace) + } + Reflect.apply(nspHandler, null, args) + } + return nspObj; +}; + +// only need this one to export +module.exports = socketIoSetup; diff --git a/packages/ws-base/src/server/ws/add-property.js b/packages/ws-base/src/server/ws/add-property.js new file mode 100644 index 0000000000000000000000000000000000000000..99b39b3ba03d2b2227ce91161f3326c3aa9e7bbd --- /dev/null +++ b/packages/ws-base/src/server/ws/add-property.js @@ -0,0 +1,32 @@ + +// const debug = require('debug')('jsonql-ws-server:ws-add-property'); +const createWsReply = require('./create-ws-reply') +const { EMIT_REPLY_TYPE, SEND_MSG_PROP_NAME, MESSAGE_PROP_NAME } = require('jsonql-constants') +// const { JsonqlError } = require('jsonql-errors') +const addHandlerProperty = require('../share/add-handler-property') +const { nil } = require('../share/helpers') + +/** + * @param {function} fn the actual resolver function + * @param {string} resolverName name of resolver + * @param {object} ws the io instance + * @return {function} fn with additional property + */ +const addProperty = (fn, resolverName, ws) => { + let resolver; + resolver = addHandlerProperty(fn, SEND_MSG_PROP_NAME, function(prop) { + ws.send(createWsReply(EMIT_REPLY_TYPE, resolverName, prop)) + }, nil) + /* + @TODO is this necessary? + resolver = addHandlerProperty(resolver, MESSAGE_PROP_NAME, function(handler) { + if (handler && typeof handler === 'function') { + + } + throw new JsonqlError(resolverName, {message: `Require ${MESSAGE_PROP_NAME} to be a function!`}) + }, nil) + */ + return resolver; +} + +module.exports = addProperty diff --git a/packages/ws-base/src/server/ws/create-ws-reply.js b/packages/ws-base/src/server/ws/create-ws-reply.js new file mode 100644 index 0000000000000000000000000000000000000000..1b2b0ee43a82263bd88f4dbc7e51db529d746393 --- /dev/null +++ b/packages/ws-base/src/server/ws/create-ws-reply.js @@ -0,0 +1,28 @@ +// for some really really weird reason if I put this into the utils/helpers +// its unable to import into the module here +// since this is for ws only then we could just put in here instead +const { + WS_REPLY_TYPE, + WS_EVT_NAME, + WS_DATA_NAME +} = require('jsonql-constants') +// const debug = require('debug')('jsonql-ws-server:create-ws-reply'); +/** + * The ws doesn't have a acknowledge callback like socket.io + * so we have to DIY one for ws and other that doesn't have it + * @param {string} type of reply + * @param {string} resolverName which is replying + * @param {*} data payload + * @return {string} stringify json + */ +const createWsReply = (type, resolverName, data) => { + return JSON.stringify({ + data: { + [WS_REPLY_TYPE]: type, + [WS_EVT_NAME]: resolverName, + [WS_DATA_NAME]: data + } + }) +} + +module.exports = createWsReply diff --git a/packages/ws-base/src/server/ws/index.js b/packages/ws-base/src/server/ws/index.js new file mode 100644 index 0000000000000000000000000000000000000000..f001697cf99fdc6f202ca410d23cf3b386ee4cbe --- /dev/null +++ b/packages/ws-base/src/server/ws/index.js @@ -0,0 +1,7 @@ +const wsCreateServer = require('./ws-create-server') +const wsSetup = require('./ws-setup') + +module.exports = { + wsCreateServer, + wsSetup +} diff --git a/packages/ws-base/src/server/ws/ws-create-server.js b/packages/ws-base/src/server/ws/ws-create-server.js new file mode 100644 index 0000000000000000000000000000000000000000..e58a9c4d6f281709a22f12ae6b1ffb7d310212ae --- /dev/null +++ b/packages/ws-base/src/server/ws/ws-create-server.js @@ -0,0 +1,82 @@ +// web socket server based on ws +const url = require('url') +const WebSocket = require('ws') +const { wsVerifyClient } = require('jsonql-jwt') + +const { getNamespace, getDebug } = require('../share/helpers') +const debug = getDebug('ws-setup') + + +/** + * @param {array} namespace string + * @param {object} config configuration + * @return {array} of nsps + */ +const generateWss = (namespace, config) => { + let verifyClient; + if (config.enableAuth) { + let key = config.secret ? config.secret : config.publicKey; + verifyClient = wsVerifyClient(key) + } + return namespace.map((name, i) => { + let c = { noServer: true }; + if (i>0) { + c.verifyClient = verifyClient; + } + return { [name]: new WebSocket.Server(c) } + }).reduce((last, next) => Object.assign(last, next), {}) +} + +/** + * @param {object} req http.server request object + * @return {string} the strip slash of pathname + */ +const getPath = req => { + const { pathname } = url.parse(req.url) + // debug('pathname', pathname, pathname.substring(0, 1), pathname.substring(1, pathname.length)); + return pathname.substring(0, 1) === '/' ? pathname.substring(1, pathname.length) : pathname; +} + +/** + * @param {array} nsps with name as key + * @param {string} path of path name to compare + * @return {object} ws + */ +const getWssByPath = (nsps, path) => { + for (let name in nsps) { + if (nsps[path]) { + return nsps[path] + } + } + return false; +} + +/** + * @param {object} config options + * @param {object} server http.createServer instance + * @return {object} ws server instance with namespace as key + */ +module.exports = function(config, server) { + // debug('got config here', config); + const namespace = getNamespace(config) + // debug('namespace', namespace) + const nsps = generateWss(namespace, config) + // debug('nsps', nsps) + // debug('nsps', nsps); + // we will always call it via a namespace + server.on('upgrade', function upgrade(req, socket, head) { + const pathname = getPath(req) + debug('Hear the upgrade event', pathname) + let srv = false; + if ((srv = getWssByPath(nsps, pathname)) !== false) { + srv.handleUpgrade(req, socket, head, function done(ws) { + debug('found a srv to handle the call') + srv.emit('connection', ws, req) + }); + } else { + debug('Unhandled socket destroy') + socket.destroy() + } + }); + return nsps; +} diff --git a/packages/ws-base/src/server/ws/ws-setup.js b/packages/ws-base/src/server/ws/ws-setup.js new file mode 100644 index 0000000000000000000000000000000000000000..11f5dac1b50c46800e584aa1a38eab359b167fd2 --- /dev/null +++ b/packages/ws-base/src/server/ws/ws-setup.js @@ -0,0 +1,132 @@ +// ws setup +const { finalCatch } = require('jsonql-errors') +const { wsGetUserdata, groupByNamespace } = require('jsonql-jwt') +const { isNotEmpty, validateAsync } = require('jsonql-params-validator') +const { + LOGOUT_EVT_NAME, + ACKNOWLEDGE_REPLY_TYPE, + ERROR_TYPE, + LOGOUT_NAME +} = require('jsonql-constants') +const resolveMethod = require('../share/resolve-method') +const { getDebug, packError } = require('../share/helpers') +const createWsReply = require('./create-ws-reply') + +const debug = getDebug('ws-setup') + +/** + * @param {object} ws WebSocket instance + * @param {object} payload args array + * @param {string} resolverName name of resolver + * @param {object} params from contract.json + * @param {object} opts configuration + * @param {object} userdata userdata + */ +const fnHandler = (ws, payload, resolverName, params, opts, userdata) => { + debug('fnHandler', resolverName) + // need to figure out a way to create a cb using the ws + // perhaps a ws.socket.id or some kind? + const cb = data => { + ws.send(data) + } + // run + return validateAsync(payload.args, params.params, true) + .then( args => { + return resolveMethod( + resolverName, + args, // this is the clear value from validateAsync + params, + opts, + ws, + userdata + ) + }) + .then(result => { + debug('result', result) + // decide if we need to call the cb or not here + if (isNotEmpty(result)) { + cb(createWsReply(ACKNOWLEDGE_REPLY_TYPE, resolverName, result)) + } + }) + .catch(err => { + debug('catch error', err) + cb(createWsReply(ERROR_TYPE, resolverName, packError(err))) + }) +} + +/** + * The default single nsp mapping to resolver + * @param {object} ws websocket socket instance + * @param {object} json data send from client + * @param {object} socketFns contract + * @param {object} opts configuration + * @param {*} [userdata=false] userdata if any + * @return {void} nothing + */ +const callNspHandler = (ws, json, socketFns, opts, userdata = false) => { + for (let resolverName in json) { + debug('connection call', resolverName) + let payload = json[resolverName] + let params = socketFns[resolverName] + // we need to use the decoded token --> userdata + // and pass to the resolver + fnHandler(ws, payload, resolverName, params, opts, userdata) + } +} + +/** + * This will change based on the WS spec + * @param {object} ws socket + */ +const handleLogout = ws => { + ws.close(1, LOGOUT_NAME) +} + +/** + * @param {object} opts configuration + * @param {object} nspObj the ws nsp instance + * @return {object} nspObj itself with addtional properties + */ +const wsSetup = (opts, nspObj) => { + let nspInfo = {}; + const socketFns = opts.contract.socket; + if (opts.enableAuth) { + nspInfo = groupByNamespace(socketFns) + } + let { publicNamespace, nspSet } = nspInfo; + for (let namespace in nspObj) { + let userdata; + nspObj[namespace].on('connection', (ws, req) => { + // the data part is different again + // because there are only two events type in ws + // send --> message + // so the data is wrap inside the resolverName + ws.on('message', data => { + let json = JSON.parse(data) + debug('called with: ', json) + if (nspSet) { + let methodsInNsp = nspSet[namespace] + debug(namespace, methodsInNsp) + for (let resolverName in json) { + if (resolverName === LOGOUT_EVT_NAME) { + return handleLogout(ws) + } + // only allow the method under that particular namespace + if (methodsInNsp[resolverName]) { + // only the private one will get a userdata property + if (namespace !== publicNamespace) { + userdata = wsGetUserdata(req) + } + callNspHandler(ws, json, socketFns, opts, userdata) + } + } + } else { // just public nsp + callNspHandler(ws, json, socketFns, opts) + } + }) + }) + } + return nspObj; +} + +module.exports = wsSetup diff --git a/packages/ws-base/tests/client-test-node.test.js b/packages/ws-base/tests/client-test-node.test.js new file mode 100644 index 0000000000000000000000000000000000000000..af83dde7f3e942ea4f36d31fa8187fd21326f00d --- /dev/null +++ b/packages/ws-base/tests/client-test-node.test.js @@ -0,0 +1,204 @@ +/// breaking things apart and try to figure out what went wrong at the last step +const test = require('ava') +const debug = require('debug')('jsonql-ws-client:test:node') +/// SERVER SETUP /// +const { join } = require('path') +const fsx = require('fs-extra') + +const serverSetup = require('./fixtures/server-setup') +const genToken = require('./fixtures/token') + +const contractDir = join(__dirname, 'fixtures', 'contract', 'auth') +const contract = fsx.readJsonSync(join(contractDir, 'contract.json')) +const publicContract = fsx.readJsonSync(join(contractDir, 'public-contract.json')) +const { NOT_LOGIN_ERR_MSG, JS_WS_SOCKET_IO_NAME, JS_WS_NAME } = require('jsonql-constants') +const payload = {name: 'Joel'}; +const token = genToken(payload) +const port = 8010; +const url = `ws://localhost:${port}` +//////////////////// +const { + chainCreateNsps, + clientGenerator, + es +} = require('./fixtures/node') + +/// PREPARE TEST /// +test.before(async t => { + const { io, app } = await serverSetup({ + contract, + contractDir, + resolverDir: join(__dirname, 'fixtures', 'resolvers'), + serverType: JS_WS_SOCKET_IO_NAME, + enableAuth: true, + useJwt: true, + keysDir: join(__dirname, 'fixtures', 'keys') + }) + + t.context.server = app.listen(port) + + let config = { opts: { serverType: JS_WS_SOCKET_IO_NAME }, nspMap: {}, ee: es }; + let { opts, ee } = clientGenerator(config) + t.context.opts = opts; + t.context.ee = ee; +}) + +// real test start here +test.serial.cb('It should able to replace the same event with new method', t => { + + t.plan(3) + // try a sequence with swapping out the event handler + let ee = t.context.ee; + let evtName = 'main-event'; + let fnName = 'testFn'; + + ee.$on(evtName, (from, value) => { + debug(evtName, from, value) + // (1) + t.is(from, fnName) + return ++value; + }) + // first trigger it + ee.$call(evtName, [fnName, 1]) + // (2) + t.is(ee.$done, 2) + // now replace this event with another callback + ee.$replace(evtName, (from, value) => { + debug(evtName, from, value) + // (3) + t.is(value, 3) + t.end() + return --value; + }) + ee.$call(evtName, [fnName, 3]) +}) + +test.serial.cb.only('It should able to resolve the promise one after the other', t => { + t.plan(1) + let opts = t.context.opts; + let p1 = () => opts.nspAuthClient([url, 'jsonql/private'].join('/'), token) + let p2 = () => opts.nspClient([url, 'jsonql/public'].join('/')) + /* + let p1 = () => new Promise(resolver => { + setTimeout(() => { + resolver('first') + }, 1000) + }) + let p2 = () => new Promise(resolver => { resolver('second') }) + */ + chainCreateNsps([ + p1(), p2() + ]).then(results => { + debug(results) + t.pass() + t.end() + }) +}) + +test.serial.cb('Just test with the ws client can connect to the server normally', t => { + t.plan(2) + let opts = t.context.opts; + + // t.truthy(opts.nspClient) + // t.truthy(opts.nspAuthClient) + + opts.nspAuthClient([url, 'jsonql/private'].join('/'), token) + .then(socket => { + debug('io1 pass') + t.pass() + opts.nspClient([url, 'jsonql/public'].join('/')) + .then( socket => { + debug('io2 pass') + t.pass() + t.end() + }) + }) + + /* + this was for ws + ws1.onopen = function() { + t.pass() + debug('ws1 connected') + } + ws2.onopen = function() { + t.pass() + debug('ws2 connected') + ws1.terminate() + ws2.terminate() + t.end() + } + */ +}) + + +test.serial.cb('It should able to use the chainCreateNsps to run connection in sequence', t => { + t.plan(1) + let opts = t.context.opts; + + // t.truthy(opts.nspClientAsync) + // t.truthy(opts.nspAuthClientAsync) + + opts.nspAuthClientAsync([url, 'jsonql/private'].join('/'), token) + .then(ws => { + t.pass() + t.end() + }) + + // the bug is the chainCreateNsps somehow when we put it in there + // the connection just hang + + /* + chainCreateNsps([ + opts.nspAuthClientAsync([url, 'jsonql/private'].join('/'), token), + // opts.nspClientAsync([url, 'jsonql/public'].join('/')) + ]).then(nsps => { + t.is(nsps.length, 2) + nsps.forEach(nsp => { + nsp.terminate() + }) + t.end() + }) + */ +}) + +test.serial.cb.skip('It should able to wrap the connect call with the onopen callback', t => { + t.plan(3) + let opts = t.context.opts; + let ee = t.context.ee; + let publicConnect = () => { + return opts.nspClientAsync([url, 'jsonql/public'].join('/')) + .then(ws => { + ws.onopen = function() { + ee.$trigger('onReady1', ws) + } + return 'jsonql/public' + }) + } + + let privateConnect = () => { + return opts.nspAuthClientAsync([url, 'jsonql/private'].join('/'), token) + .then(ws => { + ws.onopen = function() { + ee.$trigger('onReady2', ws) + } + return 'jsonql/private' + }) + } + + chainCreateNsps([ + privateConnect(), + publicConnect() + ]).then(namespaces => { + t.is(namespaces.length, 2) + }) + + ee.$on('onReady1', function() { + t.pass() + t.end() + }) + + ee.$on('onReady2', function() { + t.pass() + }) + +}) diff --git a/packages/ws-base/tests/fixtures/client/auth/contract.json b/packages/ws-base/tests/fixtures/client/auth/contract.json new file mode 100644 index 0000000000000000000000000000000000000000..72510498a1edea8f1eb2f2f5ea6a9970c66d8ba6 --- /dev/null +++ b/packages/ws-base/tests/fixtures/client/auth/contract.json @@ -0,0 +1,111 @@ +{ + "query": {}, + "mutation": {}, + "auth": {}, + "timestamp": 1560348254, + "socket": { + "continuous": { + "namespace": "jsonql/private", + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-client/tests/fixtures/resolvers/socket/continuous.js", + "description": false, + "params": [ + { + "type": [ + "string" + ], + "name": "msg", + "description": "a message" + } + ], + "returns": [ + { + "type": [ + "string" + ], + "description": "a message with timestamp" + } + ] + }, + "pinging": { + "namespace": "jsonql/public", + "public": true, + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-client/tests/fixtures/resolvers/socket/public/pinging.js", + "description": false, + "params": [ + { + "type": [ + "string" + ], + "name": "msg", + "description": "message" + } + ], + "returns": [ + { + "type": [ + "string" + ], + "description": "reply message based on your message" + } + ] + }, + "sendExtraMsg": { + "namespace": "jsonql/private", + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-client/tests/fixtures/resolvers/socket/send-extra-msg.js", + "description": false, + "params": [ + { + "type": [ + "number" + ], + "name": "x", + "description": "a number for process" + } + ], + "returns": [ + { + "type": [ + "number" + ], + "description": "x + ?" + } + ] + }, + "simple": { + "namespace": "jsonql/private", + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-client/tests/fixtures/resolvers/socket/simple.js", + "description": false, + "params": [ + { + "type": [ + "number" + ], + "name": "i", + "description": "a number" + } + ], + "returns": [ + { + "type": [ + "number" + ], + "description": "a number + 1;" + } + ] + }, + "throwError": { + "namespace": "jsonql/private", + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-client/tests/fixtures/resolvers/socket/throw-error.js", + "description": "Testing the throw error", + "params": [], + "returns": [ + { + "type": [ + "any" + ], + "description": "just throw" + } + ] + } + } +} diff --git a/packages/ws-base/tests/fixtures/client/auth/public-contract.json b/packages/ws-base/tests/fixtures/client/auth/public-contract.json new file mode 100644 index 0000000000000000000000000000000000000000..62d35ef6f9fff4c98ffe37e6d8299614c4f6a797 --- /dev/null +++ b/packages/ws-base/tests/fixtures/client/auth/public-contract.json @@ -0,0 +1,117 @@ +{ + "query": { + "helloWorld": { + "description": "This is the stock resolver for testing purpose", + "params": [], + "returns": [ + { + "type": "string", + "description": "stock message" + } + ] + } + }, + "mutation": {}, + "auth": {}, + "timestamp": 1560348254, + "socket": { + "continuous": { + "namespace": "jsonql/private", + "description": false, + "params": [ + { + "type": [ + "string" + ], + "name": "msg", + "description": "a message" + } + ], + "returns": [ + { + "type": [ + "string" + ], + "description": "a message with timestamp" + } + ] + }, + "pinging": { + "namespace": "jsonql/public", + "public": true, + "description": false, + "params": [ + { + "type": [ + "string" + ], + "name": "msg", + "description": "message" + } + ], + "returns": [ + { + "type": [ + "string" + ], + "description": "reply message based on your message" + } + ] + }, + "sendExtraMsg": { + "namespace": "jsonql/private", + "description": false, + "params": [ + { + "type": [ + "number" + ], + "name": "x", + "description": "a number for process" + } + ], + "returns": [ + { + "type": [ + "number" + ], + "description": "x + ?" + } + ] + }, + "simple": { + "namespace": "jsonql/private", + "description": false, + "params": [ + { + "type": [ + "number" + ], + "name": "i", + "description": "a number" + } + ], + "returns": [ + { + "type": [ + "number" + ], + "description": "a number + 1;" + } + ] + }, + "throwError": { + "namespace": "jsonql/private", + "description": "Testing the throw error", + "params": [], + "returns": [ + { + "type": [ + "any" + ], + "description": "just throw" + } + ] + } + } +} diff --git a/packages/ws-base/tests/fixtures/client/contract-config-auth.js b/packages/ws-base/tests/fixtures/client/contract-config-auth.js new file mode 100644 index 0000000000000000000000000000000000000000..25fd9e0d39c1b5e34e1c62f590488e1f05c83421 --- /dev/null +++ b/packages/ws-base/tests/fixtures/client/contract-config-auth.js @@ -0,0 +1,10 @@ + +const { join } = require('path'); + +module.exports = { + contractDir: join(__dirname, 'contract', 'auth'), + resolverDir: join(__dirname, 'resolvers'), + public: true, + enableAuth: true, + useJwt: true +} diff --git a/packages/ws-base/tests/fixtures/client/contract-config.js b/packages/ws-base/tests/fixtures/client/contract-config.js new file mode 100644 index 0000000000000000000000000000000000000000..2187de94e54e2a3a8dc0fa0b7d8e115c83644e8d --- /dev/null +++ b/packages/ws-base/tests/fixtures/client/contract-config.js @@ -0,0 +1,9 @@ + +const { join } = require('path'); + +module.exports = { + contractDir: join(__dirname, 'contract', 'auth'), + resolverDir: join(__dirname, 'resolvers'), + enableAuth: true, + useJwt: true +}; diff --git a/packages/ws-base/tests/fixtures/client/contract.json b/packages/ws-base/tests/fixtures/client/contract.json new file mode 100644 index 0000000000000000000000000000000000000000..3728746dba80315150bf4d5c1b555326f0e25c17 --- /dev/null +++ b/packages/ws-base/tests/fixtures/client/contract.json @@ -0,0 +1,84 @@ +{ + "query": {}, + "mutation": {}, + "auth": {}, + "timestamp": 1560347818, + "socket": { + "continuous": { + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-client/tests/fixtures/resolvers/socket/continuous.js", + "description": false, + "params": [ + { + "type": [ + "string" + ], + "name": "msg", + "description": "a message" + } + ], + "returns": [ + { + "type": [ + "string" + ], + "description": "a message with timestamp" + } + ] + }, + "sendExtraMsg": { + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-client/tests/fixtures/resolvers/socket/send-extra-msg.js", + "description": false, + "params": [ + { + "type": [ + "number" + ], + "name": "x", + "description": "a number for process" + } + ], + "returns": [ + { + "type": [ + "number" + ], + "description": "x + ?" + } + ] + }, + "simple": { + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-client/tests/fixtures/resolvers/socket/simple.js", + "description": false, + "params": [ + { + "type": [ + "number" + ], + "name": "i", + "description": "a number" + } + ], + "returns": [ + { + "type": [ + "number" + ], + "description": "a number + 1;" + } + ] + }, + "throwError": { + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-client/tests/fixtures/resolvers/socket/throw-error.js", + "description": "Testing the throw error", + "params": [], + "returns": [ + { + "type": [ + "any" + ], + "description": "just throw" + } + ] + } + } +} diff --git a/packages/ws-base/tests/fixtures/client/contract/auth/contract.json b/packages/ws-base/tests/fixtures/client/contract/auth/contract.json new file mode 100644 index 0000000000000000000000000000000000000000..72510498a1edea8f1eb2f2f5ea6a9970c66d8ba6 --- /dev/null +++ b/packages/ws-base/tests/fixtures/client/contract/auth/contract.json @@ -0,0 +1,111 @@ +{ + "query": {}, + "mutation": {}, + "auth": {}, + "timestamp": 1560348254, + "socket": { + "continuous": { + "namespace": "jsonql/private", + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-client/tests/fixtures/resolvers/socket/continuous.js", + "description": false, + "params": [ + { + "type": [ + "string" + ], + "name": "msg", + "description": "a message" + } + ], + "returns": [ + { + "type": [ + "string" + ], + "description": "a message with timestamp" + } + ] + }, + "pinging": { + "namespace": "jsonql/public", + "public": true, + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-client/tests/fixtures/resolvers/socket/public/pinging.js", + "description": false, + "params": [ + { + "type": [ + "string" + ], + "name": "msg", + "description": "message" + } + ], + "returns": [ + { + "type": [ + "string" + ], + "description": "reply message based on your message" + } + ] + }, + "sendExtraMsg": { + "namespace": "jsonql/private", + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-client/tests/fixtures/resolvers/socket/send-extra-msg.js", + "description": false, + "params": [ + { + "type": [ + "number" + ], + "name": "x", + "description": "a number for process" + } + ], + "returns": [ + { + "type": [ + "number" + ], + "description": "x + ?" + } + ] + }, + "simple": { + "namespace": "jsonql/private", + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-client/tests/fixtures/resolvers/socket/simple.js", + "description": false, + "params": [ + { + "type": [ + "number" + ], + "name": "i", + "description": "a number" + } + ], + "returns": [ + { + "type": [ + "number" + ], + "description": "a number + 1;" + } + ] + }, + "throwError": { + "namespace": "jsonql/private", + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-client/tests/fixtures/resolvers/socket/throw-error.js", + "description": "Testing the throw error", + "params": [], + "returns": [ + { + "type": [ + "any" + ], + "description": "just throw" + } + ] + } + } +} diff --git a/packages/ws-base/tests/fixtures/client/contract/auth/public-contract.json b/packages/ws-base/tests/fixtures/client/contract/auth/public-contract.json new file mode 100644 index 0000000000000000000000000000000000000000..62d35ef6f9fff4c98ffe37e6d8299614c4f6a797 --- /dev/null +++ b/packages/ws-base/tests/fixtures/client/contract/auth/public-contract.json @@ -0,0 +1,117 @@ +{ + "query": { + "helloWorld": { + "description": "This is the stock resolver for testing purpose", + "params": [], + "returns": [ + { + "type": "string", + "description": "stock message" + } + ] + } + }, + "mutation": {}, + "auth": {}, + "timestamp": 1560348254, + "socket": { + "continuous": { + "namespace": "jsonql/private", + "description": false, + "params": [ + { + "type": [ + "string" + ], + "name": "msg", + "description": "a message" + } + ], + "returns": [ + { + "type": [ + "string" + ], + "description": "a message with timestamp" + } + ] + }, + "pinging": { + "namespace": "jsonql/public", + "public": true, + "description": false, + "params": [ + { + "type": [ + "string" + ], + "name": "msg", + "description": "message" + } + ], + "returns": [ + { + "type": [ + "string" + ], + "description": "reply message based on your message" + } + ] + }, + "sendExtraMsg": { + "namespace": "jsonql/private", + "description": false, + "params": [ + { + "type": [ + "number" + ], + "name": "x", + "description": "a number for process" + } + ], + "returns": [ + { + "type": [ + "number" + ], + "description": "x + ?" + } + ] + }, + "simple": { + "namespace": "jsonql/private", + "description": false, + "params": [ + { + "type": [ + "number" + ], + "name": "i", + "description": "a number" + } + ], + "returns": [ + { + "type": [ + "number" + ], + "description": "a number + 1;" + } + ] + }, + "throwError": { + "namespace": "jsonql/private", + "description": "Testing the throw error", + "params": [], + "returns": [ + { + "type": [ + "any" + ], + "description": "just throw" + } + ] + } + } +} diff --git a/packages/ws-base/tests/fixtures/client/contract/contract.json b/packages/ws-base/tests/fixtures/client/contract/contract.json new file mode 100644 index 0000000000000000000000000000000000000000..3728746dba80315150bf4d5c1b555326f0e25c17 --- /dev/null +++ b/packages/ws-base/tests/fixtures/client/contract/contract.json @@ -0,0 +1,84 @@ +{ + "query": {}, + "mutation": {}, + "auth": {}, + "timestamp": 1560347818, + "socket": { + "continuous": { + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-client/tests/fixtures/resolvers/socket/continuous.js", + "description": false, + "params": [ + { + "type": [ + "string" + ], + "name": "msg", + "description": "a message" + } + ], + "returns": [ + { + "type": [ + "string" + ], + "description": "a message with timestamp" + } + ] + }, + "sendExtraMsg": { + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-client/tests/fixtures/resolvers/socket/send-extra-msg.js", + "description": false, + "params": [ + { + "type": [ + "number" + ], + "name": "x", + "description": "a number for process" + } + ], + "returns": [ + { + "type": [ + "number" + ], + "description": "x + ?" + } + ] + }, + "simple": { + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-client/tests/fixtures/resolvers/socket/simple.js", + "description": false, + "params": [ + { + "type": [ + "number" + ], + "name": "i", + "description": "a number" + } + ], + "returns": [ + { + "type": [ + "number" + ], + "description": "a number + 1;" + } + ] + }, + "throwError": { + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-client/tests/fixtures/resolvers/socket/throw-error.js", + "description": "Testing the throw error", + "params": [], + "returns": [ + { + "type": [ + "any" + ], + "description": "just throw" + } + ] + } + } +} diff --git a/packages/ws-base/tests/fixtures/client/contract/public-contract.json b/packages/ws-base/tests/fixtures/client/contract/public-contract.json new file mode 100644 index 0000000000000000000000000000000000000000..43cfd2b37fce72eec46df65703bb5e054e015c0f --- /dev/null +++ b/packages/ws-base/tests/fixtures/client/contract/public-contract.json @@ -0,0 +1,91 @@ +{ + "query": { + "helloWorld": { + "description": "This is the stock resolver for testing purpose", + "params": [], + "returns": [ + { + "type": "string", + "description": "stock message" + } + ] + } + }, + "mutation": {}, + "auth": {}, + "timestamp": 1560347818, + "socket": { + "continuous": { + "description": false, + "params": [ + { + "type": [ + "string" + ], + "name": "msg", + "description": "a message" + } + ], + "returns": [ + { + "type": [ + "string" + ], + "description": "a message with timestamp" + } + ] + }, + "sendExtraMsg": { + "description": false, + "params": [ + { + "type": [ + "number" + ], + "name": "x", + "description": "a number for process" + } + ], + "returns": [ + { + "type": [ + "number" + ], + "description": "x + ?" + } + ] + }, + "simple": { + "description": false, + "params": [ + { + "type": [ + "number" + ], + "name": "i", + "description": "a number" + } + ], + "returns": [ + { + "type": [ + "number" + ], + "description": "a number + 1;" + } + ] + }, + "throwError": { + "description": "Testing the throw error", + "params": [], + "returns": [ + { + "type": [ + "any" + ], + "description": "just throw" + } + ] + } + } +} diff --git a/packages/ws-base/tests/fixtures/client/io-setup.js b/packages/ws-base/tests/fixtures/client/io-setup.js new file mode 100644 index 0000000000000000000000000000000000000000..65f37ec3a20f81671372c3ef0d88b6abdc857ec0 --- /dev/null +++ b/packages/ws-base/tests/fixtures/client/io-setup.js @@ -0,0 +1,23 @@ +// const Koa = require('koa'); +const bodyparser = require('koa-bodyparser'); +const jsonqlWsServer = require('jsonql-ws-server'); +const config = require('./contract-config'); +const { join } = require('path'); +const fsx = require('fs-extra'); +const contract = fsx.readJsonSync(join(config.contractDir, 'contract.json')); +const debug = require('debug')('jsonql-ws-client:fixtures:io-setup'); +const baseOptions = { + serverType: 'socket.io', + contract +}; + +module.exports = function(app, _config = {}) { + const opts = Object.assign(baseOptions, config, _config); + return new Promise(resolver => { + jsonqlWsServer(opts, app) + .then(io => { + debug('setup completed'); + resolver({ app, io }); + }); + }); +} diff --git a/packages/ws-base/tests/fixtures/client/keys/privateKey.pem b/packages/ws-base/tests/fixtures/client/keys/privateKey.pem new file mode 100644 index 0000000000000000000000000000000000000000..52ceae92066efead75838933d8bca25eeb9666ac --- /dev/null +++ b/packages/ws-base/tests/fixtures/client/keys/privateKey.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQDfDqpnh8TceIRuemm8GWM6nvE6KumK/Lq+POrZqghgHpZa5zjv +wwjsJ2iK45zWIRpggMkSlQZWvnRRjj/TWfv7448qhhiTB7hmqV63XjfYXJ5OgTtN +fPW36ZQ48Ha0y4sjlU4gvSijHpnzrJ5yV/vjLLLp9WxTux4ColeZu2B/XQIDAQAB +AoGAEmLJFQOR7IJamCiq8oA9N6XGSH8lBPnUAr5OtWZYjmO3DQMmJE01PRH6gghE +8zmDTRUQfeGexiOovtg01p0CMhehwS8D8d0m01s43zQ77xVJuFAvuW1U1kER4Xze +tVkLEvvO9PcWpKUEmxYpDoCJXGIfXuHaSAVbVLYDKn2MEUECQQD4FeWlpkxSNQT9 +u6w01zR/byjXzUmibOP5zrpaEsDGIxxTlxc/7WJZlKLNybXUyZE8oHgepuefdcL0 +ybk6gvQpAkEA5ixdMtnsbUImJYNFrt5BbLzEU9eF76hovsOSjOc2eTUJHEeiXeDA +Q66WZwXNBf/CRrZdsAvBPMQcWzJLwp24FQJBAPaojtPMLEXwAS5l0ioXblLlqq4l +pfigW2qcaBv2WUSm1BsoNi2RUB/Q8K26x9bxMj4dLlELkW+yHkxT5J6QZUECQFRO +A4TQlOwfwmETB77Y4RW2viIHWqNBB7x3XYIGXclfR4r4IdxIqaMgmy34zfNYjgvg +V8hXRdu/6LLuZRlPM1ECQHUppZNG7WKP9F7ywAr33u3xD0+9MqsfqcgKPfP9VOxs +Lo8EdmjmB30lyyP/Cd1hzxb+BsJjGmxzU/DikGbWos8= +-----END RSA PRIVATE KEY----- diff --git a/packages/ws-base/tests/fixtures/client/keys/publicKey.pem b/packages/ws-base/tests/fixtures/client/keys/publicKey.pem new file mode 100644 index 0000000000000000000000000000000000000000..7bd2532afbeb5b3be8b9c9f275ffac7b32456d0d --- /dev/null +++ b/packages/ws-base/tests/fixtures/client/keys/publicKey.pem @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDfDqpnh8TceIRuemm8GWM6nvE6 +KumK/Lq+POrZqghgHpZa5zjvwwjsJ2iK45zWIRpggMkSlQZWvnRRjj/TWfv7448q +hhiTB7hmqV63XjfYXJ5OgTtNfPW36ZQ48Ha0y4sjlU4gvSijHpnzrJ5yV/vjLLLp +9WxTux4ColeZu2B/XQIDAQAB +-----END PUBLIC KEY----- diff --git a/packages/ws-base/tests/fixtures/client/node/index.js b/packages/ws-base/tests/fixtures/client/node/index.js new file mode 100644 index 0000000000000000000000000000000000000000..3d6f0446b8b240a89dc38c981c0744c4f40509ac --- /dev/null +++ b/packages/ws-base/tests/fixtures/client/node/index.js @@ -0,0 +1,8 @@ +const clientGenerator = require('../../../src/node/client-generator') +const { chainCreateNsps, es } = require('./test.cjs') + +module.exports = { + clientGenerator, + chainCreateNsps, + es +} diff --git a/packages/ws-base/tests/fixtures/client/node/test.cjs.js b/packages/ws-base/tests/fixtures/client/node/test.cjs.js new file mode 100644 index 0000000000000000000000000000000000000000..0a8cb3e84de1444f5962e1a9d6930cee7a8466a8 --- /dev/null +++ b/packages/ws-base/tests/fixtures/client/node/test.cjs.js @@ -0,0 +1,720 @@ +'use strict'; + +Object.defineProperty(exports, '__esModule', { value: true }); + +function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } + +var debug = _interopDefault(require('debug')); + +/** + * Try to normalize it to use between browser and node + * @param {string} name for the debug output + * @return {function} debug + */ +var getDebug = function (name) { + if (debug) { + return debug('jsonql-ws-client').extend(name) + } + return function () { + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + console.info.apply(null, [name].concat(args)); + } +}; +try { + if (window && window.localStorage) { + localStorage.setItem('DEBUG', 'jsonql-ws-client*'); + } +} catch(e) {} + +var NB_EVENT_SERVICE_PRIVATE_STORE = new WeakMap(); +var NB_EVENT_SERVICE_PRIVATE_LAZY = new WeakMap(); + +/** + * generate a 32bit hash based on the function.toString() + * _from http://stackoverflow.com/questions/7616461/generate-a-hash-_from-string-in-javascript-jquery + * @param {string} s the converted to string function + * @return {string} the hashed function string + */ +function hashCode(s) { + return s.split("").reduce(function(a,b){a=((a<<5)-a)+b.charCodeAt(0);return a&a},0) +} + +// this is the new implementation without the hash key +// export +var EventService = function EventService(config) { + if ( config === void 0 ) config = {}; + + if (config.logger && typeof config.logger === 'function') { + this.logger = config.logger; + } + this.keep = config.keep; + // for the $done setter + this.result = config.keep ? [] : null; + // we need to init the store first otherwise it could be a lot of checking later + this.normalStore = new Map(); + this.lazyStore = new Map(); +}; + +var prototypeAccessors = { $done: { configurable: true },normalStore: { configurable: true },lazyStore: { configurable: true } }; + +/** + * logger function for overwrite + */ +EventService.prototype.logger = function logger () {}; + +////////////////////////// +// PUBLIC METHODS // +////////////////////////// + +/** + * Register your evt handler, note we don't check the type here, + * we expect you to be sensible and know what you are doing. + * @param {string} evt name of event + * @param {function} callback bind method --> if it's array or not + * @param {object} [context=null] to execute this call in + * @return {number} the size of the store + */ +EventService.prototype.$on = function $on (evt , callback , context) { + var this$1 = this; + if ( context === void 0 ) context = null; + + var type = 'on'; + this.validate(evt, callback); + // first need to check if this evt is in lazy store + var lazyStoreContent = this.takeFromStore(evt); + // this is normal register first then call later + if (lazyStoreContent === false) { + this.logger('$on', (evt + " callback is not in lazy store")); + // @TODO we need to check if there was other listener to this + // event and are they the same type then we could solve that + // register the different type to the same event name + + return this.addToNormalStore(evt, type, callback, context) + } + this.logger('$on', (evt + " found in lazy store")); + // this is when they call $trigger before register this callback + var size = 0; + lazyStoreContent.forEach(function (content) { + var payload = content[0]; + var ctx = content[1]; + var t = content[2]; + if (t && t !== type) { + throw new Error(("You are trying to register an event already been taken by other type: " + t)) + } + this$1.run(callback, payload, context || ctx); + size += this$1.addToNormalStore(evt, type, callback, context || ctx); + }); + return size; +}; + +/** + * once only registered it once, there is no overwrite option here + * @NOTE change in v1.3.0 $once can add multiple listeners + * but once the event fired, it will remove this event (see $only) + * @param {string} evt name + * @param {function} callback to execute + * @param {object} [context=null] the handler execute in + * @return {boolean} result + */ +EventService.prototype.$once = function $once (evt , callback , context) { + if ( context === void 0 ) context = null; + + this.validate(evt, callback); + var type = 'once'; + var lazyStoreContent = this.takeFromStore(evt); + // this is normal register before call $trigger + var nStore = this.normalStore; + if (lazyStoreContent === false) { + this.logger('$once', (evt + " not in the lazy store")); + // v1.3.0 $once now allow to add multiple listeners + return this.addToNormalStore(evt, type, callback, context) + } else { + // now this is the tricky bit + // there is a potential bug here that cause by the developer + // if they call $trigger first, the lazy won't know it's a once call + // so if in the middle they register any call with the same evt name + // then this $once call will be fucked - add this to the documentation + this.logger('$once', lazyStoreContent); + var list = Array.from(lazyStoreContent); + // should never have more than 1 + var ref = list[0]; + var payload = ref[0]; + var ctx = ref[1]; + var t = ref[2]; + if (t && t !== type) { + throw new Error(("You are trying to register an event already been taken by other type: " + t)) + } + this.run(callback, payload, context || ctx); + // remove this evt from store + this.$off(evt); + } +}; + +/** + * This one event can only bind one callbackback + * @param {string} evt event name + * @param {function} callback event handler + * @param {object} [context=null] the context the event handler execute in + * @return {boolean} true bind for first time, false already existed + */ +EventService.prototype.$only = function $only (evt, callback, context) { + var this$1 = this; + if ( context === void 0 ) context = null; + + this.validate(evt, callback); + var type = 'only'; + var added = false; + var lazyStoreContent = this.takeFromStore(evt); + // this is normal register before call $trigger + var nStore = this.normalStore; + if (!nStore.has(evt)) { + this.logger("$only", (evt + " add to store")); + added = this.addToNormalStore(evt, type, callback, context); + } + if (lazyStoreContent !== false) { + // there are data store in lazy store + this.logger('$only', (evt + " found data in lazy store to execute")); + var list = Array.from(lazyStoreContent); + // $only allow to trigger this multiple time on the single handler + list.forEach( function (l) { + var payload = l[0]; + var ctx = l[1]; + var t = l[2]; + if (t && t !== type) { + throw new Error(("You are trying to register an event already been taken by other type: " + t)) + } + this$1.run(callback, payload, context || ctx); + }); + } + return added; +}; + +/** + * $only + $once this is because I found a very subtile bug when we pass a + * resolver, rejecter - and it never fire because that's OLD adeed in v1.4.0 + * @param {string} evt event name + * @param {function} callback to call later + * @param {object} [context=null] exeucte context + * @return {void} + */ +EventService.prototype.$onlyOnce = function $onlyOnce (evt, callback, context) { + if ( context === void 0 ) context = null; + + this.validate(evt, callback); + var type = 'onlyOnce'; + var added = false; + var lazyStoreContent = this.takeFromStore(evt); + // this is normal register before call $trigger + var nStore = this.normalStore; + if (!nStore.has(evt)) { + this.logger("$onlyOnce", (evt + " add to store")); + added = this.addToNormalStore(evt, type, callback, context); + } + if (lazyStoreContent !== false) { + // there are data store in lazy store + this.logger('$onlyOnce', lazyStoreContent); + var list = Array.from(lazyStoreContent); + // should never have more than 1 + var ref = list[0]; + var payload = ref[0]; + var ctx = ref[1]; + var t = ref[2]; + if (t && t !== 'onlyOnce') { + throw new Error(("You are trying to register an event already been taken by other type: " + t)) + } + this.run(callback, payload, context || ctx); + // remove this evt from store + this.$off(evt); + } + return added; +}; + +/** + * This is a shorthand of $off + $on added in V1.5.0 + * @param {string} evt event name + * @param {function} callback to exeucte + * @param {object} [context = null] or pass a string as type + * @param {string} [type=on] what type of method to replace + * @return {} + */ +EventService.prototype.$replace = function $replace (evt, callback, context, type) { + if ( context === void 0 ) context = null; + if ( type === void 0 ) type = 'on'; + + if (this.validateType(type)) { + this.$off(evt); + var method = this['$' + type]; + return Reflect.apply(method, this, [evt, callback, context]) + } + throw new Error((type + " is not supported!")) +}; + +/** + * trigger the event + * @param {string} evt name NOT allow array anymore! + * @param {mixed} [payload = []] pass to fn + * @param {object|string} [context = null] overwrite what stored + * @param {string} [type=false] if pass this then we need to add type to store too + * @return {number} if it has been execute how many times + */ +EventService.prototype.$trigger = function $trigger (evt , payload , context, type) { + if ( payload === void 0 ) payload = []; + if ( context === void 0 ) context = null; + if ( type === void 0 ) type = false; + + this.validateEvt(evt); + var found = 0; + // first check the normal store + var nStore = this.normalStore; + this.logger('$trigger', nStore); + if (nStore.has(evt)) { + this.logger('$trigger', evt, 'found'); + var nSet = Array.from(nStore.get(evt)); + var ctn = nSet.length; + var hasOnce = false; + for (var i=0; i < ctn; ++i) { + ++found; + // this.logger('found', found) + var ref = nSet[i]; + var _ = ref[0]; + var callback = ref[1]; + var ctx = ref[2]; + var type$1 = ref[3]; + this.run(callback, payload, context || ctx); + if (type$1 === 'once' || type$1 === 'onlyOnce') { + hasOnce = true; + } + } + if (hasOnce) { + nStore.delete(evt); + } + return found; + } + // now this is not register yet + this.addToLazyStore(evt, payload, context, type); + return found; +}; + +/** + * this is an alias to the $trigger + * @NOTE breaking change in V1.6.0 we swap the parameter around + * @param {string} evt event name + * @param {*} params pass to the callback + * @param {string} type of call + * @param {object} context what context callback execute in + * @return {*} from $trigger + */ +EventService.prototype.$call = function $call (evt, params, type, context) { + if ( type === void 0 ) type = false; + if ( context === void 0 ) context = null; + + var args = [evt, params]; + args.push(context, type); + return Reflect.apply(this.$trigger, this, args) +}; + +/** + * remove the evt from all the stores + * @param {string} evt name + * @return {boolean} true actually delete something + */ +EventService.prototype.$off = function $off (evt) { + this.validateEvt(evt); + var stores = [ this.lazyStore, this.normalStore ]; + var found = false; + stores.forEach(function (store) { + if (store.has(evt)) { + found = true; + store.delete(evt); + } + }); + return found; +}; + +/** + * return all the listener from the event + * @param {string} evtName event name + * @param {boolean} [full=false] if true then return the entire content + * @return {array|boolean} listerner(s) or false when not found + */ +EventService.prototype.$get = function $get (evt, full) { + if ( full === void 0 ) full = false; + + this.validateEvt(evt); + var store = this.normalStore; + if (store.has(evt)) { + return Array + .from(store.get(evt)) + .map( function (l) { + if (full) { + return l; + } + var key = l[0]; + var callback = l[1]; + return callback; + }) + } + return false; +}; + +/** + * store the return result from the run + * @param {*} value whatever return from callback + */ +prototypeAccessors.$done.set = function (value) { + this.logger('set $done', value); + if (this.keep) { + this.result.push(value); + } else { + this.result = value; + } +}; + +/** + * @TODO is there any real use with the keep prop? + * getter for $done + * @return {*} whatever last store result + */ +prototypeAccessors.$done.get = function () { + if (this.keep) { + this.logger(this.result); + return this.result[this.result.length - 1] + } + return this.result; +}; + +///////////////////////////// +// PRIVATE METHODS // +///////////////////////////// + +/** + * validate the event name + * @param {string} evt event name + * @return {boolean} true when OK + */ +EventService.prototype.validateEvt = function validateEvt (evt) { + if (typeof evt === 'string') { + return true; + } + throw new Error("event name must be string type!") +}; + +/** + * Simple quick check on the two main parameters + * @param {string} evt event name + * @param {function} callback function to call + * @return {boolean} true when OK + */ +EventService.prototype.validate = function validate (evt, callback) { + if (this.validateEvt(evt)) { + if (typeof callback === 'function') { + return true; + } + } + throw new Error("callback required to be function type!") +}; + +/** + * Check if this type is correct or not added in V1.5.0 + * @param {string} type for checking + * @return {boolean} true on OK + */ +EventService.prototype.validateType = function validateType (type) { + var types = ['on', 'only', 'once', 'onlyOnce']; + return !!types.filter(function (t) { return type === t; }).length; +}; + +/** + * Run the callback + * @param {function} callback function to execute + * @param {array} payload for callback + * @param {object} ctx context or null + * @return {void} the result store in $done + */ +EventService.prototype.run = function run (callback, payload, ctx) { + this.logger('run', callback, payload, ctx); + this.$done = Reflect.apply(callback, ctx, this.toArray(payload)); +}; + +/** + * Take the content out and remove it from store id by the name + * @param {string} evt event name + * @param {string} [storeName = lazyStore] name of store + * @return {object|boolean} content or false on not found + */ +EventService.prototype.takeFromStore = function takeFromStore (evt, storeName) { + if ( storeName === void 0 ) storeName = 'lazyStore'; + + var store = this[storeName]; // it could be empty at this point + if (store) { + this.logger('takeFromStore', storeName, store); + if (store.has(evt)) { + var content = store.get(evt); + this.logger('takeFromStore', content); + store.delete(evt); + return content; + } + return false; + } + throw new Error((storeName + " is not supported!")) +}; + +/** + * The add to store step is similar so make it generic for resuse + * @param {object} store which store to use + * @param {string} evt event name + * @param {spread} args because the lazy store and normal store store different things + * @return {array} store and the size of the store + */ +EventService.prototype.addToStore = function addToStore (store, evt) { + var args = [], len = arguments.length - 2; + while ( len-- > 0 ) args[ len ] = arguments[ len + 2 ]; + + var fnSet; + if (store.has(evt)) { + this.logger('addToStore', (evt + " existed")); + fnSet = store.get(evt); + } else { + this.logger('addToStore', ("create new Set for " + evt)); + // this is new + fnSet = new Set(); + } + // lazy only store 2 items - this is not the case in V1.6.0 anymore + // we need to check the first parameter is string or not + if (args.length > 2) { + if (Array.isArray(args[0])) { // lazy store + // check if this type of this event already register in the lazy store + var t = args[2]; + if (!this.checkTypeInLazyStore(evt, t)) { + fnSet.add(args); + } + } else { + if (!this.checkContentExist(args, fnSet)) { + this.logger('addToStore', "insert new", args); + fnSet.add(args); + } + } + } else { // add straight to lazy store + fnSet.add(args); + } + store.set(evt, fnSet); + return [store, fnSet.size] +}; + +/** + * @param {array} args for compare + * @param {object} fnSet A Set to search from + * @return {boolean} true on exist + */ +EventService.prototype.checkContentExist = function checkContentExist (args, fnSet) { + var list = Array.from(fnSet); + return !!list.filter(function (l) { + var hash = l[0]; + if (hash === args[0]) { + return true; + } + return false; + }).length; +}; + +/** + * get the existing type to make sure no mix type add to the same store + * @param {string} evtName event name + * @param {string} type the type to check + * @return {boolean} true you can add, false then you can't add this type + */ +EventService.prototype.checkTypeInStore = function checkTypeInStore (evtName, type) { + this.validateEvt(evtName); + this.validateEvt(type); + var all = this.$get(evtName, true); + if (all === false) { + // pristine it means you can add + return true; + } + // it should only have ONE type in ONE event store + return !all.filter(function (list) { + var t = list[3]; + return type !== t; + }).length; +}; + +/** + * This is checking just the lazy store because the structure is different + * therefore we need to use a new method to check it + */ +EventService.prototype.checkTypeInLazyStore = function checkTypeInLazyStore (evtName, type) { + this.validateEvt(evtName); + this.validateEvt(type); + var store = this.lazyStore.get(evtName); + this.logger('checkTypeInLazyStore', store); + if (store) { + return !!Array + .from(store) + .filter(function (l) { + var t = l[2]; + return t !== type; + }).length + } + return false; +}; + +/** + * wrapper to re-use the addToStore, + * V1.3.0 add extra check to see if this type can add to this evt + * @param {string} evt event name + * @param {string} type on or once + * @param {function} callback function + * @param {object} context the context the function execute in or null + * @return {number} size of the store + */ +EventService.prototype.addToNormalStore = function addToNormalStore (evt, type, callback, context) { + if ( context === void 0 ) context = null; + + this.logger('addToNormalStore', evt, type, 'add to normal store'); + // @TODO we need to check the existing store for the type first! + if (this.checkTypeInStore(evt, type)) { + this.logger((type + " can add to " + evt + " store")); + var key = this.hashFnToKey(callback); + var args = [this.normalStore, evt, key, callback, context, type]; + var ref = Reflect.apply(this.addToStore, this, args); + var _store = ref[0]; + var size = ref[1]; + this.normalStore = _store; + return size; + } + return false; +}; + +/** + * Add to lazy store this get calls when the callback is not register yet + * so we only get a payload object or even nothing + * @param {string} evt event name + * @param {array} payload of arguments or empty if there is none + * @param {object} [context=null] the context the callback execute in + * @param {string} [type=false] register a type so no other type can add to this evt + * @return {number} size of the store + */ +EventService.prototype.addToLazyStore = function addToLazyStore (evt, payload, context, type) { + if ( payload === void 0 ) payload = []; + if ( context === void 0 ) context = null; + if ( type === void 0 ) type = false; + + // this is add in V1.6.0 + // when there is type then we will need to check if this already added in lazy store + // and no other type can add to this lazy store + var args = [this.lazyStore, evt, this.toArray(payload), context]; + if (type) { + args.push(type); + } + var ref = Reflect.apply(this.addToStore, this, args); + var _store = ref[0]; + var size = ref[1]; + this.lazyStore = _store; + return size; +}; + +/** + * make sure we store the argument correctly + * @param {*} arg could be array + * @return {array} make sured + */ +EventService.prototype.toArray = function toArray (arg) { + return Array.isArray(arg) ? arg : [arg]; +}; + +/** + * setter to store the Set in private + * @param {object} obj a Set + */ +prototypeAccessors.normalStore.set = function (obj) { + NB_EVENT_SERVICE_PRIVATE_STORE.set(this, obj); +}; + +/** + * @return {object} Set object + */ +prototypeAccessors.normalStore.get = function () { + return NB_EVENT_SERVICE_PRIVATE_STORE.get(this) +}; + +/** + * setter to store the Set in lazy store + * @param {object} obj a Set + */ +prototypeAccessors.lazyStore.set = function (obj) { + NB_EVENT_SERVICE_PRIVATE_LAZY.set(this , obj); +}; + +/** + * @return {object} the lazy store Set + */ +prototypeAccessors.lazyStore.get = function () { + return NB_EVENT_SERVICE_PRIVATE_LAZY.get(this) +}; + +/** + * generate a hashKey to identify the function call + * The build-in store some how could store the same values! + * @param {function} fn the converted to string function + * @return {string} hashKey + */ +EventService.prototype.hashFnToKey = function hashFnToKey (fn) { + return hashCode(fn.toString()) + ''; +}; + +Object.defineProperties( EventService.prototype, prototypeAccessors ); + +// default + +// create a clone version so we know which one we actually is using +var JsonqlWsEvt = /*@__PURE__*/(function (NBEventService) { + function JsonqlWsEvt() { + NBEventService.call(this, {logger: getDebug('nb-event-service')}); + } + + if ( NBEventService ) JsonqlWsEvt.__proto__ = NBEventService; + JsonqlWsEvt.prototype = Object.create( NBEventService && NBEventService.prototype ); + JsonqlWsEvt.prototype.constructor = JsonqlWsEvt; + + var prototypeAccessors = { name: { configurable: true } }; + + prototypeAccessors.name.get = function () { + return 'jsonql-ws-client' + }; + + Object.defineProperties( JsonqlWsEvt.prototype, prototypeAccessors ); + + return JsonqlWsEvt; +}(EventService)); + +// break this out for testing purposes +var debugFn = getDebug('chain-create-nsps'); +/** + * previously we already make sure the order of the namespaces + * and attach the auth client to it + * @param {array} promises array of unresolved promises + * @return {object} promise resolved with the array of promises resolved results + */ +function chainCreateNsps(promises) { + return promises.reduce(function (promiseChain, currentTask) { + debugFn('out %O', currentTask); + return promiseChain.then(function (chainResults) { return ( + currentTask.then(function (currentResult) { return ( + chainResults.concat( [currentResult]) + ); }) + ); }) + }, Promise.resolve([])) +} + +// test inteface to figure out what went wrong with the connection + +/// INIT //// +var es = new JsonqlWsEvt({ + logger: debugFn$1 +}); +var debugFn$1 = debug('jsonql-ws-client:test:cjs'); + +exports.chainCreateNsps = chainCreateNsps; +exports.es = es; diff --git a/packages/ws-base/tests/fixtures/client/node/test.js b/packages/ws-base/tests/fixtures/client/node/test.js new file mode 100644 index 0000000000000000000000000000000000000000..d23876165c43d6be6a66702699fda1989a13e4b5 --- /dev/null +++ b/packages/ws-base/tests/fixtures/client/node/test.js @@ -0,0 +1,16 @@ +// test inteface to figure out what went wrong with the connection + +import debug from 'debug' +import ee from '../../../src/utils/ee' +import chainCreateNsps from '../../../src/utils/chain-create-nsps' + +/// INIT //// +const es = new ee({ + logger: debugFn +}) +const debugFn = debug('jsonql-ws-client:test:cjs') + +export { + chainCreateNsps, + es +} diff --git a/packages/ws-base/tests/fixtures/client/public-contract.json b/packages/ws-base/tests/fixtures/client/public-contract.json new file mode 100644 index 0000000000000000000000000000000000000000..43cfd2b37fce72eec46df65703bb5e054e015c0f --- /dev/null +++ b/packages/ws-base/tests/fixtures/client/public-contract.json @@ -0,0 +1,91 @@ +{ + "query": { + "helloWorld": { + "description": "This is the stock resolver for testing purpose", + "params": [], + "returns": [ + { + "type": "string", + "description": "stock message" + } + ] + } + }, + "mutation": {}, + "auth": {}, + "timestamp": 1560347818, + "socket": { + "continuous": { + "description": false, + "params": [ + { + "type": [ + "string" + ], + "name": "msg", + "description": "a message" + } + ], + "returns": [ + { + "type": [ + "string" + ], + "description": "a message with timestamp" + } + ] + }, + "sendExtraMsg": { + "description": false, + "params": [ + { + "type": [ + "number" + ], + "name": "x", + "description": "a number for process" + } + ], + "returns": [ + { + "type": [ + "number" + ], + "description": "x + ?" + } + ] + }, + "simple": { + "description": false, + "params": [ + { + "type": [ + "number" + ], + "name": "i", + "description": "a number" + } + ], + "returns": [ + { + "type": [ + "number" + ], + "description": "a number + 1;" + } + ] + }, + "throwError": { + "description": "Testing the throw error", + "params": [], + "returns": [ + { + "type": [ + "any" + ], + "description": "just throw" + } + ] + } + } +} diff --git a/packages/ws-base/tests/fixtures/client/resolvers/socket/continuous.js b/packages/ws-base/tests/fixtures/client/resolvers/socket/continuous.js new file mode 100644 index 0000000000000000000000000000000000000000..3d468bb97fbe076216fde1fa5137f6c2f0b58dc9 --- /dev/null +++ b/packages/ws-base/tests/fixtures/client/resolvers/socket/continuous.js @@ -0,0 +1,21 @@ +// this will keep sending out message until received a terminate call +let timer; +let ctn = 0; +const debug = require('debug')('jsonql-ws-client:socket:continous') +/** + * @param {string} msg a message + * @return {string} a message with timestamp + */ +module.exports = function continuous(msg) { + if (msg === 'terminate') { + return clearInterval(timer) + } + // use the send setter instead + timer = setInterval(() => { + continuous.send = msg + ` [${++ctn}] ${Date.now()}`; + }, 1000); + // return result + return new Promise((resolver) => { + resolver(`start at ${Date.now()}`) + }); +} diff --git a/packages/ws-base/tests/fixtures/client/resolvers/socket/public/pinging.js b/packages/ws-base/tests/fixtures/client/resolvers/socket/public/pinging.js new file mode 100644 index 0000000000000000000000000000000000000000..2cf0735176ab954756f092e9468dfc55f4d42663 --- /dev/null +++ b/packages/ws-base/tests/fixtures/client/resolvers/socket/public/pinging.js @@ -0,0 +1,22 @@ +// this is a public method always avaialble +let ctn = 0; +/** + * @param {string} msg message + * @return {string} reply message based on your message + */ +module.exports = function pinging(msg) { + if (ctn > 0) { + switch (msg) { + case 'ping': + pinging.send = 'pong'; + case 'pong': + pinging.send = 'ping'; + default: + return; + //pinging.send = 'You lose!'; + } + return; + } + ++ctn; + return 'connection established'; +} diff --git a/packages/ws-base/tests/fixtures/client/resolvers/socket/send-extra-msg.js b/packages/ws-base/tests/fixtures/client/resolvers/socket/send-extra-msg.js new file mode 100644 index 0000000000000000000000000000000000000000..9144b99adac8272955e4f636174093a38a184604 --- /dev/null +++ b/packages/ws-base/tests/fixtures/client/resolvers/socket/send-extra-msg.js @@ -0,0 +1,12 @@ +// this one will use the property send to send an different message + + +/** + * @param {number} x a number for process + * @return {number} x + ? + */ +module.exports = function sendExtraMsg(x) { + sendExtraMsg.send = x + 2; + + return x + 1; +} diff --git a/packages/ws-base/tests/fixtures/client/resolvers/socket/simple.js b/packages/ws-base/tests/fixtures/client/resolvers/socket/simple.js new file mode 100644 index 0000000000000000000000000000000000000000..61810eae68ed7c1c7c2442288d62843acdbd873c --- /dev/null +++ b/packages/ws-base/tests/fixtures/client/resolvers/socket/simple.js @@ -0,0 +1,9 @@ +// just simple send and process + +/** + * @param {number} i a number + * @return {number} a number + 1; + */ +module.exports = function(i) { + return ++i; +} diff --git a/packages/ws-base/tests/fixtures/client/resolvers/socket/throw-error.js b/packages/ws-base/tests/fixtures/client/resolvers/socket/throw-error.js new file mode 100644 index 0000000000000000000000000000000000000000..a7fcd6f4b4bce70b962a312f7abe1c5025852130 --- /dev/null +++ b/packages/ws-base/tests/fixtures/client/resolvers/socket/throw-error.js @@ -0,0 +1,9 @@ +// this one will throw an error + +/** + * Testing the throw error + * @return {error} just throw + */ +module.exports = function() { + throw new Error('Shitty Shitty Bang Bang'); +} diff --git a/packages/ws-base/tests/fixtures/client/server-setup.js b/packages/ws-base/tests/fixtures/client/server-setup.js new file mode 100644 index 0000000000000000000000000000000000000000..1bb5ef2be4ac691269b8a6aa5204eda82019a855 --- /dev/null +++ b/packages/ws-base/tests/fixtures/client/server-setup.js @@ -0,0 +1,37 @@ +const http = require('http') +const fsx = require('fs-extra') +const { join } = require('path') +const debug = require('debug')('jsonql-ws-client:fixtures:server') +const { JSONQL_PATH } = require('jsonql-constants') + +const resolverDir = join(__dirname, 'resolvers') +const contractDir = join(__dirname, 'contract') +// require('../../../ws-server') +const wsServer = require('jsonql-ws-server') + +// start +const server = http.createServer(function(req, res) { + res.writeHead(200, { 'Content-Type': 'text/plain' }) + res.write('request successfully proxied!' + '\n' + JSON.stringify(req.headers, true, 2)) + res.end() +}) + +module.exports = function(extra = {}) { + // extra.contract = extra.contract || contract; + return new Promise(resolver => { + wsServer( + Object.assign({ + resolverDir, + contractDir, + serverType: 'ws' + }, extra), + server + ) + .then(io => { + resolver({ + io, + app: server + }); + }) + }); +} diff --git a/packages/ws-base/tests/fixtures/client/token.js b/packages/ws-base/tests/fixtures/client/token.js new file mode 100644 index 0000000000000000000000000000000000000000..b1d444430ce0430ea315ea3e822e7d940f4e0e69 --- /dev/null +++ b/packages/ws-base/tests/fixtures/client/token.js @@ -0,0 +1,16 @@ +// generate token + +const { join } = require('path') +const fsx = require('fs-extra') +const privateKey = fsx.readFileSync(join(__dirname, 'keys', 'privateKey.pem')) +const { jwtRsaToken, jwtToken } = require('jsonql-jwt') +const { HSA_ALGO } = require('jsonql-constants') + +module.exports = function(payload, key = false) { + if (key === false) { + return jwtRsaToken(payload, privateKey) + } + return jwtToken(payload, key, { + algorithm: HSA_ALGO + }) +} diff --git a/packages/ws-base/tests/fixtures/server/auth/contract.json b/packages/ws-base/tests/fixtures/server/auth/contract.json new file mode 100644 index 0000000000000000000000000000000000000000..6f283cfd1cf971026570ed0fc7f8914ecf995452 --- /dev/null +++ b/packages/ws-base/tests/fixtures/server/auth/contract.json @@ -0,0 +1,189 @@ +{ + "query": {}, + "mutation": {}, + "auth": { + "login": { + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/resolvers/auth/login.js", + "description": "create a login method for testing", + "params": [ + { + "type": [ + "string" + ], + "name": "username", + "description": "username" + } + ], + "returns": [ + { + "type": [ + "object" + ], + "description": "userdata" + } + ] + }, + "logout": { + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/resolvers/auth/logout.js", + "description": "testing the logout method call", + "params": [], + "returns": [ + { + "type": [ + "boolean" + ], + "description": "true" + } + ] + } + }, + "timestamp": 1560178874072, + "socket": { + "causeError": { + "namespace": "jsonql/private", + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/resolvers/socket/cause-error.js", + "description": false, + "params": [ + { + "type": [ + "string" + ], + "name": "msg", + "description": "a message" + } + ], + "returns": [ + { + "type": [ + "string" + ], + "description": "a message but here we throw an error" + } + ] + }, + "chatroom": { + "namespace": "jsonql/private", + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/resolvers/socket/chatroom.js", + "description": false, + "params": [ + { + "type": [ + "string" + ], + "name": "msg", + "description": "message" + }, + { + "type": [ + "number" + ], + "name": "timestamp", + "description": "for checking the time" + } + ], + "returns": [ + { + "type": [ + "string" + ], + "description": "reply" + } + ] + }, + "delayFn": { + "namespace": "jsonql/private", + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/resolvers/socket/delay-fn.js", + "description": false, + "params": [ + { + "type": [ + "string" + ], + "name": "msg", + "description": "a message" + }, + { + "type": [ + "number" + ], + "name": "timestamp", + "description": "a timestamp" + } + ], + "returns": false + }, + "secretChatroom": { + "namespace": "jsonql/private", + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/resolvers/socket/private/secret-chatroom.js", + "description": false, + "params": [ + { + "type": [ + "string" + ], + "name": "room", + "description": "room name" + }, + { + "type": [ + "any" + ], + "name": "msg", + "description": "message to that room" + } + ], + "returns": [ + { + "type": [ + "any" + ], + "description": "depends" + } + ] + }, + "availableToEveryone": { + "namespace": "jsonql/public", + "public": true, + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/resolvers/socket/public/available-to-everyone/index.js", + "description": "There is no parameter require for this call", + "params": [], + "returns": [ + { + "type": [ + "string" + ], + "description": "a message" + } + ] + }, + "wsHandler": { + "namespace": "jsonql/private", + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/resolvers/socket/ws-handler.js", + "description": "method just for testing the ws", + "params": [ + { + "type": [ + "string" + ], + "name": "msg", + "description": "message" + }, + { + "type": [ + "number" + ], + "name": "timestamp", + "description": "timestamp" + } + ], + "returns": [ + { + "type": [ + "string" + ], + "description": "msg + time lapsed" + } + ] + } + } +} diff --git a/packages/ws-base/tests/fixtures/server/browser-test-setup.js b/packages/ws-base/tests/fixtures/server/browser-test-setup.js new file mode 100644 index 0000000000000000000000000000000000000000..bdf92d6cff7181daef191f98a804a7e43b66a8e8 --- /dev/null +++ b/packages/ws-base/tests/fixtures/server/browser-test-setup.js @@ -0,0 +1,41 @@ +const Koa = require('koa') +const IO = require('koa-socket-2') +const socketioJwt = require('socketio-jwt') +const { join } = require('path'); + +const default_io = new IO({namespace: 'jsonql'}) +const app = new Koa() + +const root = join(__dirname, 'demo') +const port = process.env.PORT || 3001; +let timer; +app.use(require('koa-static')(root)) + +default_io.attach(app) + +// should able to show the methods +console.log(app['jsonql'].onConnection) + +default_io.on('chat message', function(msg){ + console.info('got a message', msg); + if (msg === 'terminate') { + clearTimeout(timer); + } + default_io.emit('chat message', msg); +}) + +// hijack the connnection for all channels +/* +app['jsonql'].on('connection', socketioJwt.authorize({ + secret: 'your secret or public key', + timeout: 15000 // 15 seconds to send the authentication message + })).on('authenticated', function(socket) { + //this socket is authenticated, we are good to handle more events from it. + console.log('hello! ' + socket.decoded_token.name); + })); +*/ + +app.listen( port , () => { + console.log('start @ ' + port) + open('http://localhost:' + port) +}) diff --git a/packages/ws-base/tests/fixtures/server/contract-config.js b/packages/ws-base/tests/fixtures/server/contract-config.js new file mode 100644 index 0000000000000000000000000000000000000000..1fea5a751a3578eeec828b8f387ed41be3b570d1 --- /dev/null +++ b/packages/ws-base/tests/fixtures/server/contract-config.js @@ -0,0 +1,6 @@ +const { join } = require('path'); + +module.exports = { + resolverDir: join(__dirname, 'resolvers'), + contractDir: join(__dirname, 'contract') +}; diff --git a/packages/ws-base/tests/fixtures/server/contract.js b/packages/ws-base/tests/fixtures/server/contract.js new file mode 100644 index 0000000000000000000000000000000000000000..71643a2f45b678eecbef5c27b071e0a995f2737c --- /dev/null +++ b/packages/ws-base/tests/fixtures/server/contract.js @@ -0,0 +1,13 @@ +// generate contract +const { join } = require('path') +const contractApi = require('jsonql-contract'); +const contractDir = join(__dirname , 'contract', 'auth') +const resolverDir = join(__dirname , 'resolvers') + +contractApi({ + contractDir, + resolverDir, + enableAuth: true, + useJwt: true, + privateMethodDir: 'private' +}) diff --git a/packages/ws-base/tests/fixtures/server/contract.json b/packages/ws-base/tests/fixtures/server/contract.json new file mode 100644 index 0000000000000000000000000000000000000000..dd3a5ef6af6889271a1fbde197e61020f781b1ab --- /dev/null +++ b/packages/ws-base/tests/fixtures/server/contract.json @@ -0,0 +1,110 @@ +{ + "query": {}, + "mutation": {}, + "auth": {}, + "timestamp": 1559136905329, + "socket": { + "causeError": { + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/resolvers/socket/cause-error.js", + "event": "cause-error", + "description": false, + "params": [ + { + "type": [ + "string" + ], + "name": "msg", + "description": "a message" + } + ], + "returns": [ + { + "type": [ + "string" + ], + "description": "a message but here we throw an error" + } + ] + }, + "chatroom": { + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/resolvers/socket/chatroom.js", + "event": "chatroom", + "description": false, + "params": [ + { + "type": [ + "string" + ], + "name": "msg", + "description": "message" + }, + { + "type": [ + "number" + ], + "name": "timestamp", + "description": "for checking the time" + } + ], + "returns": [ + { + "type": [ + "string" + ], + "description": "reply" + } + ] + }, + "delayFn": { + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/resolvers/socket/delay-fn.js", + "event": "delay-fn", + "description": false, + "params": [ + { + "type": [ + "string" + ], + "name": "msg", + "description": "a message" + }, + { + "type": [ + "number" + ], + "name": "timestamp", + "description": "a timestamp" + } + ], + "returns": false + }, + "wsHandler": { + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/resolvers/socket/ws-handler.js", + "event": "ws-handler", + "description": "method just for testing the ws", + "params": [ + { + "type": [ + "string" + ], + "name": "msg", + "description": "message" + }, + { + "type": [ + "number" + ], + "name": "timestamp", + "description": "timestamp" + } + ], + "returns": [ + { + "type": [ + "string" + ], + "description": "msg + time lapsed" + } + ] + } + } +} diff --git a/packages/ws-base/tests/fixtures/server/contract/auth/contract.json b/packages/ws-base/tests/fixtures/server/contract/auth/contract.json new file mode 100644 index 0000000000000000000000000000000000000000..6f283cfd1cf971026570ed0fc7f8914ecf995452 --- /dev/null +++ b/packages/ws-base/tests/fixtures/server/contract/auth/contract.json @@ -0,0 +1,189 @@ +{ + "query": {}, + "mutation": {}, + "auth": { + "login": { + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/resolvers/auth/login.js", + "description": "create a login method for testing", + "params": [ + { + "type": [ + "string" + ], + "name": "username", + "description": "username" + } + ], + "returns": [ + { + "type": [ + "object" + ], + "description": "userdata" + } + ] + }, + "logout": { + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/resolvers/auth/logout.js", + "description": "testing the logout method call", + "params": [], + "returns": [ + { + "type": [ + "boolean" + ], + "description": "true" + } + ] + } + }, + "timestamp": 1560178874072, + "socket": { + "causeError": { + "namespace": "jsonql/private", + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/resolvers/socket/cause-error.js", + "description": false, + "params": [ + { + "type": [ + "string" + ], + "name": "msg", + "description": "a message" + } + ], + "returns": [ + { + "type": [ + "string" + ], + "description": "a message but here we throw an error" + } + ] + }, + "chatroom": { + "namespace": "jsonql/private", + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/resolvers/socket/chatroom.js", + "description": false, + "params": [ + { + "type": [ + "string" + ], + "name": "msg", + "description": "message" + }, + { + "type": [ + "number" + ], + "name": "timestamp", + "description": "for checking the time" + } + ], + "returns": [ + { + "type": [ + "string" + ], + "description": "reply" + } + ] + }, + "delayFn": { + "namespace": "jsonql/private", + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/resolvers/socket/delay-fn.js", + "description": false, + "params": [ + { + "type": [ + "string" + ], + "name": "msg", + "description": "a message" + }, + { + "type": [ + "number" + ], + "name": "timestamp", + "description": "a timestamp" + } + ], + "returns": false + }, + "secretChatroom": { + "namespace": "jsonql/private", + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/resolvers/socket/private/secret-chatroom.js", + "description": false, + "params": [ + { + "type": [ + "string" + ], + "name": "room", + "description": "room name" + }, + { + "type": [ + "any" + ], + "name": "msg", + "description": "message to that room" + } + ], + "returns": [ + { + "type": [ + "any" + ], + "description": "depends" + } + ] + }, + "availableToEveryone": { + "namespace": "jsonql/public", + "public": true, + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/resolvers/socket/public/available-to-everyone/index.js", + "description": "There is no parameter require for this call", + "params": [], + "returns": [ + { + "type": [ + "string" + ], + "description": "a message" + } + ] + }, + "wsHandler": { + "namespace": "jsonql/private", + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/resolvers/socket/ws-handler.js", + "description": "method just for testing the ws", + "params": [ + { + "type": [ + "string" + ], + "name": "msg", + "description": "message" + }, + { + "type": [ + "number" + ], + "name": "timestamp", + "description": "timestamp" + } + ], + "returns": [ + { + "type": [ + "string" + ], + "description": "msg + time lapsed" + } + ] + } + } +} diff --git a/packages/ws-base/tests/fixtures/server/contract/contract.json b/packages/ws-base/tests/fixtures/server/contract/contract.json new file mode 100644 index 0000000000000000000000000000000000000000..dd3a5ef6af6889271a1fbde197e61020f781b1ab --- /dev/null +++ b/packages/ws-base/tests/fixtures/server/contract/contract.json @@ -0,0 +1,110 @@ +{ + "query": {}, + "mutation": {}, + "auth": {}, + "timestamp": 1559136905329, + "socket": { + "causeError": { + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/resolvers/socket/cause-error.js", + "event": "cause-error", + "description": false, + "params": [ + { + "type": [ + "string" + ], + "name": "msg", + "description": "a message" + } + ], + "returns": [ + { + "type": [ + "string" + ], + "description": "a message but here we throw an error" + } + ] + }, + "chatroom": { + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/resolvers/socket/chatroom.js", + "event": "chatroom", + "description": false, + "params": [ + { + "type": [ + "string" + ], + "name": "msg", + "description": "message" + }, + { + "type": [ + "number" + ], + "name": "timestamp", + "description": "for checking the time" + } + ], + "returns": [ + { + "type": [ + "string" + ], + "description": "reply" + } + ] + }, + "delayFn": { + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/resolvers/socket/delay-fn.js", + "event": "delay-fn", + "description": false, + "params": [ + { + "type": [ + "string" + ], + "name": "msg", + "description": "a message" + }, + { + "type": [ + "number" + ], + "name": "timestamp", + "description": "a timestamp" + } + ], + "returns": false + }, + "wsHandler": { + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/resolvers/socket/ws-handler.js", + "event": "ws-handler", + "description": "method just for testing the ws", + "params": [ + { + "type": [ + "string" + ], + "name": "msg", + "description": "message" + }, + { + "type": [ + "number" + ], + "name": "timestamp", + "description": "timestamp" + } + ], + "returns": [ + { + "type": [ + "string" + ], + "description": "msg + time lapsed" + } + ] + } + } +} diff --git a/packages/ws-base/tests/fixtures/server/contract/es6/contract.json b/packages/ws-base/tests/fixtures/server/contract/es6/contract.json new file mode 100644 index 0000000000000000000000000000000000000000..e2a682d238cc4c5f3f17f91d788674c9fe17f206 --- /dev/null +++ b/packages/ws-base/tests/fixtures/server/contract/es6/contract.json @@ -0,0 +1,177 @@ +{ + "query": { + "getSomething": { + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/es6resolvers/query/get-something.js", + "description": false, + "params": [], + "returns": [ + { + "type": [ + "string" + ], + "description": "a message" + } + ] + } + }, + "mutation": { + "saveSomething": { + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/es6resolvers/mutation/save-something.js", + "description": false, + "params": [ + { + "type": [ + "any" + ], + "name": "data" + } + ], + "returns": [ + { + "type": [ + "boolean" + ], + "description": "true when success" + } + ] + } + }, + "auth": { + "login": { + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/es6resolvers/auth/login.js", + "description": "create a login method for testing", + "params": [ + { + "type": [ + "string" + ], + "description": "username", + "name": "username" + } + ], + "returns": [ + { + "type": [ + "object" + ], + "description": "userdata" + } + ] + }, + "logout": { + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/es6resolvers/auth/logout.js", + "description": "testing the logout method call", + "params": [], + "returns": [ + { + "type": [ + "boolean" + ], + "description": "true" + } + ] + } + }, + "timestamp": 1561989449, + "sourceType": "module", + "socket": { + "causeError": { + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/es6resolvers/socket/cause-error.js", + "description": false, + "params": [ + { + "type": [ + "string" + ], + "description": "a message", + "name": "msg" + } + ], + "returns": [ + { + "type": [ + "string" + ], + "description": "a message but here we throw an error" + } + ] + }, + "chatroom": { + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/es6resolvers/socket/chatroom.js", + "description": false, + "params": [ + { + "type": [ + "string" + ], + "description": "message", + "name": "msg" + }, + { + "type": [ + "number" + ], + "description": "for checking the time", + "name": "timestamp" + } + ], + "returns": [ + { + "type": [ + "string" + ], + "description": "reply" + } + ] + }, + "delayFn": { + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/es6resolvers/socket/delay-fn.js", + "description": false, + "params": [ + { + "type": [ + "string" + ], + "description": "a message", + "name": "msg" + }, + { + "type": [ + "number" + ], + "description": "a timestamp", + "name": "timestamp" + } + ], + "returns": false + }, + "wsHandler": { + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/es6resolvers/socket/ws-handler.js", + "description": "method just for testing the ws", + "params": [ + { + "type": [ + "string" + ], + "description": "message", + "name": "msg" + }, + { + "type": [ + "number" + ], + "description": "timestamp", + "name": "timestamp" + } + ], + "returns": [ + { + "type": [ + "string" + ], + "description": "msg + time lapsed" + } + ] + } + } +} diff --git a/packages/ws-base/tests/fixtures/server/create-payload.js b/packages/ws-base/tests/fixtures/server/create-payload.js new file mode 100644 index 0000000000000000000000000000000000000000..d5056cb06c23b8ec0beeb2a864c3d746f695a297 --- /dev/null +++ b/packages/ws-base/tests/fixtures/server/create-payload.js @@ -0,0 +1,7 @@ +const createPayload = (name, ...args) => { + return JSON.stringify({ + [name]: { args: args } + }); +} + +module.exports = createPayload; diff --git a/packages/ws-base/tests/fixtures/server/demo/index.html b/packages/ws-base/tests/fixtures/server/demo/index.html new file mode 100644 index 0000000000000000000000000000000000000000..3e07a99f858935c90e8c113451a6c2582a18cf5a --- /dev/null +++ b/packages/ws-base/tests/fixtures/server/demo/index.html @@ -0,0 +1,23 @@ + + + + Socket.IO chat + + + +

    +
    + +
    + + diff --git a/packages/ws-base/tests/fixtures/server/es6/contract.json b/packages/ws-base/tests/fixtures/server/es6/contract.json new file mode 100644 index 0000000000000000000000000000000000000000..e2a682d238cc4c5f3f17f91d788674c9fe17f206 --- /dev/null +++ b/packages/ws-base/tests/fixtures/server/es6/contract.json @@ -0,0 +1,177 @@ +{ + "query": { + "getSomething": { + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/es6resolvers/query/get-something.js", + "description": false, + "params": [], + "returns": [ + { + "type": [ + "string" + ], + "description": "a message" + } + ] + } + }, + "mutation": { + "saveSomething": { + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/es6resolvers/mutation/save-something.js", + "description": false, + "params": [ + { + "type": [ + "any" + ], + "name": "data" + } + ], + "returns": [ + { + "type": [ + "boolean" + ], + "description": "true when success" + } + ] + } + }, + "auth": { + "login": { + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/es6resolvers/auth/login.js", + "description": "create a login method for testing", + "params": [ + { + "type": [ + "string" + ], + "description": "username", + "name": "username" + } + ], + "returns": [ + { + "type": [ + "object" + ], + "description": "userdata" + } + ] + }, + "logout": { + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/es6resolvers/auth/logout.js", + "description": "testing the logout method call", + "params": [], + "returns": [ + { + "type": [ + "boolean" + ], + "description": "true" + } + ] + } + }, + "timestamp": 1561989449, + "sourceType": "module", + "socket": { + "causeError": { + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/es6resolvers/socket/cause-error.js", + "description": false, + "params": [ + { + "type": [ + "string" + ], + "description": "a message", + "name": "msg" + } + ], + "returns": [ + { + "type": [ + "string" + ], + "description": "a message but here we throw an error" + } + ] + }, + "chatroom": { + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/es6resolvers/socket/chatroom.js", + "description": false, + "params": [ + { + "type": [ + "string" + ], + "description": "message", + "name": "msg" + }, + { + "type": [ + "number" + ], + "description": "for checking the time", + "name": "timestamp" + } + ], + "returns": [ + { + "type": [ + "string" + ], + "description": "reply" + } + ] + }, + "delayFn": { + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/es6resolvers/socket/delay-fn.js", + "description": false, + "params": [ + { + "type": [ + "string" + ], + "description": "a message", + "name": "msg" + }, + { + "type": [ + "number" + ], + "description": "a timestamp", + "name": "timestamp" + } + ], + "returns": false + }, + "wsHandler": { + "file": "/home/joel/projects/open-source/jsonqltools/packages/ws-server/tests/fixtures/es6resolvers/socket/ws-handler.js", + "description": "method just for testing the ws", + "params": [ + { + "type": [ + "string" + ], + "description": "message", + "name": "msg" + }, + { + "type": [ + "number" + ], + "description": "timestamp", + "name": "timestamp" + } + ], + "returns": [ + { + "type": [ + "string" + ], + "description": "msg + time lapsed" + } + ] + } + } +} diff --git a/packages/ws-base/tests/fixtures/server/es6resolvers/auth/login.js b/packages/ws-base/tests/fixtures/server/es6resolvers/auth/login.js new file mode 100644 index 0000000000000000000000000000000000000000..1a6f309227b105ff31f39f2f989ce45243e85541 --- /dev/null +++ b/packages/ws-base/tests/fixtures/server/es6resolvers/auth/login.js @@ -0,0 +1,9 @@ + +/** + * create a login method for testing + * @param {string} username username + * @return {object} userdata + */ +export default function login(username) { + return {name: username} +} diff --git a/packages/ws-base/tests/fixtures/server/es6resolvers/auth/logout.js b/packages/ws-base/tests/fixtures/server/es6resolvers/auth/logout.js new file mode 100644 index 0000000000000000000000000000000000000000..93ae5ff6f2bba90c7fb6ecbe3fe1f4fbbb258224 --- /dev/null +++ b/packages/ws-base/tests/fixtures/server/es6resolvers/auth/logout.js @@ -0,0 +1,7 @@ +/** + * testing the logout method call + * @return {boolean} true + */ +export default function logout() { + return true; +} diff --git a/packages/ws-base/tests/fixtures/server/es6resolvers/import.js b/packages/ws-base/tests/fixtures/server/es6resolvers/import.js new file mode 100644 index 0000000000000000000000000000000000000000..6601f782c3497ae953db86928e4e6e922da04a10 --- /dev/null +++ b/packages/ws-base/tests/fixtures/server/es6resolvers/import.js @@ -0,0 +1,2 @@ +require = require("esm")(module/*, options*/) +module.exports = require("./resolver.js") diff --git a/packages/ws-base/tests/fixtures/server/es6resolvers/mutation/save-something.js b/packages/ws-base/tests/fixtures/server/es6resolvers/mutation/save-something.js new file mode 100644 index 0000000000000000000000000000000000000000..cc2371a3014d79e309d22d8d8339c41edfb513da --- /dev/null +++ b/packages/ws-base/tests/fixtures/server/es6resolvers/mutation/save-something.js @@ -0,0 +1,8 @@ + +/** + * @param {any} data + * @return {boolean} true when success + */ +export default function saveSomething(data) { + return true; +} diff --git a/packages/ws-base/tests/fixtures/server/es6resolvers/query/get-something.js b/packages/ws-base/tests/fixtures/server/es6resolvers/query/get-something.js new file mode 100644 index 0000000000000000000000000000000000000000..c2282dec7fab7d746eaac4b32f0fb651e71d58c6 --- /dev/null +++ b/packages/ws-base/tests/fixtures/server/es6resolvers/query/get-something.js @@ -0,0 +1,7 @@ +/** + * @return {string} a message + * + */ +export default function getSomething() { + return 'You got it!' +} diff --git a/packages/ws-base/tests/fixtures/server/es6resolvers/resolver.js b/packages/ws-base/tests/fixtures/server/es6resolvers/resolver.js new file mode 100644 index 0000000000000000000000000000000000000000..e61e6a77a607f32bc219b1d8560eb49a919214ec --- /dev/null +++ b/packages/ws-base/tests/fixtures/server/es6resolvers/resolver.js @@ -0,0 +1,19 @@ +import querygetSomething from './query/get-something.js' +import mutationsaveSomething from './mutation/save-something.js' +import authlogin from './auth/login.js' +import authlogout from './auth/logout.js' +import socketcauseError from './socket/cause-error.js' +import socketchatroom from './socket/chatroom.js' +import socketdelayFn from './socket/delay-fn.js' +import socketwsHandler from './socket/ws-handler.js' + +export { + querygetSomething, +mutationsaveSomething, +authlogin, +authlogout, +socketcauseError, +socketchatroom, +socketdelayFn, +socketwsHandler +} \ No newline at end of file diff --git a/packages/ws-base/tests/fixtures/server/es6resolvers/socket/cause-error.js b/packages/ws-base/tests/fixtures/server/es6resolvers/socket/cause-error.js new file mode 100644 index 0000000000000000000000000000000000000000..40c1b247885c4654206bdc6507c444841e39c102 --- /dev/null +++ b/packages/ws-base/tests/fixtures/server/es6resolvers/socket/cause-error.js @@ -0,0 +1,9 @@ +// this method will throw an error + +/** + * @param {string} msg a message + * @return {string} a message but here we throw an error + */ +export default function causeError(msg) { + throw new Error(msg) +} diff --git a/packages/ws-base/tests/fixtures/server/es6resolvers/socket/chatroom.js b/packages/ws-base/tests/fixtures/server/es6resolvers/socket/chatroom.js new file mode 100644 index 0000000000000000000000000000000000000000..bb8495b09fc189538c420684cdc99417a75fdaad --- /dev/null +++ b/packages/ws-base/tests/fixtures/server/es6resolvers/socket/chatroom.js @@ -0,0 +1,12 @@ +// a private method + +/** + * + * @param {string} msg message + * @param {number} timestamp for checking the time + * @return {string} reply + */ +export default function chatroom(msg, timestamp) { + const d = Date.now() - timestamp; + return msg + ` took ${d} ms`; +} diff --git a/packages/ws-base/tests/fixtures/server/es6resolvers/socket/delay-fn.js b/packages/ws-base/tests/fixtures/server/es6resolvers/socket/delay-fn.js new file mode 100644 index 0000000000000000000000000000000000000000..a6a0e767fd5ea06f18a5af9f42d3a3e3cc05e4be --- /dev/null +++ b/packages/ws-base/tests/fixtures/server/es6resolvers/socket/delay-fn.js @@ -0,0 +1,15 @@ +// test this with a Promise return result +/** + * @param {string} msg a message + * @param {number} timestamp a timestamp + */ +export default function delayFn(msg, timestamp) { + // also test the global socket instance + delayFn.send = 'I am calling from delayFn'; + + return new Promise(resolver => { + setTimeout(() => { + resolver(msg + (Date.now() - timestamp)) + }, 1000) + }) +} diff --git a/packages/ws-base/tests/fixtures/server/es6resolvers/socket/private/secret-chatroom.js b/packages/ws-base/tests/fixtures/server/es6resolvers/socket/private/secret-chatroom.js new file mode 100644 index 0000000000000000000000000000000000000000..6b2b77bcd84934e352a95595a66f2e7c335d7097 --- /dev/null +++ b/packages/ws-base/tests/fixtures/server/es6resolvers/socket/private/secret-chatroom.js @@ -0,0 +1,12 @@ + +/** + * @param {string} room room name + * @param {*} msg message to that room + * @return {*} depends + */ +export default function secretChatroom(room, msg) { + + let userdata = secretChatroom.userdata; + // @TODO + return `send ${msg+''} to ${room} room from ${userdata.name}`; +} diff --git a/packages/ws-base/tests/fixtures/server/es6resolvers/socket/public/available-to-everyone/index.js b/packages/ws-base/tests/fixtures/server/es6resolvers/socket/public/available-to-everyone/index.js new file mode 100644 index 0000000000000000000000000000000000000000..9737a3b876c96c57dec9ddc974ed2ab94f6b0db6 --- /dev/null +++ b/packages/ws-base/tests/fixtures/server/es6resolvers/socket/public/available-to-everyone/index.js @@ -0,0 +1,9 @@ +// This method for testing the public call + +/** + * There is no parameter require for this call + * @return {string} a message + */ +export default function availableToEveryone() { + return 'You get a public message'; +} diff --git a/packages/ws-base/tests/fixtures/server/es6resolvers/socket/ws-handler.js b/packages/ws-base/tests/fixtures/server/es6resolvers/socket/ws-handler.js new file mode 100644 index 0000000000000000000000000000000000000000..37aeeedf16b9afd1de972c4b19c25c32727ada63 --- /dev/null +++ b/packages/ws-base/tests/fixtures/server/es6resolvers/socket/ws-handler.js @@ -0,0 +1,18 @@ +// will test this one with the send property + +/** + * method just for testing the ws + * @param {string} msg message + * @param {number} timestamp timestamp + * @return {string} msg + time lapsed + */ +export default function wsHandler(msg, timestamp) { + + wsHandler.send = 'I am sending a message back from ws'; + + return new Promise(resolver => { + setTimeout(() => { + resolver(msg + ' - ' +(Date.now() - timestamp)) + }, 1000) + }) +} diff --git a/packages/ws-base/tests/fixtures/server/fn.js b/packages/ws-base/tests/fixtures/server/fn.js new file mode 100644 index 0000000000000000000000000000000000000000..cba044c07299a81db376761b6481e274632f36e5 --- /dev/null +++ b/packages/ws-base/tests/fixtures/server/fn.js @@ -0,0 +1,10 @@ +const debug = require('debug')('jsonql-ws-server:test:object'); +/** + * @return {string} a msg + * + * + */ +module.exports = function myFn() { + // debug(myFn.ctx); + return myFn.ctx(); // 'This is a fn'; +} diff --git a/packages/ws-base/tests/fixtures/server/full-setup.js b/packages/ws-base/tests/fixtures/server/full-setup.js new file mode 100644 index 0000000000000000000000000000000000000000..efb15c7ccda63805619a088b80413b89f0b2b340 --- /dev/null +++ b/packages/ws-base/tests/fixtures/server/full-setup.js @@ -0,0 +1,34 @@ +// this one will provide all the configuration options including the contract +const { join } = require('path') +const fsx = require('fs-extra') +const { getDebug } = require('../../lib/share/helpers') +const debug = getDebug('test:full-setup') +const resolverDir = join(__dirname, 'resolvers') + +const wsServer = require('../../index') +const http = require('http') + +// start +const server = http.createServer(function(req, res) { + res.writeHead(200, { 'Content-Type': 'text/plain' }) + res.write('request successfully proxied!' + '\n' + JSON.stringify(req.headers, true, 2)) + res.end() +}) + +module.exports = function(_config = {}) { + const config = { + resolverDir, + serverType: 'socket.io' + } + const opts = Object.assign(config, _config) + return new Promise(resolver => { + wsServer(opts, server) + .then(io => { + debug('socket setup completed') + resolver({ + app: server, + io + }) + }) + }) +} diff --git a/packages/ws-base/tests/fixtures/server/keys/privateKey.pem b/packages/ws-base/tests/fixtures/server/keys/privateKey.pem new file mode 100644 index 0000000000000000000000000000000000000000..52ceae92066efead75838933d8bca25eeb9666ac --- /dev/null +++ b/packages/ws-base/tests/fixtures/server/keys/privateKey.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQDfDqpnh8TceIRuemm8GWM6nvE6KumK/Lq+POrZqghgHpZa5zjv +wwjsJ2iK45zWIRpggMkSlQZWvnRRjj/TWfv7448qhhiTB7hmqV63XjfYXJ5OgTtN +fPW36ZQ48Ha0y4sjlU4gvSijHpnzrJ5yV/vjLLLp9WxTux4ColeZu2B/XQIDAQAB +AoGAEmLJFQOR7IJamCiq8oA9N6XGSH8lBPnUAr5OtWZYjmO3DQMmJE01PRH6gghE +8zmDTRUQfeGexiOovtg01p0CMhehwS8D8d0m01s43zQ77xVJuFAvuW1U1kER4Xze +tVkLEvvO9PcWpKUEmxYpDoCJXGIfXuHaSAVbVLYDKn2MEUECQQD4FeWlpkxSNQT9 +u6w01zR/byjXzUmibOP5zrpaEsDGIxxTlxc/7WJZlKLNybXUyZE8oHgepuefdcL0 +ybk6gvQpAkEA5ixdMtnsbUImJYNFrt5BbLzEU9eF76hovsOSjOc2eTUJHEeiXeDA +Q66WZwXNBf/CRrZdsAvBPMQcWzJLwp24FQJBAPaojtPMLEXwAS5l0ioXblLlqq4l +pfigW2qcaBv2WUSm1BsoNi2RUB/Q8K26x9bxMj4dLlELkW+yHkxT5J6QZUECQFRO +A4TQlOwfwmETB77Y4RW2viIHWqNBB7x3XYIGXclfR4r4IdxIqaMgmy34zfNYjgvg +V8hXRdu/6LLuZRlPM1ECQHUppZNG7WKP9F7ywAr33u3xD0+9MqsfqcgKPfP9VOxs +Lo8EdmjmB30lyyP/Cd1hzxb+BsJjGmxzU/DikGbWos8= +-----END RSA PRIVATE KEY----- diff --git a/packages/ws-base/tests/fixtures/server/keys/publicKey.pem b/packages/ws-base/tests/fixtures/server/keys/publicKey.pem new file mode 100644 index 0000000000000000000000000000000000000000..7bd2532afbeb5b3be8b9c9f275ffac7b32456d0d --- /dev/null +++ b/packages/ws-base/tests/fixtures/server/keys/publicKey.pem @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDfDqpnh8TceIRuemm8GWM6nvE6 +KumK/Lq+POrZqghgHpZa5zjvwwjsJ2iK45zWIRpggMkSlQZWvnRRjj/TWfv7448q +hhiTB7hmqV63XjfYXJ5OgTtNfPW36ZQ48Ha0y4sjlU4gvSijHpnzrJ5yV/vjLLLp +9WxTux4ColeZu2B/XQIDAQAB +-----END PUBLIC KEY----- diff --git a/packages/ws-base/tests/fixtures/server/resolvers/auth/login.js b/packages/ws-base/tests/fixtures/server/resolvers/auth/login.js new file mode 100644 index 0000000000000000000000000000000000000000..8aa2524793c6eb6f3a470482bc8e9bdea2f26002 --- /dev/null +++ b/packages/ws-base/tests/fixtures/server/resolvers/auth/login.js @@ -0,0 +1,9 @@ + +/** + * create a login method for testing + * @param {string} username username + * @return {object} userdata + */ +module.exports = function login(username) { + return {name: username} +} diff --git a/packages/ws-base/tests/fixtures/server/resolvers/auth/logout.js b/packages/ws-base/tests/fixtures/server/resolvers/auth/logout.js new file mode 100644 index 0000000000000000000000000000000000000000..b5957eb2e26f4ac5a0cd0ede1e36847d5f1e6163 --- /dev/null +++ b/packages/ws-base/tests/fixtures/server/resolvers/auth/logout.js @@ -0,0 +1,7 @@ +/** + * testing the logout method call + * @return {boolean} true + */ +module.exports = function logout() { + return true; +} diff --git a/packages/ws-base/tests/fixtures/server/resolvers/socket/cause-error.js b/packages/ws-base/tests/fixtures/server/resolvers/socket/cause-error.js new file mode 100644 index 0000000000000000000000000000000000000000..6b9278c314ead5b4072cc81b76f0b502dee07e12 --- /dev/null +++ b/packages/ws-base/tests/fixtures/server/resolvers/socket/cause-error.js @@ -0,0 +1,9 @@ +// this method will throw an error + +/** + * @param {string} msg a message + * @return {string} a message but here we throw an error + */ +module.exports = function(msg) { + throw new Error(msg) +} diff --git a/packages/ws-base/tests/fixtures/server/resolvers/socket/chatroom.js b/packages/ws-base/tests/fixtures/server/resolvers/socket/chatroom.js new file mode 100644 index 0000000000000000000000000000000000000000..0b57676e1e3a1cd3ee583eaaf249a9accea26ce6 --- /dev/null +++ b/packages/ws-base/tests/fixtures/server/resolvers/socket/chatroom.js @@ -0,0 +1,12 @@ +// a private method + +/** + * + * @param {string} msg message + * @param {number} timestamp for checking the time + * @return {string} reply + */ +module.exports = function(msg, timestamp) { + const d = Date.now() - timestamp; + return msg + ` took ${d} ms`; +} diff --git a/packages/ws-base/tests/fixtures/server/resolvers/socket/delay-fn.js b/packages/ws-base/tests/fixtures/server/resolvers/socket/delay-fn.js new file mode 100644 index 0000000000000000000000000000000000000000..523ed5c1640af604a093213806dcdad5da0f1422 --- /dev/null +++ b/packages/ws-base/tests/fixtures/server/resolvers/socket/delay-fn.js @@ -0,0 +1,15 @@ +// test this with a Promise return result +/** + * @param {string} msg a message + * @param {number} timestamp a timestamp + */ +module.exports = function delayFn(msg, timestamp) { + // also test the global socket instance + delayFn.send = 'I am calling from delayFn'; + + return new Promise(resolver => { + setTimeout(() => { + resolver(msg + (Date.now() - timestamp)) + }, 1000) + }) +} diff --git a/packages/ws-base/tests/fixtures/server/resolvers/socket/private/secret-chatroom.js b/packages/ws-base/tests/fixtures/server/resolvers/socket/private/secret-chatroom.js new file mode 100644 index 0000000000000000000000000000000000000000..a8dfc2f66546046675d85f67bf0877e557d9d8e9 --- /dev/null +++ b/packages/ws-base/tests/fixtures/server/resolvers/socket/private/secret-chatroom.js @@ -0,0 +1,12 @@ + +/** + * @param {string} room room name + * @param {*} msg message to that room + * @return {*} depends + */ +module.exports = function secretChatroom(room, msg) { + + let userdata = secretChatroom.userdata; + // @TODO + return `send ${msg+''} to ${room} room from ${userdata.name}`; +} diff --git a/packages/ws-base/tests/fixtures/server/resolvers/socket/public/available-to-everyone/index.js b/packages/ws-base/tests/fixtures/server/resolvers/socket/public/available-to-everyone/index.js new file mode 100644 index 0000000000000000000000000000000000000000..16b326f970dc769884dda13e36b4df9f3f924166 --- /dev/null +++ b/packages/ws-base/tests/fixtures/server/resolvers/socket/public/available-to-everyone/index.js @@ -0,0 +1,9 @@ +// This method for testing the public call + +/** + * There is no parameter require for this call + * @return {string} a message + */ +module.exports = function availableToEveryone() { + return 'You get a public message'; +} diff --git a/packages/ws-base/tests/fixtures/server/resolvers/socket/ws-handler.js b/packages/ws-base/tests/fixtures/server/resolvers/socket/ws-handler.js new file mode 100644 index 0000000000000000000000000000000000000000..0a6a10e0c4b96c40a363157e05e6b26622d325ff --- /dev/null +++ b/packages/ws-base/tests/fixtures/server/resolvers/socket/ws-handler.js @@ -0,0 +1,18 @@ +// will test this one with the send property + +/** + * method just for testing the ws + * @param {string} msg message + * @param {number} timestamp timestamp + * @return {string} msg + time lapsed + */ +module.exports = function wsHandler(msg, timestamp) { + + wsHandler.send = 'I am sending a message back from ws'; + + return new Promise(resolver => { + setTimeout(() => { + resolver(msg + ' - ' +(Date.now() - timestamp)) + }, 1000) + }) +} diff --git a/packages/ws-base/tests/fixtures/server/server.js b/packages/ws-base/tests/fixtures/server/server.js new file mode 100644 index 0000000000000000000000000000000000000000..28997b958f06b7e42b8f519e64c4422e57781eb1 --- /dev/null +++ b/packages/ws-base/tests/fixtures/server/server.js @@ -0,0 +1,38 @@ +const http = require('http') +const fsx = require('fs-extra') +const debug = require('debug')('jsonql-ws-server:fixtures:server') +const fs = require('fs') +const { join } = require('path') +const { JSONQL_PATH } = require('jsonql-constants') + +const resolverDir = join(__dirname, 'resolvers') +const contractDir = join(__dirname, 'contract') + +const wsServer = require('../../index') + +// start +const server = http.createServer(function(req, res) { + res.writeHead(200, { 'Content-Type': 'text/plain' }) + res.write('request successfully proxied!' + '\n' + JSON.stringify(req.headers, true, 2)) + res.end() +}) + +module.exports = function(extra = {}) { + // extra.contract = extra.contract || contract; + return new Promise(resolver => { + wsServer( + Object.assign({ + resolverDir, + contractDir, + serverType: 'ws' + }, extra), + server + ) + .then(io => { + resolver({ + io, + app: server + }) + }) + }) +} diff --git a/packages/ws-base/tests/fixtures/server/token.js b/packages/ws-base/tests/fixtures/server/token.js new file mode 100644 index 0000000000000000000000000000000000000000..1382f1954f3a8ccd388345518f50e14323cf6b26 --- /dev/null +++ b/packages/ws-base/tests/fixtures/server/token.js @@ -0,0 +1,15 @@ +// generate token for use for auth +const { join } = require('path') +const fsx = require('fs-extra') +const privateKey = fsx.readFileSync(join(__dirname, 'keys', 'privateKey.pem')) +const { jwtRsaToken, jwtToken } = require('jsonql-jwt') +const { HSA_ALGO } = require('jsonql-constants') + +module.exports = function(payload, key = false) { + if (key === false) { + return jwtRsaToken(payload, privateKey) + } + return jwtToken(payload, key, { + algorithm: HSA_ALGO + }) +} diff --git a/packages/ws-base/tests/object.test.js b/packages/ws-base/tests/object.test.js new file mode 100644 index 0000000000000000000000000000000000000000..d1b817572a4181481d3a8a227ef44117e83803de --- /dev/null +++ b/packages/ws-base/tests/object.test.js @@ -0,0 +1,20 @@ +// this is a standalone test to try the Object.defineProperty +const test = require('ava') + +test.before( t => { + const fn = require('./fixtures/fn'); + Object.defineProperty(fn, 'ctx', { + value: function() { + return 'I am ctx'; + } + }) + t.context.fn = fn; +}) + +test("I should able to defined an object property on a anonymous function", t => { + let fn1 = t.context.fn; + const msg = fn1() + + // t.is('This is a fn', msg); + t.is('I am ctx', msg) +}) diff --git a/packages/ws-base/tests/socket-io-handshake.test.js b/packages/ws-base/tests/socket-io-handshake.test.js new file mode 100644 index 0000000000000000000000000000000000000000..d26829f93d1e07b7559c389d51244f1f43df0015 --- /dev/null +++ b/packages/ws-base/tests/socket-io-handshake.test.js @@ -0,0 +1,88 @@ +// Testing with jwt method +const debug = require('debug')('jsonql-ws-server:test:socket.io') +const { join } = require('path') +const fsx = require('fs-extra') +const test = require('ava') + +// const socketIoClient = require('socket.io-client') + +const { JSONQL_PATH, RSA_ALGO } = require('jsonql-constants') +const socketIo = require('socket.io') +const { socketIoNodeHandshakeLogin, socketIoNodeClient } = require('jsonql-jwt') + +const createToken = require('./fixtures/token') + +const contractDir = join(__dirname, 'fixtures', 'contract', 'auth') +const contractFile = join(contractDir, 'contract.json') +const contract = fsx.readJsonSync(contractFile) +const keysDir = join(__dirname, 'fixtures', 'keys') +const publicKey = fsx.readFileSync(join(keysDir, 'publicKey.pem')) + +const setup = require('./fixtures/full-setup') +const port = 8899; +const payload = {name: 'Joel'}; +const msg = 'Hello there!'; + +test.before(async t => { + const { app, io } = await setup({ + contractDir, + contract, + keysDir, + privateMethodDir: 'private', + enableAuth: true + }) + + t.context.io = io; + + t.context.token = createToken(payload) + + t.context.baseUrl = `ws://localhost:${port}/${JSONQL_PATH}/`; + + t.context.server = app.listen(port) +}); + +test.after(t => { + t.context.server.close() +}) + +// private + + +test.cb("It should able to connect to socket.io private namespace with a token", t => { + t.plan(2); + + t.truthy(t.context.io[[JSONQL_PATH, 'private'].join('/')]) + + socketIoNodeHandshakeLogin(t.context.baseUrl + 'private', t.context.token) + .then(client => { + debug('connection to private established') + client.emit('secretChatroom', {args: ['back', 'stuff']} , reply => { + debug(reply) + t.truthy(reply) + t.end() + }) + }) + .catch(err => { + debug('died!', err) + t.fail() + }) + +}) + +// public +test.cb('It should able to connect to a socket.io public namespace', t => { + + let client_public = socketIoNodeClient(t.context.baseUrl + 'public') + + t.plan(2) + + t.truthy(t.context.io[ [JSONQL_PATH, 'public'].join('/') ]) + + client_public.on('connect', () => { + client_public.emit('availableToEveryone', {args: []} , reply => { + debug('reply', reply) + t.truthy(reply.data) + t.end() + }) + }) +}) diff --git a/packages/ws-base/tests/socket-io-roundtrip.test.js b/packages/ws-base/tests/socket-io-roundtrip.test.js new file mode 100644 index 0000000000000000000000000000000000000000..899f7193e2001ca2b0861fc1e628d5e33653aae2 --- /dev/null +++ b/packages/ws-base/tests/socket-io-roundtrip.test.js @@ -0,0 +1,84 @@ +// Testing with jwt method +const debug = require('debug')('jsonql-ws-server:test:socket.io') +const { join } = require('path') +const fsx = require('fs-extra') +const test = require('ava') +const socketIoClient = require('socket.io-client') +const { JSONQL_PATH, RSA_ALGO } = require('jsonql-constants') +const socketIo = require('socket.io') +const { socketIoNodeRoundtripLogin, socketIoNodeClient } = require('jsonql-jwt') +const createToken = require('./fixtures/token') +const secret = '12345678'; + +const contractDir = join(__dirname, 'fixtures', 'contract', 'auth') +const contractFile = join(contractDir, 'contract.json') +const contract = fsx.readJsonSync(contractFile) +const keysDir = join(__dirname, 'fixtures', 'keys') +const publicKey = fsx.readFileSync(join(keysDir, 'publicKey.pem')) +const setup = require('./fixtures/full-setup') + +const port = 8888; +const payload = {name: 'Joel'}; +const msg = 'Hello there!'; + +test.before(async t => { + const { app, io } = await setup({ + contractDir, + contract, + useJwt: secret, + privateMethodDir: 'private', + enableAuth: true + }) + t.context.io = io; + + t.context.baseUrl = `ws://localhost:${port}/${JSONQL_PATH}/`; + + t.context.server = app.listen(port) + + t.context.token = createToken(payload, secret) + +}); + +test.after(t => { + t.context.server.close() +}) + +// private +test.cb("It should able to connect to socket.io private namespace with a token", t => { + t.plan(2); + + t.truthy(t.context.io[[JSONQL_PATH, 'private'].join('/')]) + + debug('token:', t.context.token) + + socketIoNodeRoundtripLogin(t.context.baseUrl + 'private', t.context.token) + .then(client => { + client.emit('secretChatroom', {args: ['back', 'stuff']} , reply => { + debug(reply) + t.truthy(reply) + t.end() + }) + }) + .catch(err => { + debug('died!', err) + t.fail() + }) +}) + +// public +test.cb('It should able to connect to a socket.io public namespace', t => { + + t.plan(2) + + let client_public = socketIoNodeClient(t.context.baseUrl + 'public') + + t.truthy(t.context.io[ [JSONQL_PATH, 'public'].join('/') ]) + + client_public.on('connect', () => { + client_public.emit('availableToEveryone', {args: []} , reply => { + debug('reply', reply) + t.truthy(reply.data) + t.end() + }) + }) +}) diff --git a/packages/ws-base/tests/socket-io.test.js b/packages/ws-base/tests/socket-io.test.js new file mode 100644 index 0000000000000000000000000000000000000000..612c9818727f2883e7f77d08f6e2886da77954b3 --- /dev/null +++ b/packages/ws-base/tests/socket-io.test.js @@ -0,0 +1,70 @@ +// now test the socket.io set up +const debug = require('debug')('jsonql-ws-server:test:socket.io') +const { join } = require('path') +const fsx = require('fs-extra') +const test = require('ava') +const client = require('socket.io-client') + +const { JSONQL_PATH } = require('jsonql-constants') + +const contractDir = join(__dirname, 'fixtures', 'contract') +const contractFile = join(contractDir, 'contract.json') +const contract = fsx.readJsonSync(contractFile) + +const setup = require('./fixtures/full-setup') +const port = 8878; + +const msg = 'Hello there!'; + +test.before(async t => { + const { app, io } = await setup({ + contractDir, + contract + }) + + t.context.server = app.listen(port) + t.context.io = io; + + const ioClient = client.connect(`http://localhost:${port}/${JSONQL_PATH}`) + t.context.client = ioClient; +}) + +test.after(t => { + t.context.server.close() +}) + +test.cb('It should able to connect to a socket.io server namespace', t => { + t.plan(2) + t.truthy(t.context.io[JSONQL_PATH]) + // the whole on.connect thing never f**king work! + t.context.client.emit('chatroom', {args: [msg, Date.now()]}, function(result) { + // debug(result); + t.truthy(result.data.indexOf(msg) > -1) + t.end() + }) +}) + +test.cb("It should able to call delayFn even the result will be delay for one second", t => { + t.plan(2); + let msg1 = 'Waiting'; + t.context.client.on('delayFn', result => { + t.truthy(result.data, 'Should able to received message send from resolver'); + }) + + t.context.client.emit('delayFn', {args: [msg1, Date.now()]}, result => { + // debug(result); + t.truthy(result.data.indexOf(msg1) > -1, 'should get result callback') + t.end() + }) +}) + +test.cb("This will cause an error and we should able to get the error", t => { + t.plan(2) + t.context.client.emit('causeError', {args: ['whatever']}, result => { + /// debug(result); + t.truthy(result.error) + t.is('JsonqlResolverAppError', result.error.className) + t.end() + }) + +}) diff --git a/packages/ws-base/tests/ws-connect-es6.test.js b/packages/ws-base/tests/ws-connect-es6.test.js new file mode 100644 index 0000000000000000000000000000000000000000..395e9ca0451905999c8a63139ed47ec8fe985b34 --- /dev/null +++ b/packages/ws-base/tests/ws-connect-es6.test.js @@ -0,0 +1,96 @@ +// test the basic connection and such +const test = require('ava') +// const WebSocket = require('ws') +const { join } = require('path') +const fsx = require('fs-extra') + +const { wsNodeClient } = require('jsonql-jwt') + +const wsServer = require('./fixtures/server') +const { JSONQL_PATH, ERROR_TYPE } = require('jsonql-constants') +const debug = require('debug')('jsonql-ws-server:test:ws') + +const contractDir = join(__dirname, 'fixtures', 'contract', 'es6') +const resolverDir = join(__dirname, 'fixtures', 'es6resolvers') +const contractFile = join(contractDir, 'contract.json') +const contract = fsx.readJsonSync(contractFile) + +const { extractWsPayload } = require('../lib/share/helpers') +const createPayload = require('./fixtures/create-payload') +const port = 8898; +const msg = 'Something'; +let ctn = 0; + +test.before(async t => { + const { app, io } = await wsServer({ + contractDir, + resolverDir, + contract + }) + t.context.io = io; + t.context.server = app; + t.context.server.listen(port) + + t.context.client = wsNodeClient(`ws://localhost:${port}/${JSONQL_PATH}`) +}); + +test.after(t => { + t.context.server.close() +}); + +test.cb('Grouping all test together as one because the way ws reponse to on.message', t => { + t.plan(6); + // connect to the server + let client = t.context.client; + + t.is(true , t.context.io[JSONQL_PATH] !== undefined) + + client.on('open', () => { + + client.send( createPayload('wsHandler', msg + ' 1st', Date.now()) ) + + client.send( createPayload('chatroom', msg + ' 2nd', Date.now()) ) + + setTimeout(() => { + client.send( createPayload('causeError', msg + ' 3nd') ) + }, 500) + + }) + + // wait for reply + client.on('message', (data) => { + let json; + try { + json = extractWsPayload(data) + debug('on message received: ', json) + switch (json.resolverName) { + case 'wsHandler': + t.truthy(json.data, 'wsHandler should reply with a message') + break; + case 'chatroom': + t.truthy(json.data, 'chatroom should reply with a message') + break; + default: + debug('catch error here %O', json) + let { data } = json; + + t.truthy(data.error, 'causeError should have error field') + t.is(json.type, ERROR_TYPE) + t.true(data.error.className === 'JsonqlResolverAppError' && data.error.message === 'causeError') + + t.end() + } + } catch(e) { + + debug('error', e) + /* + let { data } = error; + t.true(data.error.className === 'JsonqlResolverAppError' && data.error.message === 'causeError') + t.is(data.error.type, ERROR_TYPE) + t.truthy(data.error, 'causeError should have error field') + */ + + t.end() + } + }); +}); diff --git a/packages/ws-base/tests/ws-connect.test.js b/packages/ws-base/tests/ws-connect.test.js new file mode 100644 index 0000000000000000000000000000000000000000..f44d437ec1204b914a992de8afd8f58b6a7a0fe9 --- /dev/null +++ b/packages/ws-base/tests/ws-connect.test.js @@ -0,0 +1,94 @@ +// test the basic connection and such +const test = require('ava') +// const WebSocket = require('ws') +const { join } = require('path') +const fsx = require('fs-extra') + +const { wsNodeClient } = require('jsonql-jwt') + +const wsServer = require('./fixtures/server') +const { JSONQL_PATH, ERROR_TYPE } = require('jsonql-constants') +const debug = require('debug')('jsonql-ws-server:test:ws') + +const contractDir = join(__dirname, 'fixtures', 'contract') +const contractFile = join(contractDir, 'contract.json'); +const contract = fsx.readJsonSync(contractFile); + +const { extractWsPayload } = require('../lib/share/helpers') +const createPayload = require('./fixtures/create-payload') +const port = 8898; +const msg = 'Something'; +let ctn = 0; + +test.before(async t => { + const { app, io } = await wsServer({ + contractDir, + contract + }) + t.context.io = io; + t.context.server = app; + t.context.server.listen(port) + + t.context.client = wsNodeClient(`ws://localhost:${port}/${JSONQL_PATH}`) +}); + +test.after(t => { + t.context.server.close() +}); + +test.cb('Grouping all test together as one because the way ws reponse to on.message', t => { + t.plan(6); + // connect to the server + let client = t.context.client; + + t.is(true , t.context.io[JSONQL_PATH] !== undefined) + + client.on('open', () => { + + client.send( createPayload('wsHandler', msg + ' 1st', Date.now()) ) + + client.send( createPayload('chatroom', msg + ' 2nd', Date.now()) ) + + setTimeout(() => { + client.send( createPayload('causeError', msg + ' 3nd') ) + }, 500) + + }) + + // wait for reply + client.on('message', (data) => { + let json; + try { + json = extractWsPayload(data) + debug('on message received: ', json) + switch (json.resolverName) { + case 'wsHandler': + t.truthy(json.data, 'wsHandler should reply with a message') + break; + case 'chatroom': + t.truthy(json.data, 'chatroom should reply with a message') + break; + default: + debug('catch error here %O', json) + let { data } = json; + + t.truthy(data.error, 'causeError should have error field') + t.is(json.type, ERROR_TYPE) + t.true(data.error.className === 'JsonqlResolverAppError' && data.error.message === 'causeError') + + t.end() + } + } catch(e) { + + debug('error', e) + /* + let { data } = error; + t.true(data.error.className === 'JsonqlResolverAppError' && data.error.message === 'causeError') + t.is(data.error.type, ERROR_TYPE) + t.truthy(data.error, 'causeError should have error field') + */ + + t.end() + } + }); +}); diff --git a/packages/ws-base/tests/ws-jwt-auth.test.js b/packages/ws-base/tests/ws-jwt-auth.test.js new file mode 100644 index 0000000000000000000000000000000000000000..836d2583301659c048c50483fbd867829c1a0ba8 --- /dev/null +++ b/packages/ws-base/tests/ws-jwt-auth.test.js @@ -0,0 +1,86 @@ +const test = require('ava') +const { join } = require('path') +const fsx = require('fs-extra') +// this will get the umd version of the client module +const { wsNodeAuthClient, wsNodeClient, decodeToken } = require('jsonql-jwt') +// const WebSocket = require('ws') +const { extractWsPayload } = require('../lib/share/helpers') + +const debug = require('debug')('jsonql-ws-server:test:jwt-auth') +const { JSONQL_PATH } = require('jsonql-constants') +const contractDir = join(__dirname, 'fixtures', 'contract', 'auth') +const contract = fsx.readJsonSync(join(contractDir, 'contract.json')) +const serverSetup = require('./fixtures/server') +const createToken = require('./fixtures/token') +const createPayload = require('./fixtures/create-payload') + +const payload = {name: 'Joel'}; +const port = 3003; + +test.before(async t => { + const { app, io } = await serverSetup({ + contract, + contractDir, + privateMethodDir: 'private', + enableAuth: true, + keysDir: join(__dirname, 'fixtures', 'keys') + }) + + t.context.token = createToken(payload) + + t.context.io = io; + t.context.app = app; + + t.context.app.listen(port) + + const baseUrl = `ws://localhost:${port}/${JSONQL_PATH}/`; + + t.context.client_public = wsNodeClient(baseUrl + 'public') + + t.context.client_private = wsNodeAuthClient(baseUrl + 'private', t.context.token) + +}) + +test.after(t => { + t.context.app.close() +}) + +test.cb('It should able to connect to public namespace without a token', t => { + // connect to the private channel + t.plan(2) + + let client = t.context.client_public; + + t.truthy(t.context.io[ [JSONQL_PATH, 'public'].join('/') ]) + + client.on('open', () => { + client.send( createPayload('availableToEveryone') ) + }) + + client.on('message', data => { + let json = extractWsPayload(data); + // debug('reply', json) + t.truthy(json.data) + t.end() + }) + +}) + +test.cb('It should able to connect to the private namespace', t => { + t.plan(2) + let client = t.context.client_private; + + t.truthy(t.context.io[[JSONQL_PATH, 'private'].join('/')]) + + client.on('open', () => { + client.send( createPayload('secretChatroom', 'back', 'stuff') ) + }) + + client.on('message', data => { + let json = extractWsPayload(data); + t.truthy(json.data) + debug(json) + t.end() + }) + +}) diff --git a/packages/ws-client/README.md b/packages/ws-client/README.md index 982f95df9bb8915e8fef8c24cf8d35f2cac68cd2..08154dc73dae37a28ac7b4745701baa7a3345640 100644 --- a/packages/ws-client/README.md +++ b/packages/ws-client/README.md @@ -1,15 +1,15 @@ # jsonql-ws-client -> This is the jsonql websocket helper library, not for direct use. +**THIS PACKAGE IS NOW DEPRECATED** We have break up all the jsonql socket client / server in several modules - @jsonql/ws for WebSocket client / server - @jsonql/socketio for Socket.io client / server -- @jsonql/primus for Primus clinet / server +- @jsonql/primus for Primus client / server -Please check [jsonql.js.org](https://jsonql.js.org) for further information +Please check [jsonql.org](https://jsonql.js.org) for further information --- diff --git a/packages/ws-client/package.json b/packages/ws-client/package.json index 23873ae99e1e34b227ae175b6973cc51446313a8..18f1ec01155a26dd1eca5e49334ec1f38f331cf5 100755 --- a/packages/ws-client/package.json +++ b/packages/ws-client/package.json @@ -1,6 +1,6 @@ { "name": "jsonql-ws-client", - "version": "1.0.0-beta.3", + "version": "1.0.0", "description": "This is the web socket client helper library", "main": "main.js", "module": "index.js", @@ -12,6 +12,7 @@ "main.js" ], "scripts": { + "deprecated": "npm deprecate jsonql-ws-client \"This module is no longer maintain, please move to the new @jsonql/socketio or @jsonql/ws modules\" and check https://jsonql.js.org for up to date version", "test": "DEBUG=jsonql-ws* ava --verbose", "_prepare_": "npm run build", "build": "npm run build:browser && npm run build:cjs", @@ -30,11 +31,6 @@ ], "author": "Joel Chu ", "license": "MIT", - "devDependencies": { - "ava": "^2.3.0", - "fs-extra": "^8.1.0", - "kefir": "^3.8.6" - }, "ava": { "files": [ "tests/*.test.js", @@ -57,15 +53,24 @@ }, "dependencies": { "esm": "^3.2.25", - "jsonql-constants": "^1.8.0", + "jsonql-constants": "^1.8.2", "jsonql-errors": "^1.1.2", "jsonql-jwt": "^1.3.0", "jsonql-params-validator": "^1.4.6", - "jsonql-utils": "^0.4.0", + "jsonql-utils": "^0.4.3", "nb-event-service": "^1.8.3" }, + "devDependencies": { + "ava": "^2.3.0", + "fs-extra": "^8.1.0", + "kefir": "^3.8.6" + }, "repository": { "type": "git", "url": "git+ssh://git@gitee.com:to1source/jsonql.git" + }, + "homepage": "jsonql.org", + "bugs": { + "url": "https://gitee.com/to1source/jsonql/issues" } } diff --git a/packages/ws-server/README.md b/packages/ws-server/README.md index b554c7a1f9377a2bb3a375123291f30647dc31cb..81672c821eccd3accfb1022cfb566a51bfd01f89 100644 --- a/packages/ws-server/README.md +++ b/packages/ws-server/README.md @@ -1,81 +1,18 @@ # jsonql-ws-server -> Setup WebSocket / Socket.io server for the jsonql to run on the same host, automatic generate public / private channel using contract +**THIS PACKAGE IS NOW DEPRECATED** -This module will create socket connection with [jsonql-koa](https://www.npmjs.com/package/jsonql-koa) -under the same host. +We have break up the packages into several different one -Current it supports +- @jsonql/ws +- @jsonql/socketio +- @jsonql/primus -- [socket.io](https://socket.io/) -- [ws](https://github.com/websockets/ws) - - -## Installation - -```sh -$ npm i jsonql-ws-server -``` - -## Example - -Due to the architect of Koa, this can not simply create as a middleware. Therefore this need to init after -the Koa app has been inited. Also this require the `http.createServer` to pass the server - -**To start with socket.io** - -```js -const jsonqlWsServer = require('jsonql-ws-server') -const Koa = require('koa') -const http = require('http') -// bunch of init -const server = http.createServer(app.callback()) - -const io = jsonqlWsServer({ - serverType: 'socket.io', - options: {}, - server -}) -``` -~~Once this init is done, you will get a `global.WEB_SOCKET_SERVER` with two properties inside:~~ - -```js - -// !!! THIS HAS CHANGED, await for updated version of README !!! - -{ - "serverType": "socket.io", - "server": io -} - -``` - -Now by default if you didn't pass the `namespace` in the options. It will always create one for you using the - -```js -const { JSONQL_PATH } = require('jsonql-constants') -``` - -Then you can use it like so - -```js -// THIS HAS CHANGED AWAITING for updated version of README - -const socket = io.server[JSONQL_PATH] - -socket.on('connection', function(ctx) { - // do your thing -}); - -``` - -But in reality you don't really use it like that, because [jsonql-ws-client](https://www.npmjs.com/package/jsonql-ws-client) will use that return and construct the -usable client for you, hence the return of the `serverType` key. +Please check our main documentation site at [jsonql.org](https://jsonql.js.org) +for further information --- -MIT - -[Joel Chu](https://joelchu.com) +[NEWBRAN LTD UK](https://newbran.ch) -[NEWBRAN LTD](https://newbran.ch) / [to1source CN](https://to1source.cn) (c) 2019 +[TO1SOURCE CHINA](https://to1source.cn) diff --git a/packages/ws-server/package.json b/packages/ws-server/package.json index 8e525d6a1ea72c346681596cfa209fcc168a61a3..369b5b0cfe9af6a8a6536e03fe76f79093ab9558 100755 --- a/packages/ws-server/package.json +++ b/packages/ws-server/package.json @@ -1,6 +1,6 @@ { "name": "jsonql-ws-server", - "version": "1.2.0", + "version": "1.2.1", "description": "Setup WebSocket / Socket.io server for the jsonql to run on the same host, automatic generate public / private channel using contract", "main": "index.js", "files": [ @@ -8,6 +8,7 @@ "lib" ], "scripts": { + "deprecated": "npm deprecate jsonql-ws-server \"This module is no longer maintain, please move to the new @jsonql/socketio or @jsonql/ws modules\"", "test": "DEBUG=jsonql-ws-server* ava --verbose", "test:ws": "DEBUG=jsonql-ws-server* ava ./tests/ws-connect.test.js", "test:es6": "DEBUG=jsonql-ws-server* ava ./tests/ws-connect-es6.test.js", @@ -25,6 +26,7 @@ "jsonql", "Websocket" ], + "homepage": "jsonql.org", "author": "Joel Chu ", "license": "MIT", "dependencies": { diff --git a/public/index.html b/public/index.html deleted file mode 100755 index 39036829512dbf89e5030aa2774e8e66b5ea465e..0000000000000000000000000000000000000000 --- a/public/index.html +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - - - - - - - -
    - - - - - - -
    {json:ql} - -
    -
    -
    - -
    -
    -
    -
    - {json:ql} logo -
    -
    - json:ql is a high velocity framework that connect client and server. - The goal is to have the resolver (query or mutation) written once on the server (provider) - side, then the client (consumer) will able to call the services without needing to write any code. -
    -
    - - - - -