├── .circleci └── config.yml ├── .dockerignore ├── .editorconfig ├── .eslintrc.json ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── custom.md │ └── feature_request.md ├── .gitignore ├── .npmrc ├── .nvmrc ├── .verb.md ├── Dockerfile.prod ├── Dockerfile.test ├── LICENSE ├── Makefile ├── README.md ├── docker-compose.deps.yml ├── docker-compose.dev.yml ├── docker-compose.test.yml ├── docker-compose.yml ├── docs ├── about-graphiql.md ├── about-graphql-qix.md ├── about-graphql.md ├── about.md ├── images │ ├── graphiql-doc-account-table.png │ ├── graphiql-doc-docs.png │ ├── graphiql-documentation.png │ ├── graphiql-example-nodejs.png │ ├── graphiql-exploration.png │ ├── graphiql-global-docs.png │ ├── graphiql-global.png │ ├── graphiql-intellisense.png │ ├── header.png │ ├── header.psd │ ├── logo.png │ └── logo.psd ├── inc │ ├── author.md │ ├── contributing.md │ ├── features.md │ ├── getting-started.md │ ├── install.md │ ├── motivation.md │ ├── no-idea.md │ └── roadmap.md ├── questions.md ├── resources.md ├── scope-doc.md ├── scope-global.md ├── todos.md └── z-usage.md ├── examples ├── README.md ├── node-js │ ├── Dockerfile │ ├── README.md │ ├── index.js │ ├── package.json │ └── test.spec.js └── test │ └── build-tests.sh ├── nodemon-docs.json ├── package.json ├── renovate.json ├── src ├── app-server.js ├── config │ └── config.js ├── index.js ├── initializers │ └── routes.js ├── lib │ ├── lib.js │ ├── qix-lib.js │ └── schema-cache.js └── modules │ ├── api-docs │ ├── api-docs.routes.js │ └── api-docs.yml │ ├── doc │ ├── doc-schema-generator.js │ ├── doc.resolvers.js │ ├── doc.routes.js │ └── doc.schema.js │ ├── global │ ├── global.resolvers.js │ ├── global.routes.js │ └── global.schema.js │ └── health-check │ ├── health-check.controller.js │ └── health-check.routes.js └── test ├── .eslintrc.json ├── fixtures └── TablesAndKeys-CRM.json ├── integration ├── 00-api-docs.specs.js ├── 00-app-server.spec.js ├── 00b-app-server.config.spec.js ├── 00c-app-server.routes.spec.js ├── 01-health-check.spec.js ├── 02-global-scope.spec.js ├── 03-qix-lib.spec.js ├── 04-doc-scope.spec.js ├── 05-qix-resolver.spec.js └── enigma.spec.js ├── mocha.conf.js └── unit ├── graphql-generator.spec.js ├── lib.spec.js └── schema-cache.spec.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: ~/qix-graphql 5 | machine: true 6 | environment: 7 | SERVICE_NAME: qix-graphql 8 | DOCKER_REPO: stefanwalther/qix-graphql 9 | DOCKER_URL: https://download.docker.com/linux/static/edge/x86_64/docker-18.02.0-ce.tgz 10 | DOCKER_COMPOSE_URL: https://github.com/docker/compose/releases/download/1.19.0/docker-compose-Linux-x86_64 11 | steps: 12 | - checkout 13 | - add_ssh_keys 14 | - run: 15 | name: Upgrade Docker 16 | command: | 17 | set -e 18 | curl -sSL $DOCKER_URL | sudo tar -xz --strip-components=1 -C /usr/bin/ 19 | sudo service docker restart 20 | - run: 21 | name: Login to Docker 22 | command: docker login -u "$DOCKER_USER" --password "$DOCKER_PASS" 23 | - run: 24 | name: Pull public docker images in the background 25 | background: true 26 | command: | 27 | docker pull node:8.10.0-alpine 28 | - run: 29 | name: Upgrade Docker Compose 30 | command: | 31 | set -e 32 | loc=$(which docker-compose) 33 | sudo curl -sSLf -z $loc -o $loc $DOCKER_COMPOSE_URL 34 | sudo chmod 755 $loc 35 | - run: 36 | name: Update version 37 | command: | 38 | set -e 39 | VER=$(node -e "console.log(require('./package.json').version.replace(/-[0-9]+/, '-$CIRCLE_BUILD_NUM'))") 40 | echo "$VER" > ./version.txt 41 | if (echo $VER | egrep -- '-[0-9]+$' 1> /dev/null); then 42 | npm version $VER --no-git-tag-version 43 | fi 44 | - run: 45 | name: Preparations 46 | command: mkdir -p ./coverage 47 | - run: 48 | name: Build image 49 | command: make build 50 | - run: 51 | name: Build test image 52 | command: make build-test 53 | # - run: 54 | # name: Setting up test environment 55 | # command: make up-test 56 | # - run: 57 | # name: Lint 58 | # command: docker-compose --f=docker-compose.unit-tests.yml run auth-service-test npm run lint 59 | # - run: 60 | # name: Unit tests 61 | # command: make run-unit-tests 62 | - run: 63 | name: Run Tests 64 | command: make run-test 65 | - run: 66 | name: CodeCov (upload unit tests) 67 | command: bash <(curl -s https://codecov.io/bash) -t $(CODECOV_TOKEN) 68 | - deploy: 69 | name: Push image to Docker Hub 70 | command: | 71 | VER=$(cat ./version.txt) 72 | # Only branch "master" is being pushed to Docker Hub 73 | if [ "${CIRCLE_BRANCH}" == "master" ]; then 74 | docker push $DOCKER_REPO 75 | fi 76 | 77 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | end_of_line = lf 7 | charset = utf-8 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | insert_final_newline = false 15 | 16 | [**/{actual,fixtures,expected}/**] 17 | trim_trailing_whitespace = false 18 | insert_final_newline = false 19 | 20 | [**/templates/**] 21 | trim_trailing_whitespace = false 22 | insert_final_newline = false 23 | 24 | [{Makefile,**.mk}] 25 | # Use tabs for indentation (Makefiles require tabs) 26 | indent_style = tab 27 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-space-single/esnext", 3 | "rules": { 4 | "no-warning-comments": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Smartphone (please complete the following information):** 29 | - Device: [e.g. iPhone6] 30 | - OS: [e.g. iOS8.1] 31 | - Browser [e.g. stock browser, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | 5 | --- 6 | 7 | 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ========================= 2 | # Project specific 3 | # ========================= 4 | 5 | # ========================= 6 | # IDEs 7 | # ========================= 8 | 9 | # IntelliJ / Webstorm 10 | .idea/ 11 | 12 | # Visual Code 13 | .vscode/ 14 | 15 | # ========================= 16 | # node.js 17 | # ========================= 18 | node_modules 19 | 20 | # ========================= 21 | # npm 22 | # ========================= 23 | package-lock.json 24 | npm-debug.log 25 | */.npm 26 | 27 | # ========================= 28 | # Testing 29 | # ========================= 30 | coverage 31 | .nyc_output 32 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v8.6.0 2 | -------------------------------------------------------------------------------- /.verb.md: -------------------------------------------------------------------------------- 1 | # {%=name%} 2 | 3 | > {%=description%} 4 | 5 | {%= badge('npm') %} 6 | [![David](https://img.shields.io/david/stefanwalther/{%=name%}.svg)](https://github.com/stefanwalther/{%=name%}) 7 | [![Codecov](https://img.shields.io/codecov/c/github/stefanwalther/{%=name%}.svg?logo=codecov)](https://codecov.io/gh/stefanwalther/{%=name%}) 8 | [![CircleCI](https://img.shields.io/circleci/project/github/stefanwalther/{%=name%}.svg?logo=circleci)](https://circleci.com/gh/stefanwalther/{%=name%}/tree/master) 9 | 10 | ![](./docs/images/header.png) 11 | 12 | ## Table of Contents 13 | 14 |
15 | 16 | 17 | 18 |
19 | 20 | ## Motivation 21 | {%= docs("./docs/inc/motivation.md") %} 22 | 23 | ## Getting started 24 | {%= docs("./docs/inc/getting-started.md") %} 25 | 26 | ## Features 27 | {%= docs("./docs/inc/features.md") %} 28 | 29 | ## Installation 30 | {%= docs("./docs/inc/install.md") %} 31 | 32 | ## Roadmap 33 | {%= docs("./docs/inc/roadmap.md") %} 34 | 35 | ## Contribution 36 | {%= docs("./docs/inc/contributing.md") %} 37 | 38 | ## About 39 | ### Author 40 | {%= docs("./docs/inc/author.md") %} 41 | 42 | ### License 43 | {%= license %} 44 | 45 | *** 46 | 47 | {%= include("footer") %} -------------------------------------------------------------------------------- /Dockerfile.prod: -------------------------------------------------------------------------------- 1 | ARG NODE_VER="8.10.0" 2 | 3 | FROM node:${NODE_VER}-alpine as RELEASE 4 | 5 | ENV HOME_DIR "opt/qix-graphql" 6 | 7 | RUN mkdir -p $HOME_DIR 8 | WORKDIR $HOME_DIR 9 | 10 | COPY package.json ./ 11 | 12 | RUN npm config set loglevel warn 13 | RUN npm install --quiet --only=production --no-package-lock 14 | 15 | COPY /src ./src/ 16 | 17 | ARG PORT=3004 18 | ENV PORT=${PORT} 19 | EXPOSE $PORT 20 | 21 | CMD ["npm", "run", "start"] 22 | -------------------------------------------------------------------------------- /Dockerfile.test: -------------------------------------------------------------------------------- 1 | FROM stefanwalther/qix-graphql:latest 2 | 3 | RUN npm config set loglevel warn 4 | RUN npm install --quiet 5 | 6 | COPY /test ./test/ 7 | COPY /.eslintrc.json ./.eslintrc.json 8 | COPY /Makefile ./Makefile 9 | 10 | RUN apk --no-cache add make grep git 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-present Stefan Walther 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | QIX_ENGINE_VER := "12.215.0" 2 | 3 | help: ## Show this help. 4 | @echo '' 5 | @echo 'Available commands:' 6 | @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 7 | @echo '' 8 | .PHONY: help 9 | 10 | gen-readme: ## Generate README.md (using docker-verb) 11 | docker run --rm -v ${PWD}:/opt/verb stefanwalther/verb 12 | .PHONY: gen-readme 13 | 14 | gen-readme-watch: ## Watch docs and re-generate the README.md 15 | npm run docs:watch 16 | .PHONY: gen-readme-watch 17 | 18 | set-engine-ver: 19 | 20 | .PHONY: set-engine-ver 21 | 22 | build: ## Build the docker image (production) 23 | docker build --force-rm -t stefanwalther/qix-graphql -f Dockerfile.prod . 24 | .PHONY: build 25 | 26 | run: 27 | docker run -d -p 3004:3004 stefanwalther/qix-graphql 28 | .PHONY: build 29 | 30 | build-test: ## Build the docker image (test image) 31 | docker build --force-rm -t stefanwalther/qix-graphql-test -f Dockerfile.test . 32 | .PHONY: build-test 33 | 34 | up-deps: ## Bring up all dependencies 35 | QIX_ENGINE_VER=$(QIX_ENGINE_VER) \ 36 | QIX_ACCEPT_EULA=yes \ 37 | docker-compose --f=./docker-compose.deps.yml up --build 38 | .PHONY: up-deps 39 | 40 | down-deps: ## Tear down all dependencies 41 | docker-compose --f=./docker-compose.deps.yml down --timeout=0 42 | .PHONY: down-deps 43 | 44 | up: ## Bring up the local environment 45 | QIX_ENGINE_VER=$(QIX_ENGINE_VER) \ 46 | QIX_ACCEPT_EULA=yes \ 47 | docker-compose --f=./docker-compose.yml up --build 48 | .PHONY: up 49 | 50 | down: ## Tear down the local environment 51 | docker-compose --f=./docker-compose.yml down --timeout=0 52 | .PHONY: down 53 | 54 | run-test: ## Run tests (assumes that the test-image has been built) 55 | QIX_ENGINE_VER=$(QIX_ENGINE_VER) \ 56 | QIX_ACCEPT_EULA=yes \ 57 | docker-compose --f=docker-compose.test.yml run qix-graphql-test npm run test:ci \ 58 | && docker-compose --f=docker-compose.test.yml down --timeout=0 59 | .PHONY: run-test 60 | 61 | test: build build-test run-test ## Run tests (as they would run on CircleCI) 62 | .PHONY: test 63 | 64 | circleci-test: build build-test run-test ## Run the tests as on CircleCI 65 | .PHONY: circleci-test 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # qix-graphql 2 | 3 | > GraphQL Server on top of the Qlik Associative Engine (a.k.a. QIX Engine). 4 | 5 | [![NPM version](https://img.shields.io/npm/v/qix-graphql.svg?style=flat)](https://www.npmjs.com/package/qix-graphql) 6 | [![David](https://img.shields.io/david/stefanwalther/qix-graphql.svg)](https://github.com/stefanwalther/qix-graphql) 7 | [![Codecov](https://img.shields.io/codecov/c/github/stefanwalther/qix-graphql.svg?logo=codecov)](https://codecov.io/gh/stefanwalther/qix-graphql) 8 | [![CircleCI](https://img.shields.io/circleci/project/github/stefanwalther/qix-graphql.svg?logo=circleci)](https://circleci.com/gh/stefanwalther/qix-graphql/tree/master) 9 | 10 | ![](./docs/images/header.png) 11 | 12 | ## Table of Contents 13 | 14 |
15 | 16 | - [Motivation](#motivation) 17 | - [Getting started](#getting-started) 18 | - [Features](#features) 19 | - [Installation](#installation) 20 | - [Roadmap](#roadmap) 21 | - [Contribution](#contribution) 22 | - [About](#about) 23 | * [Author](#author) 24 | * [License](#license) 25 | 26 | _(TOC generated by [verb](https://github.com/verbose/verb) using [markdown-toc](https://github.com/jonschlinkert/markdown-toc))_ 27 | 28 |
29 | 30 | ## Motivation 31 | _qix-graphql_ provides a [GraphQL](https://graphql.org/) server sitting on top of the powerful Qlik Associative Engine (a.k.a. QIX Engine), regardless whether it is Qlik Sense Enterprise, a solution build with [Qlik Core](https://qlikcore.com/) or Qlik Sense Multi Cloud. 32 | 33 | Using GraphQL next to QIX provides a completely new experience how to develop custom solutions: 34 | 35 | - Use the powerful tooling from the [GraphQL community](https://github.com/chentsulin/awesome-graphql), such as [GraphiQL](https://github.com/graphql/graphiql). 36 | - Connect to Qlik environments and Qlik apps in most of the major programming languages, such as C#, Go, Java, JavaScript, Swift, Python, always with the same experience. 37 | - There is no need anymore to understand Qlik specific constructs such as qHyperCube, SessionObjects, etc., just use GraphQL. 38 | - Leverage the benefits of a strongly typed system (e.g in IDEs such as Visual Studio Code). 39 | 40 | ## Getting started 41 | Before we connect to various environments (such as Qlik Sense Enterprise, Qlik Core solutions, etc.), let's get started with a *simple demo*. 42 | 43 |
44 | Getting the demo up and running 45 | 46 | The demo will consist of the following logical components: 47 | 48 | - A QIX Engine (using the the Qlik Core Engine container) 49 | - A few demo apps mounted to the QIX Engine 50 | - A GraphQL server connected to the QIX Engine 51 | 52 | All services will be spawn up using docker-compose (which requires e.g. Docker for Mac/Windows running on your machine). 53 | 54 | As this demo is included in the _qix-graphql_ repo, the easiest way to get started is to clone this repo: 55 | 56 | ``` 57 | $ git clone https://github.com/stefanwalther/qix-graphql 58 | ``` 59 | 60 | Then run the following command: 61 | 62 | ``` 63 | $ QIX_ENGINE_VER=12.171.0 QIX_ACCEPT_EULA=yes docker-compose up -d 64 | ``` 65 | Note: `QIX_ENGINE_VER` and `QIX_ACCEPT_EULA` are environment variables being used in the docker-compose file. 66 | 67 |
68 | 69 | ### Explore the GraphQL API using the GraphiQL IDE 70 | 71 |
72 | Explore the GraphQL server using GraphiQL 73 | 74 | We can now open http://localhost:3004/global/graphql in your browser to get the [GraphiQL](https://github.com/graphql/graphiql) user interface. 75 | GraphiQL is a graphical interactive in-browser GraphQL IDE, which allows you to explore the API being provided by _qix-graphql_, the GraphQL server. 76 | 77 | ![](./docs/images/graphiql-global.png) 78 | 79 | We can also easily get a list of all documents in the IDE: 80 | 81 | ![](./docs/images/graphiql-global-docs.png) 82 | 83 | Note: GraphiQL also provides intellisense, built-in documentation and more, see here for some [tipps & tricks for GraphiQL](./docs/about-graphiql.md). 84 | 85 | In the examples above we have acted on the *global* scope, which allows us to explore meta-information about different Qlik documents. 86 | 87 | Using the URL stated in `doc/_links/_docs` we can connect to a single document. 88 | 89 |
90 | 91 |
92 | Explore the API of a single document, using GraphiQL 93 | 94 | If we use (as defined by this demo) `http://localhost:3004/doc/:qDocId/graphql` we connect to a single document and its API: 95 | 96 | Going to the built-in documentation, we'll see the tables of this document we can query: 97 | 98 | ![](./docs/images/graphiql-doc-docs.png) 99 | 100 | So let's query one of those tables (in this example the table `account` on the doc `CRM.qvf`: 101 | 102 | ![](./docs/images/graphiql-doc-account-table.png) 103 | 104 |
105 | 106 | ### Developing a Client 107 | 108 |
109 | Creating a client in node.js 110 | OK, so far we have seen that we can easily explore the generated APIs on a global and on a doc scope. 111 | Now let's create some code so see how we can use the server when developing a custom application using the GraphQL server. It can basically be any kind of an application, a backend-service, a website, a native mobile app; essentially the approach is always the same: 112 | 113 | ```js 114 | const client = require('graphql-client')({ 115 | url: 'http://localhost:3004/global/graphql' 116 | }); 117 | 118 | async function getDocs() { 119 | const query = `{ 120 | docs { 121 | qDocId 122 | qDocName 123 | } 124 | }`; 125 | const vars = ''; 126 | return await client.query(query, vars); 127 | } 128 | 129 | (async () => { 130 | let result = await getDocs(); 131 | console.log('Apps in the current environment:\n'); 132 | result.data.docs.forEach(item => { 133 | console.log(`\t- ${item.qDocName}`); 134 | }); 135 | })(); 136 | ``` 137 | 138 | This will return: 139 | 140 | ![](./docs/images/graphiql-example-nodejs.png) 141 | 142 | So we don't need to use enigma.js, we don't need to understand specific constructs of the QIX Engine such as qHyperCube, it's basically a very straightforward development experience using common tools. 143 | 144 |
145 | 146 | ## Features 147 | _qix-graphql_ provides two basic different types of endpoints: 148 | 149 | **Global Scope:** 150 | 151 | - Getting information about one environment (e.g. listing all Qlik docs, etc.) 152 | 153 | `http(s)://:/global/graphql` 154 | 155 | **Doc Scope:** 156 | 157 | - Connecting to a single Qlik document to perform operations such as 158 | - Getting the data from the given document 159 | - Being able to query the various tables & fields 160 | - Making selections 161 | - Creating on demand hypercubes 162 | - etc. 163 | 164 | `http(s)://:/doc/:qDocId/graphql` 165 | 166 | ## Installation 167 | _qix-graphql_ is already packaged as Docker image ([stefanwalther/qix-graphql](https://hub.docker.com/r/stefanwalther/qix-graphql/)). 168 | 169 | ``` 170 | $ docker run -d -p 3004:3004 stefanwalther/qix-graphql 171 | ``` 172 | 173 | ### Configuration 174 | 175 | The following configuration parameters are available: 176 | 177 | - `HOST` - Host of the GraphQL server, defaults to `localhost` 178 | - `PORT` - Port of the GraphQL server, defaults to `3004` 179 | - `QIX_HOST` - Host of the QIX Engine, defaults to `qix` 180 | - `QIX_PORT`- Port of the QIX Engine, defaults to `9076` 181 | 182 | ## Roadmap 183 | The work has been split into a few iterations: 184 | 185 | - **[Iteration 1: Core prototyping work](https://github.com/stefanwalther/qix-graphql/projects/2)** **_<== !!! Sorry to say: WE ARE HERE !!!_** 186 | - Focus on core functionality 187 | - Testing various ideas/concepts 188 | - No UI work 189 | - Little documentation 190 | - Sometimes Messy code ;-) 191 | - Unstable APIs 192 | - **[Iteration 2: Some work on enhanced concepts](https://github.com/stefanwalther/qix-graphql/projects/3)** 193 | - JWT support / Section Access 194 | - Using multiple engines, using mira 195 | - Using in Qlik Sense Multi Cloud 196 | - **Iteration 3: Clean UI & additional functionality** 197 | 198 | See [projects](https://github.com/stefanwalther/qix-graphql/projects) for more details. 199 | 200 | ## Contribution 201 | I am actively looking for people out there, who think that this project is an interesting idea and want to contribute. 202 | 203 |
204 | Contributing 205 | Pull requests and stars are always welcome. For bugs and feature requests, [please create an issue](https://github.com/stefanwalther/qix-graphql/issues). The process for contributing is outlined below: 206 | 207 | 1. Create a fork of the project 208 | 2. Work on whatever bug or feature you wish 209 | 3. Create a pull request (PR) 210 | 211 | I cannot guarantee that I will merge all PRs but I will evaluate them all. 212 |
213 | 214 |
215 | Local Development 216 | 217 | The easiest way to develop locally is follow these steps: 218 | 219 | 1) Clone the GitHub repo 220 | ``` 221 | $ git clone https://github.com/stefanwalther/qix-graphql 222 | ``` 223 | 224 | 2) Install the dependencies 225 | ``` 226 | $ npm install 227 | ``` 228 | 229 | 3) Start the dependencies (Qlik Associative Engine + a few sample apps mounted): 230 | ``` 231 | $ make up-deps 232 | ``` 233 | 234 | Make your code changes, then: 235 | 236 | - Run local tests: `npm run test` 237 | - Run local tests with a watcher: `npm run test` 238 | - Start the GraphQl server: `npm run start` 239 | - Start the GraphQl server with a watcher: `npm run start:watch` 240 | 241 |
242 | 243 |
244 | Running Tests 245 | 246 | Having the local dependencies up and running, you can just run the tests by executing: 247 | 248 | ``` 249 | $ npm run test 250 | ``` 251 | 252 | If you want to have an watcher active, use: 253 | 254 | ``` 255 | $ npm run test:watch 256 | ``` 257 | 258 |
259 | 260 |
261 | CircleCI Tests 262 | 263 | To simulate the tests running on CircleCI run the following: 264 | 265 | ``` 266 | $ make circleci-test 267 | ``` 268 | 269 |
270 | 271 | ## About 272 | ### Author 273 | **Stefan Walther** 274 | 275 | * [twitter](http://twitter.com/waltherstefan) 276 | * [github.com/stefanwalther](http://github.com/stefanwalther) 277 | * [LinkedIn](https://www.linkedin.com/in/stefanwalther/) 278 | * [qliksite.io](http://qliksite.io) 279 | 280 | ### License 281 | MIT 282 | 283 | *** 284 | 285 | _This file was generated by [verb-generate-readme](https://github.com/verbose/verb-generate-readme), v0.6.0, on August 12, 2018._ 286 | 287 | -------------------------------------------------------------------------------- /docker-compose.deps.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | 3 | volumes: 4 | sense-docs: 5 | 6 | services: 7 | 8 | qix: 9 | container_name: qix 10 | image: "qlikcore/engine:${QIX_ENGINE_VER:-latest}" 11 | command: [ 12 | "-S", "DocumentDirectory=/docs", 13 | "-S", "AcceptEULA=${QIX_ACCEPT_EULA:-no}" 14 | ] 15 | volumes: 16 | - sense-docs:/docs 17 | ports: 18 | - "9076:9076" 19 | expose: 20 | - 9076 21 | 22 | sense-docs: 23 | image: stefanwalther/sense-docs:latest@sha256:50f9a7baffa3eaaac1346ba64d762c64223f6dfcc6ff8c6eaed9e7c874fc5e5a 24 | volumes: 25 | - sense-docs:/opt/sense-docs/docs 26 | tty: true 27 | -------------------------------------------------------------------------------- /docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | 3 | volumes: 4 | sense-docs: 5 | 6 | services: 7 | 8 | qix: 9 | container_name: qix 10 | image: "qlikcore/engine:${QIX_ENGINE_VER:-latest}" 11 | command: [ 12 | "-S", "DocumentDirectory=/docs", 13 | "-S", "AcceptEULA=${QIX_ACCEPT_EULA:-no}" 14 | ] 15 | volumes: 16 | - sense-docs:/docs 17 | ports: 18 | - "9076:9076" 19 | expose: 20 | - 9076 21 | 22 | sense-docs: 23 | image: stefanwalther/sense-docs:latest@sha256:50f9a7baffa3eaaac1346ba64d762c64223f6dfcc6ff8c6eaed9e7c874fc5e5a 24 | volumes: 25 | - sense-docs:/opt/sense-docs/docs 26 | tty: true 27 | 28 | qix-graphql: 29 | container_name: qix-graphql 30 | build: 31 | context: . 32 | dockerfile: Dockerfile.prod 33 | ports: 34 | - "3004:3004" 35 | environment: 36 | - QIX_HOST=qix 37 | - QIX_PORT=9076 38 | - HOST=qix-graphql 39 | - PORT=3004 40 | restart: always 41 | command: ["npm", "run", "start:watch"] 42 | volumes: 43 | - ./src/:/opt/qix-graphql/src 44 | -------------------------------------------------------------------------------- /docker-compose.test.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | 3 | volumes: 4 | sense-docs: 5 | 6 | services: 7 | 8 | qix: 9 | container_name: qix 10 | image: "qlikcore/engine:${QIX_ENGINE_VER:-latest}" 11 | command: [ 12 | "-S", "DocumentDirectory=/docs", 13 | "-S", "AcceptEULA=${QIX_ACCEPT_EULA:-no}" 14 | ] 15 | volumes: 16 | - sense-docs:/docs 17 | ports: 18 | - "9076:9076" 19 | expose: 20 | - 9076 21 | 22 | sense-docs: 23 | image: stefanwalther/sense-docs:latest@sha256:50f9a7baffa3eaaac1346ba64d762c64223f6dfcc6ff8c6eaed9e7c874fc5e5a 24 | volumes: 25 | - sense-docs:/opt/sense-docs/docs 26 | tty: true 27 | 28 | qix-graphql-test: 29 | image: stefanwalther/qix-graphql-test 30 | environment: 31 | - NODE_ENV=test 32 | - QIX_HOST=qix 33 | - QIX_PORT=9076 34 | - HOST=qix-graphql 35 | - PORT=3004 36 | depends_on: 37 | - qix 38 | - sense-docs 39 | volumes: 40 | - ./coverage:/opt/qix-graphql/coverage 41 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | 3 | volumes: 4 | sense-docs: 5 | 6 | services: 7 | 8 | qix: 9 | container_name: qix 10 | image: "qlikcore/engine:${QIX_ENGINE_VER:-latest}" 11 | command: [ 12 | "-S", "DocumentDirectory=/docs", 13 | "-S", "AcceptEULA=${QIX_ACCEPT_EULA:-no}" 14 | ] 15 | volumes: 16 | - sense-docs:/docs 17 | ports: 18 | - "9076:9076" 19 | expose: 20 | - 9076 21 | 22 | sense-docs: 23 | container_name: sense-docs 24 | image: stefanwalther/sense-docs:latest@sha256:50f9a7baffa3eaaac1346ba64d762c64223f6dfcc6ff8c6eaed9e7c874fc5e5a 25 | volumes: 26 | - sense-docs:/opt/sense-docs/docs 27 | tty: true 28 | 29 | qix-graphql: 30 | container_name: qix-graphql 31 | image: "stefanwalther/qix-graphql:latest@sha256:74b751cd819042d2858a07c71904fef5eb52179b9e6ccc27771467d561d9c637" 32 | ports: 33 | - "3004:3004" 34 | environment: 35 | - QIX_HOST=qix 36 | - QIX_PORT=9076 37 | - HOST=localhost 38 | - PORT=3004 39 | command: ["npm", "run", "start"] 40 | depends_on: 41 | - qix 42 | - sense-docs 43 | -------------------------------------------------------------------------------- /docs/about-graphiql.md: -------------------------------------------------------------------------------- 1 | # About GraphiQL 2 | 3 | [GraphiQL](https://github.com/graphql/graphiql) is "a graphical interactive in-browser GraphQL IDE". 4 | 5 | GraphiQL provides: 6 | 7 | - An easy IDE to test queries and explore results 8 | - Built-in documentation 9 | - Intellisense 10 | 11 | ## Test Queries and Explore Results 12 | 13 | ![](./docs/images/graphiql-exploration.png) 14 | 15 | ## Built-In Documentation 16 | 17 | ![](./docs/images/graphiql-documentation.png) 18 | 19 | ## Intellisense 20 | 21 | Press `Alt` + `Space` and you'll get the intellisense experience: 22 | 23 | ![](./docs/images/graphiql-intellisense.png) 24 | 25 | -------------------------------------------------------------------------------- /docs/about-graphql-qix.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanwalther/qix-graphql/e262c10d464e0c4622890453ff30f9dbc93a61af/docs/about-graphql-qix.md -------------------------------------------------------------------------------- /docs/about-graphql.md: -------------------------------------------------------------------------------- 1 | # Wait, No Idea What You are Talking About! 2 | 3 | OK, absolutely no problem. Let's take it easy and start from the beginning. 4 | 5 | (to be extended) -------------------------------------------------------------------------------- /docs/about.md: -------------------------------------------------------------------------------- 1 | _{%=name%}_ provides a GraphQl server on top of any Qlik Engine by providing a lightweight server, which can sit on top of one ore more QIX engines to provide a GraphQl interface. 2 | 3 | Under the hood _{%=name%}_ uses [enigma.js](https://github.com/qlik-oss/enigma.js) to talk to the QIX engine. 4 | 5 | -------------------------------------------------------------------------------- /docs/images/graphiql-doc-account-table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanwalther/qix-graphql/e262c10d464e0c4622890453ff30f9dbc93a61af/docs/images/graphiql-doc-account-table.png -------------------------------------------------------------------------------- /docs/images/graphiql-doc-docs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanwalther/qix-graphql/e262c10d464e0c4622890453ff30f9dbc93a61af/docs/images/graphiql-doc-docs.png -------------------------------------------------------------------------------- /docs/images/graphiql-documentation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanwalther/qix-graphql/e262c10d464e0c4622890453ff30f9dbc93a61af/docs/images/graphiql-documentation.png -------------------------------------------------------------------------------- /docs/images/graphiql-example-nodejs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanwalther/qix-graphql/e262c10d464e0c4622890453ff30f9dbc93a61af/docs/images/graphiql-example-nodejs.png -------------------------------------------------------------------------------- /docs/images/graphiql-exploration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanwalther/qix-graphql/e262c10d464e0c4622890453ff30f9dbc93a61af/docs/images/graphiql-exploration.png -------------------------------------------------------------------------------- /docs/images/graphiql-global-docs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanwalther/qix-graphql/e262c10d464e0c4622890453ff30f9dbc93a61af/docs/images/graphiql-global-docs.png -------------------------------------------------------------------------------- /docs/images/graphiql-global.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanwalther/qix-graphql/e262c10d464e0c4622890453ff30f9dbc93a61af/docs/images/graphiql-global.png -------------------------------------------------------------------------------- /docs/images/graphiql-intellisense.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanwalther/qix-graphql/e262c10d464e0c4622890453ff30f9dbc93a61af/docs/images/graphiql-intellisense.png -------------------------------------------------------------------------------- /docs/images/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanwalther/qix-graphql/e262c10d464e0c4622890453ff30f9dbc93a61af/docs/images/header.png -------------------------------------------------------------------------------- /docs/images/header.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanwalther/qix-graphql/e262c10d464e0c4622890453ff30f9dbc93a61af/docs/images/header.psd -------------------------------------------------------------------------------- /docs/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanwalther/qix-graphql/e262c10d464e0c4622890453ff30f9dbc93a61af/docs/images/logo.png -------------------------------------------------------------------------------- /docs/images/logo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanwalther/qix-graphql/e262c10d464e0c4622890453ff30f9dbc93a61af/docs/images/logo.psd -------------------------------------------------------------------------------- /docs/inc/author.md: -------------------------------------------------------------------------------- 1 | **Stefan Walther** 2 | 3 | * [twitter](http://twitter.com/waltherstefan) 4 | * [github.com/stefanwalther](http://github.com/stefanwalther) 5 | * [LinkedIn](https://www.linkedin.com/in/stefanwalther/) 6 | * [qliksite.io](http://qliksite.io) -------------------------------------------------------------------------------- /docs/inc/contributing.md: -------------------------------------------------------------------------------- 1 | I am actively looking for people out there, who think that this project is an interesting idea and want to contribute. 2 | 3 |
4 | Contributing 5 | Pull requests and stars are always welcome. For bugs and feature requests, [please create an issue]({%= bugs.url %}). The process for contributing is outlined below: 6 | 7 | 1. Create a fork of the project 8 | 2. Work on whatever bug or feature you wish 9 | 3. Create a pull request (PR) 10 | 11 | I cannot guarantee that I will merge all PRs but I will evaluate them all. 12 |
13 | 14 |
15 | Local Development 16 | 17 | The easiest way to develop locally is follow these steps: 18 | 19 | 1) Clone the GitHub repo 20 | ``` 21 | $ git clone https://github.com/stefanwalther/qix-graphql 22 | ``` 23 | 24 | 2) Install the dependencies 25 | ``` 26 | $ npm install 27 | ``` 28 | 29 | 3) Start the dependencies (Qlik Associative Engine + a few sample apps mounted): 30 | ``` 31 | $ make up-deps 32 | ``` 33 | 34 | Make your code changes, then: 35 | 36 | - Run local tests: `npm run test` 37 | - Run local tests with a watcher: `npm run test` 38 | - Start the GraphQl server: `npm run start` 39 | - Start the GraphQl server with a watcher: `npm run start:watch` 40 | 41 |
42 | 43 |
44 | Running Tests 45 | 46 | Having the local dependencies up and running, you can just run the tests by executing: 47 | 48 | ``` 49 | $ npm run test 50 | ``` 51 | 52 | If you want to have an watcher active, use: 53 | 54 | ``` 55 | $ npm run test:watch 56 | ``` 57 | 58 |
59 | 60 |
61 | CircleCI Tests 62 | 63 | To simulate the tests running on CircleCI run the following: 64 | 65 | ``` 66 | $ make circleci-test 67 | ``` 68 | 69 |
70 | -------------------------------------------------------------------------------- /docs/inc/features.md: -------------------------------------------------------------------------------- 1 | _qix-graphql_ provides two basic different types of endpoints: 2 | 3 | **Global Scope:** 4 | 5 | - Getting information about one environment (e.g. listing all Qlik docs, etc.) 6 | 7 | `http(s)://:/global/graphql` 8 | 9 | **Doc Scope:** 10 | 11 | - Connecting to a single Qlik document to perform operations such as 12 | - Getting the data from the given document 13 | - Being able to query the various tables & fields 14 | - Making selections 15 | - Creating on demand hypercubes 16 | - etc. 17 | 18 | `http(s)://:/doc/:qDocId/graphql` 19 | -------------------------------------------------------------------------------- /docs/inc/getting-started.md: -------------------------------------------------------------------------------- 1 | Before we connect to various environments (such as Qlik Sense Enterprise, Qlik Core solutions, etc.), let's get started with a *simple demo*. 2 | 3 |
4 | Getting the demo up and running 5 | 6 | The demo will consist of the following logical components: 7 | 8 | - A QIX Engine (using the the Qlik Core Engine container) 9 | - A few demo apps mounted to the QIX Engine 10 | - A GraphQL server connected to the QIX Engine 11 | 12 | All services will be spawn up using docker-compose (which requires e.g. Docker for Mac/Windows running on your machine). 13 | 14 | As this demo is included in the _qix-graphql_ repo, the easiest way to get started is to clone this repo: 15 | 16 | ``` 17 | $ git clone https://github.com/stefanwalther/qix-graphql 18 | ``` 19 | 20 | Then run the following command: 21 | 22 | ``` 23 | $ QIX_ENGINE_VER=12.171.0 QIX_ACCEPT_EULA=yes docker-compose up -d 24 | ``` 25 | Note: `QIX_ENGINE_VER` and `QIX_ACCEPT_EULA` are environment variables being used in the docker-compose file. 26 | 27 |
28 | 29 | ### Explore the GraphQL API using the GraphiQL IDE 30 | 31 |
32 | Explore the GraphQL server using GraphiQL 33 | 34 | We can now open http://localhost:3004/global/graphql in your browser to get the [GraphiQL](https://github.com/graphql/graphiql) user interface. 35 | GraphiQL is a graphical interactive in-browser GraphQL IDE, which allows you to explore the API being provided by _qix-graphql_, the GraphQL server. 36 | 37 | ![](./docs/images/graphiql-global.png) 38 | 39 | We can also easily get a list of all documents in the IDE: 40 | 41 | ![](./docs/images/graphiql-global-docs.png) 42 | 43 | Note: GraphiQL also provides intellisense, built-in documentation and more, see here for some [tipps & tricks for GraphiQL](./docs/about-graphiql.md). 44 | 45 | In the examples above we have acted on the *global* scope, which allows us to explore meta-information about different Qlik documents. 46 | 47 | Using the URL stated in `doc/_links/_docs` we can connect to a single document. 48 | 49 |
50 | 51 |
52 | Explore the API of a single document, using GraphiQL 53 | 54 | If we use (as defined by this demo) `http://localhost:3004/doc/:qDocId/graphql` we connect to a single document and its API: 55 | 56 | Going to the built-in documentation, we'll see the tables of this document we can query: 57 | 58 | ![](./docs/images/graphiql-doc-docs.png) 59 | 60 | So let's query one of those tables (in this example the table `account` on the doc `CRM.qvf`: 61 | 62 | ![](./docs/images/graphiql-doc-account-table.png) 63 | 64 |
65 | 66 | ### Developing a Client 67 | 68 |
69 | Creating a client in node.js 70 | OK, so far we have seen that we can easily explore the generated APIs on a global and on a doc scope. 71 | Now let's create some code so see how we can use the server when developing a custom application using the GraphQL server. It can basically be any kind of an application, a backend-service, a website, a native mobile app; essentially the approach is always the same: 72 | 73 | ```js 74 | const client = require('graphql-client')({ 75 | url: 'http://localhost:3004/global/graphql' 76 | }); 77 | 78 | async function getDocs() { 79 | const query = `{ 80 | docs { 81 | qDocId 82 | qDocName 83 | } 84 | }`; 85 | const vars = ''; 86 | return await client.query(query, vars); 87 | } 88 | 89 | (async () => { 90 | let result = await getDocs(); 91 | console.log('Apps in the current environment:\n'); 92 | result.data.docs.forEach(item => { 93 | console.log(`\t- ${item.qDocName}`); 94 | }); 95 | })(); 96 | ``` 97 | 98 | This will return: 99 | 100 | ![](./docs/images/graphiql-example-nodejs.png) 101 | 102 | So we don't need to use enigma.js, we don't need to understand specific constructs of the QIX Engine such as qHyperCube, it's basically a very straightforward development experience using common tools. 103 | 104 |
-------------------------------------------------------------------------------- /docs/inc/install.md: -------------------------------------------------------------------------------- 1 | 2 | _qix-graphql_ is already packaged as Docker image ([stefanwalther/qix-graphql](https://hub.docker.com/r/stefanwalther/qix-graphql/)). 3 | 4 | ``` 5 | $ docker run -d -p 3004:3004 stefanwalther/qix-graphql 6 | ``` 7 | 8 | ### Configuration 9 | 10 | The following configuration parameters are available: 11 | 12 | - `HOST` - Host of the GraphQL server, defaults to `localhost` 13 | - `PORT` - Port of the GraphQL server, defaults to `3004` 14 | - `QIX_HOST` - Host of the QIX Engine, defaults to `qix` 15 | - `QIX_PORT`- Port of the QIX Engine, defaults to `9076` -------------------------------------------------------------------------------- /docs/inc/motivation.md: -------------------------------------------------------------------------------- 1 | _qix-graphql_ provides a [GraphQL](https://graphql.org/) server sitting on top of the powerful Qlik Associative Engine (a.k.a. QIX Engine), regardless whether it is Qlik Sense Enterprise, a solution build with [Qlik Core](https://qlikcore.com/) or Qlik Sense Multi Cloud. 2 | 3 | Using GraphQL next to QIX provides a completely new experience how to develop custom solutions: 4 | 5 | - Use the powerful tooling from the [GraphQL community](https://github.com/chentsulin/awesome-graphql), such as [GraphiQL](https://github.com/graphql/graphiql). 6 | - Connect to Qlik environments and Qlik apps in most of the major programming languages, such as C#, Go, Java, JavaScript, Swift, Python, always with the same experience. 7 | - There is no need anymore to understand Qlik specific constructs such as qHyperCube, SessionObjects, etc., just use GraphQL. 8 | - Leverage the benefits of a strongly typed system (e.g in IDEs such as Visual Studio Code). -------------------------------------------------------------------------------- /docs/inc/no-idea.md: -------------------------------------------------------------------------------- 1 | ## Wait, I have no idea, what you are talking about 2 | OK, absolutely no problem, I have provided some more background information: 3 | 4 | - What is GraphQL 5 | - Why GraphiQl & Qlik Associative Engine? 6 | - What's GraphiQL 7 | 8 | (in the works) -------------------------------------------------------------------------------- /docs/inc/roadmap.md: -------------------------------------------------------------------------------- 1 | The work has been split into a few iterations: 2 | 3 | - **[Iteration 1: Core prototyping work](https://github.com/stefanwalther/qix-graphql/projects/2)** **_<== !!! Sorry to say: WE ARE HERE !!!_** 4 | - Focus on core functionality 5 | - Testing various ideas/concepts 6 | - No UI work 7 | - Little documentation 8 | - Sometimes Messy code ;-) 9 | - Unstable APIs 10 | - **[Iteration 2: Some work on enhanced concepts](https://github.com/stefanwalther/qix-graphql/projects/3)** 11 | - JWT support / Section Access 12 | - Using multiple engines, using mira 13 | - Using in Qlik Sense Multi Cloud 14 | - **Iteration 3: Clean UI & additional functionality** 15 | 16 | See [projects](https://github.com/stefanwalther/qix-graphql/projects) for more details. -------------------------------------------------------------------------------- /docs/questions.md: -------------------------------------------------------------------------------- 1 | - How to handle best the Websocket connection 2 | - Seems to make sense to store this in the GraphQL context 3 | - But there should be a different WS for each of the documents (see enigma.js ?!) -------------------------------------------------------------------------------- /docs/resources.md: -------------------------------------------------------------------------------- 1 | ## Editors 2 | 3 | - [Editor with tabs](http://toolbox.sangria-graphql.org/graphiql) 4 | 5 | ## Articles 6 | 7 | https://blog.graph.cool/reusing-composing-graphql-apis-with-graphql-bindings-80a4aa37cff5 -------------------------------------------------------------------------------- /docs/scope-doc.md: -------------------------------------------------------------------------------- 1 | Implementation status for the doc scope: -------------------------------------------------------------------------------- /docs/scope-global.md: -------------------------------------------------------------------------------- 1 | Implementation status for the global scope: -------------------------------------------------------------------------------- /docs/todos.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | - [ ] Publish and install qix-graphql-schema-generator 4 | - [ ] Refactor to have all functionality of the QIX global namespace in one schema 5 | - [ ] Global schema 6 | - [ ] EngineVersion 7 | - [ ] AppEntry 8 | - [ ] GetCodePages 9 | - [ ] App schema 10 | - [ ] Forward 11 | - [ ] Back 12 | - [ ] ClearAll 13 | - [ ] CreateBookmark 14 | - [ ] DoReload 15 | - [ ] DoReloadEx 16 | - [ ] GetBookmarks -------------------------------------------------------------------------------- /docs/z-usage.md: -------------------------------------------------------------------------------- 1 | 2 | ### Work with the global scope 3 | 4 | Open the GraphiQl UI: http://localhost:3004/global/graphql 5 | 6 | Get the list of docs (apps): 7 | 8 | ``` 9 | { 10 | docs { 11 | qDocId 12 | qDocName 13 | qConnectedUsers 14 | qFileTime 15 | qFileSize 16 | qConnectedUsers 17 | } 18 | } 19 | ``` 20 | 21 | Retrieve a single doc + links: 22 | 23 | ``` 24 | { 25 | doc(qDocId: "/docs/Consumer Goods Example.qvf") { 26 | qDocId 27 | _links { 28 | _doc 29 | } 30 | } 31 | } 32 | ``` 33 | 34 | Copy the link of _links/_docs to connect to a single app (e.g. [http://localhost:3004/app/%2Fdocs%2FConsumer%20Goods%20Example.qvf/graphql](http://localhost:3004/app/%2Fdocs%2FConsumer%20Goods%20Example.qvf/graphql): 35 | 36 | 37 | ### Work with the app scope 38 | 39 | 40 | 41 | Copying the value of the attribute `_links/_doc`, will give you the link to open another GraphiQl instance, *connecting to this single app*: 42 | 43 | This then allows you to get e.g. all content of a single table. 44 | 45 | **Example:** 46 | 47 | - Connect to `http://localhost:3004/app/%2Fdocs%2FConsumer%20Goods%20Example.qvf/graphql` 48 | - Then execute the following query to get all data from the `AddressDetails` table: 49 | 50 | ``` 51 | { 52 | AddressDetails { 53 | Address_Number 54 | State 55 | Customer_Address_1 56 | Customer_Address_2 57 | Customer_Address_3 58 | Customer_Address_4 59 | Zip_Code 60 | City 61 | Country 62 | QlikPoint 63 | K 64 | } 65 | } 66 | ``` -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | The beauty of GraphQL is that it can be easily used with most of the common programming languages. 4 | This means that _qix-graphql_ also opens the power of the Qlik Associative Engine (aka QIX Engine) to far more languages as it is the case right now. 5 | 6 | Here are a few examples: 7 | 8 | - [node.js](./node-js) 9 | - GoLang 10 | - C# 11 | - Python -------------------------------------------------------------------------------- /examples/node-js/Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile to include the example to the overall tests of qix-graphql on CircleCI 2 | 3 | FROM node 4 | 5 | ENV HOME_DIR "opt/qix-graphql/examples/node.js" 6 | 7 | RUN mkdir -p $HOME_DIR 8 | WORKDIR $HOME_DIR 9 | 10 | COPY package.json ./ 11 | 12 | RUN npm config set loglevel warn 13 | RUN npm install --quiet --no-package-lock 14 | 15 | COPY . . 16 | 17 | RUN ["npm", "run", "start"] 18 | -------------------------------------------------------------------------------- /examples/node-js/README.md: -------------------------------------------------------------------------------- 1 | # node.js Example 2 | 3 | This examples proves the ussage of the _qix-graphql_ server using node.js. 4 | Not a big deal actually, you could achieve the same with enigma.js, too, just with a bit more code probably. 5 | 6 | ## Run the example 7 | 8 | - cd into `./examples/node-js` 9 | - run `npm install` 10 | - start the dependencies by running `npm run up-server` 11 | - run `npm run start` -------------------------------------------------------------------------------- /examples/node-js/index.js: -------------------------------------------------------------------------------- 1 | const client = require('graphql-client')({ 2 | url: 'http://localhost:3004/global/graphql' 3 | }); 4 | 5 | async function getDocs() { 6 | const query = `{ 7 | docs { 8 | qDocId 9 | qDocName 10 | } 11 | }`; 12 | const vars = ''; 13 | return await client.query(query, vars); 14 | } 15 | 16 | (async () => { 17 | let result = await getDocs(); 18 | console.log(''); 19 | console.log('Apps in the current environment:\n'); 20 | result.data.docs.forEach(item => { 21 | console.log(`\t- ${item.qDocName}`); 22 | }); 23 | console.log(''); 24 | })(); 25 | 26 | module.exports = { 27 | getDocs 28 | }; 29 | -------------------------------------------------------------------------------- /examples/node-js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qix-graphql-node-js-example", 3 | "version": "0.1.0", 4 | "description": "Example using qix-graphql using node.js", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node index.js", 9 | "up-server": "", 10 | "down-server": "", 11 | "build": "docker build --force-rm -t stefanwalther/qix-graphql-examples-node-js ." 12 | }, 13 | "author": "Stefan Walther (http://qliksite.io)", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "chai": "^4.1.2", 17 | "mocha": "^5.2.0" 18 | }, 19 | "dependencies": { 20 | "graphql-client": "^2.0.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/node-js/test.spec.js: -------------------------------------------------------------------------------- 1 | require('mocha'); 2 | global.expect = require('chai').expect; 3 | const script = require('./index'); 4 | 5 | describe('Run the test', () => { 6 | it('returns a collection of docs', async () => { 7 | let result = await script.getDocs(); 8 | expect(result).to.exist; 9 | expect(result).to.have.a.property('data'); 10 | expect(result.data).to.have.a.property('docs'); 11 | expect(result.data.docs).to.exist.to.be.an('array'); 12 | }) 13 | }); 14 | -------------------------------------------------------------------------------- /examples/test/build-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## Scripts to build all the tests, to then be included in the CircleCI tests 4 | -------------------------------------------------------------------------------- /nodemon-docs.json: -------------------------------------------------------------------------------- 1 | { 2 | "ext": "md,png,jpg,jpeg,gif,svg", 3 | "verbose": false, 4 | "ignore": [ 5 | "README.md" 6 | ], 7 | "watch": [ 8 | ".verb.md", 9 | "docs" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qix-graphql", 3 | "version": "0.0.1", 4 | "description": "GraphQL Server on top of the Qlik Associative Engine (a.k.a. QIX Engine).", 5 | "main": "./src/index.js", 6 | "scripts": { 7 | "docs": "docker run --rm -v ${PWD}:/opt/verb stefanwalther/verb", 8 | "docs:watch": "nodemon --config ./nodemon-docs.json --exec 'npm run' docs", 9 | "lint": "npm run lint:src", 10 | "lint:fix": "npm run lint:src:fix", 11 | "lint:src": "eslint src", 12 | "lint:src:fix": "eslint src --fix", 13 | "d-build": "docker build -t stefanwalther/qix-graphql .", 14 | "d-run": "docker run -it stefanwalther/qix-graphql /bin/bash", 15 | "dc-up": "docker-compose --f=./docker-compose.yml up --build", 16 | "dc-up:deps": "docker-compose --f=./docker-compose.deps.yml up --build", 17 | "dc-down:deps": "docker-compose --f=./docker-compose.deps.yml down", 18 | "dc-down": "docker-compose --f=./docker-compose.yml down", 19 | "dc-up:dev": "docker-compose --f=./docker-compose.dev.yml up --build && npm run start:watch", 20 | "dc-down:dev": "docker-compose --f=./docker-compose.dev.yml down", 21 | "start": "node ./src/index.js", 22 | "start:watch": "cross-env QIX_HOST=localhost nodemon -L --watch ./src --exec npm run start", 23 | "start:watch:debug": "nodemon -L --watch ./src --exec node --inspect ./src/index.js", 24 | "test": "cross-env QIX_HOST=localhost nyc --reporter=lcov mocha './test/**/*.spec.js' --require './test/mocha.conf.js' --timeout 6000 && npm run coverage", 25 | "test:ci": "cross-env QIX_HOST=qix nyc --reporter=lcov mocha './test/**/*.spec.js' --require './test/mocha.conf.js' --timeout 6000 && npm run coverage", 26 | "test:watch": "cross-env QIX_HOST=localhost nyc --reporter=lcov mocha './test/**/*.spec.js' --require './test/mocha.conf.js' --reporter=min --timeout 6000 --watch", 27 | "test:integration": "cross-env QIX_HOST=localhost nyc --reporter=lcov mocha './test/integration/**/*.spec.js' --require './test/mocha.conf.js' --timeout 6000", 28 | "test:integration:watch": "cross-env QIX_HOST=localhost npm run test:unit && nyc --reporter=lcov mocha './test/integration/**/*.spec.js' --require './test/mocha.conf.js' --reporter=min --timeout 6000 --watch", 29 | "test:unit": "nyc --reporter=lcov mocha './test/unit/**/*.spec.js' --require './test/mocha.conf.js'", 30 | "coverage": "nyc report", 31 | "precommit": "npm run lint" 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "git+https://github.com/stefanwalther/qix-graphql.git" 36 | }, 37 | "keywords": [ 38 | "graphql", 39 | "qix", 40 | "enigma.js", 41 | "qlik" 42 | ], 43 | "author": "Stefan Walther (http://qliksite.io)", 44 | "license": "MIT", 45 | "bugs": { 46 | "url": "https://github.com/stefanwalther/qix-graphql/issues" 47 | }, 48 | "engines": { 49 | "node": ">=8.10.0" 50 | }, 51 | "homepage": "https://github.com/stefanwalther/qix-graphql#readme", 52 | "dependencies": { 53 | "body-parser": "^1.18.2", 54 | "compression": "^1.7.2", 55 | "cross-env": "^5.1.5", 56 | "enigma.js": "^2.2.1", 57 | "express": "^4.16.3", 58 | "express-graphql": "^0.6.12", 59 | "express-initializers": "0.0.1", 60 | "express-result": "^0.1.4", 61 | "graphql": "^0.13.0", 62 | "helmet": "^3.12.0", 63 | "js-yaml": "^3.11.0", 64 | "lodash": "^4.17.10", 65 | "read-pkg-up": "^3.0.0", 66 | "superagent-graphql": "^0.1.2", 67 | "swagger-ui-express": "^2.0.8", 68 | "winster": "^0.2.11", 69 | "ws": "^3.3.3" 70 | }, 71 | "devDependencies": { 72 | "chai": "4.1.2", 73 | "chai-subset": "1.6.0", 74 | "eslint": "4.19.1", 75 | "eslint-config-space-single": "0.3.5", 76 | "http-status-codes": "1.3.0", 77 | "husky": "0.14.3", 78 | "mocha": "3.5.3", 79 | "nodemon": "1.18.4", 80 | "nyc": "11.9.0", 81 | "supertest": "3.1.0" 82 | }, 83 | "verb": { 84 | "run": true, 85 | "toc": true, 86 | "layout": "empty", 87 | "tasks": [ 88 | "readme" 89 | ], 90 | "plugins": [ 91 | "gulp-format-md" 92 | ], 93 | "related": { 94 | "list": [] 95 | }, 96 | "lint": { 97 | "reflinks": true 98 | }, 99 | "reflinks": [] 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | "schedule:monthly" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/app-server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const bodyParser = require('body-parser'); 3 | const compression = require('compression'); 4 | const helmet = require('helmet'); 5 | const initializer = require('express-initializers'); 6 | const logger = require('winster').instance(); 7 | const path = require('path'); 8 | const _ = require('lodash'); 9 | 10 | const config = require('./config/config'); 11 | 12 | class AppServer { 13 | 14 | constructor(opts = {}) { 15 | this.server = null; 16 | this.logger = logger; 17 | this.config = _.extend(_.clone(config), opts); 18 | 19 | this.app = express(); 20 | this.app.use(compression()); 21 | this.app.use(helmet()); 22 | this.app.use(bodyParser.json()); 23 | } 24 | 25 | /** 26 | * Start the GraphQL server. 27 | */ 28 | async start() { 29 | 30 | await initializer(this.app, {directory: path.join(__dirname, 'initializers')}); 31 | 32 | this.server = this.app.listen(this.config.PORT); 33 | this.logger.info(`Express server started: 34 | - host ${this.config.HOST} 35 | - port ${this.config.PORT} 36 | - qix-host ${this.config.QIX_PORT} 37 | - qix-port ${this.config.QIX_PORT} 38 | - env "${this.config.NODE_ENV}"`); 39 | } 40 | 41 | /** 42 | * Stop the GraphQL server. 43 | */ 44 | async stop() { 45 | await this.server.close(); 46 | this.logger.info('Server stopped'); 47 | } 48 | } 49 | 50 | module.exports = AppServer; 51 | 52 | -------------------------------------------------------------------------------- /src/config/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | HOST: process.env.HOST || 'localhost', 3 | PORT: 3004, 4 | NODE_ENV: process.env.NODE_ENV || 'development', 5 | QIX_HOST: process.env.QIX_HOST || 'qix', 6 | QIX_PORT: process.env.QIX_PORT || 9076 7 | }; 8 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const AppServer = require('./app-server'); 2 | 3 | (async () => { 4 | let appServer = new AppServer(); 5 | await appServer.start(); 6 | })(); 7 | -------------------------------------------------------------------------------- /src/initializers/routes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); // eslint-disable-line new-cap 3 | const config = require('./../config/config'); 4 | 5 | // API Docs 6 | // Todo: Just crap for now 7 | // router.use('/', require('./../modules/api-docs/api-docs.routes')); 8 | 9 | // Health-check 10 | router.use('/', require('./../modules/health-check/health-check.routes.js')); 11 | 12 | // Global 13 | router.use('/', require('./../modules/global/global.routes')); 14 | 15 | // Router.use('/doc/:id', ); 16 | router.use('/', require('./../modules/doc/doc.routes')); 17 | 18 | // Fallback / root 19 | router.use('/', (req, res) => { 20 | res.json({ 21 | _links: { 22 | _self: `http://${config.HOST}:${config.PORT}`, 23 | global: `http://${config.HOST}:${config.PORT}/global/graphql`, 24 | 'health-check': `http://${config.HOST}:${config.PORT}/health-check` 25 | } 26 | }); 27 | }); 28 | 29 | module.exports = { 30 | // After: 'passport', 31 | configure: app => { 32 | app.use(router); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /src/lib/lib.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | 4 | /** 5 | * Sanitize table-names and field-names to be used in the GraphQL editor. 6 | * 7 | * Spaces and special characters will all be replaced with the underscore character ("_"). 8 | * 9 | * @param {String} s - String to be checked. 10 | * @returns {String} - The normalized string. 11 | */ 12 | sanitize: s => { 13 | s = s.replace(/ /g, '_'); // eslint-disable-line no-useless-escape 14 | s = s.replace(/[&\/\\#,+()$~%.'":*?<>{}]/g, '_'); // eslint-disable-line no-useless-escape 15 | return s; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /src/lib/qix-lib.js: -------------------------------------------------------------------------------- 1 | const enigma = require('enigma.js'); 2 | const WebSocket = require('ws'); 3 | const qixSchema = require('enigma.js/schemas/12.20.0.json'); 4 | const config = require('./../config/config'); 5 | 6 | class QixLib { 7 | 8 | /** 9 | * Get TablesAndKeys from the QIX engine. 10 | * 11 | * @param options 12 | * @param options.qDocName 13 | * 14 | * @returns {Promise.} 15 | */ 16 | static async getTablesAndKeys(options) { 17 | 18 | const session = enigma.create({ 19 | schema: qixSchema, 20 | url: `ws://${config.QIX_HOST}:${config.QIX_PORT}/app/engineData`, 21 | createSocket: url => new WebSocket(url) 22 | }); 23 | 24 | let global = await session.open(); 25 | let doc = await global.openDoc({qDocName: options.qDocName, qNoData: false}); 26 | let table_and_keys = await doc.getTablesAndKeys({qIncludeSysVars: true}); 27 | 28 | await session.close(); 29 | return table_and_keys; 30 | } 31 | 32 | } 33 | 34 | module.exports = QixLib; 35 | -------------------------------------------------------------------------------- /src/lib/schema-cache.js: -------------------------------------------------------------------------------- 1 | 2 | // The idea of a schema cache has been introduced here as generating a schema is pretty expensive as the app 3 | // has to be opened with data, opening the data without the data will not return the table_and_keys properly. 4 | // 5 | // For now, this is a very simple implementation, which can 6 | // a) either be completely removed if the engine can return the meta data without opening the app with data 7 | // b) be extended in a multi-server environment with e.g. a Redis cache 8 | 9 | class SchemaCache { 10 | constructor() { 11 | this.cache = {}; 12 | } 13 | 14 | find(qDocId) { 15 | if (this.cache[qDocId]) { 16 | return this.cache[qDocId]; 17 | } 18 | } 19 | 20 | /** 21 | * Returns if the doc with the given qDocId exists in cache or not. 22 | * @param qDocId 23 | * @returns {boolean} 24 | */ 25 | exists(qDocId) { 26 | return (this.cache[qDocId] !== (null || undefined)); 27 | } 28 | 29 | /** 30 | * Adds or replaces a cache. 31 | * @param qDocId 32 | */ 33 | add(qDocId, val) { 34 | this.cache[qDocId] = val; 35 | } 36 | 37 | count() { 38 | return Object.keys(this.cache).length; 39 | } 40 | 41 | reset() { 42 | this.cache = {}; 43 | } 44 | } 45 | 46 | module.exports = new SchemaCache(); 47 | -------------------------------------------------------------------------------- /src/modules/api-docs/api-docs.routes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); // eslint-disable-line new-cap 3 | const swaggerUi = require('swagger-ui-express'); 4 | const yaml = require('js-yaml'); 5 | const pkg = require('read-pkg-up').sync().pkg; 6 | const fs = require('fs'); 7 | const path = require('path'); 8 | 9 | // Api-docs 10 | const swaggerDoc = yaml.safeLoad(fs.readFileSync(path.join(__dirname, './api-docs.yml'), 'utf8')); 11 | swaggerDoc.info.version = pkg.version; 12 | router.use('/api-docs/', swaggerUi.serve, swaggerUi.setup(swaggerDoc)); 13 | 14 | module.exports = router; 15 | -------------------------------------------------------------------------------- /src/modules/api-docs/api-docs.yml: -------------------------------------------------------------------------------- 1 | swagger: '2.0' 2 | 3 | info: 4 | version: "0.0.1" 5 | title: qix-graphql 6 | license: 7 | name: MIT 8 | host: localhost 9 | schemes: 10 | - http 11 | - https 12 | basePath: /v1 13 | consumes: 14 | - application/json 15 | produces: 16 | - application/json 17 | 18 | ################################################################################ 19 | # Paths # 20 | ################################################################################ 21 | paths: 22 | /logs: 23 | post: 24 | description: Post `log` object. 25 | parameters: 26 | - name: log 27 | in: body 28 | schema: 29 | $ref: '#/definitions/log' 30 | responses: 31 | 201: 32 | description: Successfully created. 33 | schema: 34 | $ref: '#/definitions/logResult' 35 | tags: 36 | - Logs 37 | get: 38 | description: Get all `log` objects. 39 | responses: 40 | 200: 41 | description: OK 42 | schema: 43 | $ref: '#/definitions/logResult' 44 | tags: 45 | - Logs 46 | delete: 47 | description: Delete all log entries. 48 | summary: Delete all log entries. 49 | responses: 50 | 200: 51 | description: OK 52 | 53 | 54 | /logs/:id: 55 | get: 56 | description: Get log entry by Id. 57 | responses: 58 | 200: 59 | description: OK 60 | schema: 61 | $ref: '#/definitions/logResult' 62 | tags: 63 | - Logs 64 | delete: 65 | description: Delete log entry by Id. 66 | responses: 67 | 200: 68 | description: OK 69 | 70 | 71 | ################################################################################ 72 | # Definitions # 73 | ################################################################################ 74 | definitions: 75 | log: 76 | type: object 77 | properties: 78 | name: 79 | type: string 80 | source: 81 | type: string 82 | level: 83 | type: string 84 | enum: 85 | - fatal 86 | - error 87 | - debug 88 | - warn 89 | - data 90 | - info 91 | - verbose 92 | - trace 93 | defaultValue: info 94 | message: 95 | type: object 96 | 97 | 98 | logResult: 99 | type: object 100 | properties: 101 | _id: 102 | type: string 103 | name: 104 | type: string 105 | s5r_created_at: 106 | type: string 107 | format: date 108 | s5r_update_at: 109 | type: string 110 | format: date 111 | 112 | 113 | -------------------------------------------------------------------------------- /src/modules/doc/doc-schema-generator.js: -------------------------------------------------------------------------------- 1 | const logger = require('winster').instance(); // eslint-disable-line no-unused-vars 2 | const lib = require('../../lib/lib'); 3 | const { 4 | GraphQLSchema, 5 | GraphQLObjectType, 6 | GraphQLString, 7 | GraphQLList 8 | } = require('graphql'); 9 | 10 | class DocSchemaGenerator { 11 | 12 | /** 13 | * 14 | * @param options 15 | * @param options.qDocId 16 | * @param options.tables_and_keys 17 | */ 18 | constructor(options) { 19 | this.options = options; 20 | this.logger = logger; 21 | 22 | this._validateOptions(); 23 | 24 | this._types = {}; 25 | this._tableCache = {}; 26 | } 27 | 28 | /** 29 | * Some validations to be used when initializing the class. 30 | * @returns {boolean} 31 | * @private 32 | */ 33 | _validateOptions() { 34 | if (!this.options.qDocId) { 35 | throw new Error('qDocId is missing'); 36 | } 37 | if (!this.options.tables_and_keys) { 38 | throw new Error('tables_and_keys is missing'); 39 | } 40 | if (!this.options.tables_and_keys.qtr) { 41 | throw new Error('tables_and_keys.qtr is missing'); 42 | } 43 | return true; 44 | } 45 | 46 | /** 47 | * Options.tables_and_keys 48 | * options.table_and_keys 49 | * @param options 50 | * @param {String} options.qDocId - Id of the document. 51 | * @param {Object} options.tables_and_keys 52 | * options.nx_app_layout 53 | * 54 | * Todo: bookmarks 55 | * 56 | */ 57 | getSchema() { 58 | 59 | this._initTypes(); 60 | this._initTableCache(); 61 | 62 | return new GraphQLSchema({ 63 | query: this._getRootQuery() 64 | }); 65 | } 66 | 67 | /** 68 | * Return the root query for the given document. 69 | * 70 | * @private 71 | */ 72 | _getRootQuery() { 73 | return new GraphQLObjectType({ 74 | name: 'Tables', 75 | fields: this._getTables() 76 | }); 77 | } 78 | 79 | // Todo: Could make sense to refactor this to be a function to return the types. 80 | /** 81 | * Initializes the types based on this.options.tables_and_keys 82 | * 83 | * @private 84 | */ 85 | _initTypes() { 86 | // Console.log('generateTypes.tables_and_keys.qtr', this.options.tables_and_keys.qtr); 87 | this.options.tables_and_keys.qtr.forEach(t => { 88 | this._types[lib.sanitize(t.qName)] = new GraphQLObjectType({ 89 | name: lib.sanitize(t.qName), 90 | description: `${t.qName} table`, 91 | fields: this._getFields(t) 92 | }); 93 | }); 94 | } 95 | 96 | /** 97 | * Instantiate the internal _tableCache object, which makes it easier to work with table of this document. 98 | * 99 | * @private 100 | */ 101 | _initTableCache() { 102 | this.options.tables_and_keys.qtr.forEach(t => { 103 | let fields = []; 104 | t.qFields.forEach(f => { 105 | fields.push(lib.sanitize(f.qName)); 106 | }); 107 | this._tableCache[lib.sanitize(t.qName)] = fields; 108 | }); 109 | } 110 | 111 | /** 112 | * @Todo: Document this 113 | * 114 | * @returns {{}} 115 | * @private 116 | */ 117 | _getTables() { 118 | let r = {}; 119 | 120 | this.options.tables_and_keys.qtr.forEach(t => { 121 | let tableName = lib.sanitize(t.qName); 122 | let inputType = this._types[tableName]; 123 | let fields = this._tableCache[tableName]; 124 | r[lib.sanitize(t.qName)] = { 125 | type: new GraphQLList(inputType), 126 | resolve: (obj, args, ctx) => { 127 | // Todo(AAA): Here we can potentially pass in the list of fields 128 | return ctx.qixResolvers.resolveTable(this.options.qDocId, tableName, fields, ctx); 129 | } 130 | }; 131 | }); 132 | 133 | return r; 134 | } 135 | 136 | /** 137 | * Return the fields for a given table. 138 | * 139 | * @param {string} table - The name of the table. 140 | * 141 | * @returns {object} 142 | * @private 143 | */ 144 | _getFields(table) { 145 | let r = {}; 146 | 147 | table.qFields.forEach(f => { 148 | r[lib.sanitize(f.qName)] = { 149 | type: DocSchemaGenerator._matchTypeFromTags(f.qTags) 150 | }; 151 | }); 152 | 153 | return r; 154 | } 155 | 156 | // Todo: There are several cases we have to think of => get some insights how tagging works ... 157 | static _matchTypeFromTags(/* tags */) { 158 | 159 | return GraphQLString; 160 | 161 | // Todo: needs to be tested properly 162 | // if (tags.indexOf('$numeric')) { 163 | // return GraphQLFloat; 164 | // // eslint-disable-next-line no-else-return 165 | // } else { 166 | // return GraphQLString; 167 | // } 168 | } 169 | 170 | } 171 | 172 | module.exports = DocSchemaGenerator; 173 | -------------------------------------------------------------------------------- /src/modules/doc/doc.resolvers.js: -------------------------------------------------------------------------------- 1 | const enigma = require('../../../node_modules/enigma.js/enigma'); 2 | const WebSocket = require('ws'); 3 | // Todo: check the version of the schema we should use with the given version of the QIX engine 4 | const qixSchema = require('enigma.js/schemas/12.20.0.json'); 5 | const logger = require('winster').instance(); 6 | 7 | // Todo: We should add better validation for the given context ... 8 | /** 9 | * Resolver for getting the table data. 10 | * 11 | * @param tableName 12 | * @param fields 13 | * @param {Object} ctx - The context. 14 | * @return {Promise | *} 15 | */ 16 | const resolveTable = (docId, tableName, fields, ctx) => { 17 | 18 | let docToOpen = docId; 19 | 20 | // Todo(AAA): we should be able to pass in an existing connection 21 | const session = enigma.create({ 22 | schema: qixSchema, 23 | url: `ws://${ctx.config.QIX_HOST}:${ctx.config.QIX_PORT}/app/engineData`, 24 | createSocket: url => new WebSocket(url) 25 | }); 26 | 27 | // Todo(AAA): We are not closing the session here 28 | return session.open() 29 | .then(global => global.openDoc({qDocName: docToOpen, qNoData: false})) 30 | .then(doc => { 31 | // Console.log('doc', doc); 32 | 33 | // Todo: Paging needs to be implemented here 34 | return doc.getTableData({ 35 | qOffset: 0, 36 | qRows: 10, 37 | qSyntheticMode: false, 38 | qTableName: tableName 39 | }) 40 | .then(qResult => { 41 | 42 | let result = []; 43 | 44 | qResult.forEach(qResultRow => { 45 | let resultRow = {}; 46 | qResultRow.qValue.forEach((qResultField, qResultFieldIndex) => { 47 | 48 | resultRow[fields[qResultFieldIndex]] = qResultField.qText; 49 | 50 | }); 51 | result.push(resultRow); 52 | }); 53 | 54 | return result; 55 | }); 56 | // .catch(err => { 57 | // console.log('err in getTableData', err); 58 | // }); 59 | }); 60 | // .catch(err => { 61 | // logger.error('Err in getTablesAndKeys', err); 62 | // throw err; 63 | // }, err => { 64 | // logger.error('There is another error here', err); 65 | // throw err; 66 | // }); 67 | }; 68 | 69 | /* istanbul ignore next */ 70 | const outputOptions = ctx => { 71 | logger.verbose('qixResolvers.outputOptions'); 72 | logger.verbose('==> ctx.config', ctx.config); 73 | return null; 74 | }; 75 | 76 | module.exports = { 77 | outputOptions, 78 | resolveTable 79 | }; 80 | -------------------------------------------------------------------------------- /src/modules/doc/doc.routes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); // eslint-disable-line new-cap 3 | const graphqlHTTP = require('express-graphql'); 4 | 5 | const defaultConfig = require('./../../config/config'); 6 | const docResolvers = require('./doc.resolvers'); 7 | // Const AppController = require('./app.controller'); 8 | const DocSchema = require('./doc.schema'); 9 | 10 | /** 11 | * Endpoint to generate a route for the given document. 12 | */ 13 | router.all('/doc/:qDocId/graphql', async (req, res) => { 14 | 15 | let schema = await DocSchema.generateDocSchema(req.params.qDocId); 16 | return graphqlHTTP({ 17 | schema: schema, 18 | graphiql: true, 19 | context: { 20 | config: defaultConfig, 21 | qixResolvers: docResolvers 22 | } 23 | })(req, res); 24 | }); 25 | 26 | module.exports = router; 27 | -------------------------------------------------------------------------------- /src/modules/doc/doc.schema.js: -------------------------------------------------------------------------------- 1 | const DocSchemaGenerator = require('./doc-schema-generator'); 2 | const QixLib = require('./../../lib/qix-lib'); 3 | 4 | // Todo: to be replaced with the schemaCache ... 5 | // let schemas = {}; 6 | 7 | class DocSchema { 8 | 9 | static async generateDocSchema(qDocId) { 10 | 11 | let tk = await QixLib.getTablesAndKeys({qDocName: qDocId}); 12 | let generator = new DocSchemaGenerator({ 13 | qDocId: qDocId, 14 | tables_and_keys: tk 15 | }); 16 | return generator.getSchema(); 17 | } 18 | 19 | // ------------------------------------------------------------------------------------------------------------------- 20 | 21 | // Todo: Old implementation using the cache 22 | // static async genSchema(qDocId) { 23 | // return new Promise((resolve, reject) => { 24 | // if (schemas[qDocId]) { 25 | // logger.verbose('Returning schema from cache', qDocId); 26 | // return resolve(schemas[qDocId]); 27 | // } 28 | // logger.verbose('Creating schema: ', qDocId); 29 | // SchemaGenerator.generateSchema({qDocId}) 30 | // .then(schema => { 31 | // logger.verbose('==> OK, we got a schema'); 32 | // schemas[qDocId] = schema; 33 | // resolve(schema); 34 | // }) 35 | // .catch(err => { 36 | // logger.error('We have an error creating the schema', err); 37 | // reject(err); 38 | // }); 39 | // }); 40 | // 41 | // } 42 | 43 | } 44 | 45 | module.exports = DocSchema; 46 | -------------------------------------------------------------------------------- /src/modules/global/global.resolvers.js: -------------------------------------------------------------------------------- 1 | const enigma = require('enigma.js'); 2 | const WebSocket = require('ws'); 3 | const schema = require('enigma.js/schemas/12.20.0.json'); 4 | const config = require('./../../config/config'); // Todo: we ignore the context here!?!?! 5 | const _ = require('lodash'); 6 | 7 | class Resolvers { 8 | 9 | /** 10 | * Return a list of documents. 11 | * 12 | * @returns {Promise<*>} 13 | */ 14 | static async getDocs() { 15 | 16 | let retVal; 17 | const session = enigma.create({ 18 | schema, 19 | url: `ws://${config.QIX_HOST}:${config.QIX_PORT}`, 20 | createSocket: url => new WebSocket(url) 21 | }); 22 | 23 | let global = await session.open(); 24 | let docs = await global.getDocList(); 25 | 26 | docs.map(doc => { // eslint-disable-line array-callback-return 27 | doc._links = { 28 | _doc: `http://${config.HOST}:${config.PORT}/doc/${encodeURIComponent(doc.qDocId)}/graphql` 29 | }; 30 | }); 31 | retVal = docs; 32 | await session.close(); 33 | return retVal; 34 | } 35 | 36 | /** 37 | * Return a single document. 38 | * 39 | * @param qDocId 40 | * @returns {Promise<*>} 41 | */ 42 | static async getDoc(qDocId) { 43 | let docs = await Resolvers.getDocs(); 44 | return docs.filter(doc => doc.qDocId === qDocId)[0]; 45 | } 46 | 47 | static async getEnv() { 48 | const e = { 49 | HOST: config.HOST, 50 | PORT: _.parseInt(config.PORT), 51 | QIX_HOST: config.QIX_HOST, 52 | QIX_PORT: _.parseInt(config.QIX_PORT) 53 | }; 54 | return e; 55 | } 56 | } 57 | 58 | module.exports = Resolvers; 59 | -------------------------------------------------------------------------------- /src/modules/global/global.routes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); // eslint-disable-line new-cap 3 | const graphqlHTTP = require('express-graphql'); 4 | 5 | const graphQlSchema = require('./global.schema'); 6 | const defaultConfig = require('./../../config/config'); 7 | 8 | router.use('/global/graphql', graphqlHTTP({ 9 | schema: graphQlSchema, 10 | graphiql: true, 11 | context: { 12 | config: defaultConfig 13 | } 14 | })); 15 | 16 | module.exports = router; 17 | -------------------------------------------------------------------------------- /src/modules/global/global.schema.js: -------------------------------------------------------------------------------- 1 | const GlobalResolvers = require('./global.resolvers'); 2 | const { 3 | GraphQLNonNull, 4 | GraphQLObjectType, 5 | GraphQLSchema, 6 | GraphQLString, 7 | GraphQLList, 8 | GraphQLInt, 9 | GraphQLFloat 10 | } = require('graphql'); 11 | 12 | // Todo: implement additional fields 13 | const TableRecord = new GraphQLObjectType({ 14 | name: 'TableRecord', 15 | fields: { 16 | qName: {type: GraphQLString} 17 | // , 18 | // qLoose: {type: GraphQLBoolean}, 19 | // qNoOfRows: {type: GraphQLInt}, 20 | // // qFields: {type: FieldInTableData}, 21 | // // qPos: { type: Point}, 22 | // qComment: {type: GraphQLString}, 23 | // qIsDirectDiscovery: {type: GraphQLBoolean}, 24 | // qIsSynthetic: {type: GraphQLBoolean} 25 | } 26 | }); 27 | 28 | // Todo: implement additional fields 29 | // eslint-disable-next-line no-unused-vars 30 | const TableAndKeys = new GraphQLObjectType({ 31 | name: 'TableAndKeys', 32 | fields: { 33 | qtr: {type: new GraphQLList(TableRecord)} 34 | // , 35 | // qk: {new GraphQLList(SourceKeyRecord)} 36 | } 37 | }); 38 | 39 | const ConfigType = new GraphQLObjectType({ 40 | name: 'ConfigType', 41 | fields: { 42 | HOST: { 43 | type: GraphQLString, 44 | description: 'The host of the GraphQL server' 45 | }, 46 | PORT: { 47 | type: GraphQLInt, 48 | description: 'The port of the GraphQL server' 49 | }, 50 | QIX_HOST: { 51 | type: GraphQLString, 52 | description: 'The host of the QIX server' 53 | }, 54 | QIX_PORT: { 55 | type: GraphQLInt, 56 | description: 'The port of the QIX server' 57 | } 58 | } 59 | }); 60 | 61 | const DocType = new GraphQLObjectType({ 62 | name: 'DocType', 63 | 64 | fields: { 65 | qDocName: { 66 | type: GraphQLString, 67 | description: 'Name of the document' 68 | }, 69 | qConnectedUsers: {type: GraphQLInt}, 70 | qFileTime: {type: GraphQLFloat}, 71 | qFileSize: {type: GraphQLFloat}, 72 | qDocId: { 73 | type: GraphQLString, 74 | description: 'Id of the document' 75 | }, 76 | qMeta: { 77 | type: new GraphQLObjectType({ 78 | name: 'qMeta', 79 | fields: { 80 | description: {type: GraphQLString}, 81 | qFileSize: {type: GraphQLFloat}, 82 | dynamicColor: {type: GraphQLString} 83 | } 84 | }) 85 | }, 86 | qLastReloadTime: {type: GraphQLString}, 87 | qTitle: { 88 | type: GraphQLString, 89 | description: 'Title of the document' 90 | }, 91 | qThumbNail: { 92 | type: new GraphQLObjectType({ 93 | name: 'qThumbnail', 94 | fields: { 95 | qUrl: {type: GraphQLString} 96 | } 97 | }) 98 | }, 99 | _links: { 100 | type: new GraphQLObjectType({ 101 | name: '_links', 102 | fields: { 103 | _doc: {type: GraphQLString} 104 | } 105 | }) 106 | } 107 | } 108 | }); 109 | 110 | const RootQueryType = new GraphQLObjectType({ 111 | name: 'Global', 112 | fields: { 113 | doc: { 114 | type: DocType, 115 | description: 'Return a single document.', 116 | args: { 117 | qDocId: { 118 | type: new GraphQLNonNull(GraphQLString) 119 | } 120 | }, 121 | resolve: (obj, args /* , ctx */) => { 122 | return GlobalResolvers.getDoc(args.qDocId); 123 | } 124 | }, 125 | docs: { 126 | type: new GraphQLList(DocType), 127 | description: 'Return all Qlik documents available in the current environment.', 128 | resolve: (/* obj, args, ctx */) => { 129 | return GlobalResolvers.getDocs(); 130 | } 131 | }, 132 | env: { 133 | type: ConfigType, 134 | description: 'Return the configuration of the entire environment.', 135 | resolve: (/* obj, args, ctx */) => { 136 | return GlobalResolvers.getEnv(); 137 | } 138 | } 139 | } 140 | }); 141 | 142 | const schema = new GraphQLSchema({ 143 | query: RootQueryType 144 | }); 145 | 146 | module.exports = schema; 147 | -------------------------------------------------------------------------------- /src/modules/health-check/health-check.controller.js: -------------------------------------------------------------------------------- 1 | const pkg = require('read-pkg-up').sync().pkg; 2 | 3 | class HealthController { 4 | 5 | static get(req, res) { 6 | res.setHeader('Content-Type', 'application/json'); 7 | res.send({ 8 | ts: new Date().toJSON(), 9 | version: pkg.version, 10 | name: pkg.name, 11 | repository: pkg.repository 12 | }); 13 | } 14 | } 15 | 16 | module.exports = HealthController; 17 | -------------------------------------------------------------------------------- /src/modules/health-check/health-check.routes.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); // eslint-disable-line new-cap 3 | const HealthCheckController = require('./health-check.controller.js'); 4 | 5 | /** 6 | * @swagger 7 | * 8 | * definitions: 9 | * HealthCheckResult: 10 | * type: object 11 | * properties: 12 | * ts: 13 | * type: string 14 | * format: date-time 15 | * description: Timestamp of the returned health-check result. 16 | * example: "2018-03-24T23:05:28.341Z" 17 | * name: 18 | * type: string 19 | * description: "The name of the service." 20 | * example: "auth-service" 21 | * repository: 22 | * type: object 23 | * properties: 24 | * type: 25 | * type: string 26 | * example: "git" 27 | * url: 28 | * type: string 29 | * example: "https://github.com/sammler/auth-service" 30 | * version: 31 | * type: string 32 | * description: "The current version of the service." 33 | * example: "0.1.0" 34 | * 35 | * 36 | * /health-check: 37 | * get: 38 | * description: Get the status of the auth-server. 39 | * security: [] 40 | * produces: 41 | * - application/json 42 | * tags: 43 | * - health-check 44 | * responses: 45 | * 200: 46 | * description: Returned health-check status. 47 | * schema: 48 | * $ref: '#/definitions/HealthCheckResult' 49 | */ 50 | router.get('/health-check', HealthCheckController.get); 51 | 52 | module.exports = router; 53 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es6": true, 5 | "mocha": true 6 | }, 7 | "plugins": [ 8 | "mocha" 9 | ], 10 | "rules": { 11 | "mocha/no-exclusive-tests": "error", 12 | "no-inline-comments": "off", 13 | "no-undef": "off", 14 | "no-unused-vars": "off", 15 | "no-unused-expressions": "off" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/fixtures/TablesAndKeys-CRM.json: -------------------------------------------------------------------------------- 1 | { 2 | "qtr": [ 3 | { 4 | "qName": "account", 5 | "qNoOfRows": 8701, 6 | "qFields": [ 7 | { 8 | "qName": "AccountId", 9 | "qOriginalFields": [ ], 10 | "qPresent": true, 11 | "qInformationDensity": 1, 12 | "qnNonNulls": 8701, 13 | "qnRows": 8701, 14 | "qSubsetRatio": 1, 15 | "qnTotalDistinctValues": 8701, 16 | "qnPresentDistinctValues": 8701, 17 | "qKeyType": "ANY_KEY", 18 | "qTags": [ 19 | "$key", 20 | "$ascii", 21 | "$text" 22 | ], 23 | "qDerivedFields": [ ] 24 | }, 25 | { 26 | "qName": "Account Rep Name", 27 | "qOriginalFields": [ ], 28 | "qPresent": true, 29 | "qHasDuplicates": true, 30 | "qInformationDensity": 1, 31 | "qnNonNulls": 8701, 32 | "qnRows": 8701, 33 | "qSubsetRatio": 1, 34 | "qnTotalDistinctValues": 8644, 35 | "qnPresentDistinctValues": 8644, 36 | "qKeyType": "NOT_KEY", 37 | "qTags": [ ], 38 | "qDerivedFields": [ ] 39 | }, 40 | { 41 | "qName": "Account Type", 42 | "qOriginalFields": [ ], 43 | "qPresent": true, 44 | "qHasDuplicates": true, 45 | "qInformationDensity": 1, 46 | "qnNonNulls": 8701, 47 | "qnRows": 8701, 48 | "qSubsetRatio": 1, 49 | "qnTotalDistinctValues": 5, 50 | "qnPresentDistinctValues": 5, 51 | "qKeyType": "NOT_KEY", 52 | "qTags": [ 53 | "$ascii", 54 | "$text" 55 | ], 56 | "qDerivedFields": [ ] 57 | }, 58 | { 59 | "qName": "Account Billing Street", 60 | "qOriginalFields": [ ], 61 | "qPresent": true, 62 | "qHasDuplicates": true, 63 | "qInformationDensity": 1, 64 | "qnNonNulls": 8701, 65 | "qnRows": 8701, 66 | "qSubsetRatio": 1, 67 | "qnTotalDistinctValues": 8462, 68 | "qnPresentDistinctValues": 8462, 69 | "qKeyType": "NOT_KEY", 70 | "qTags": [ 71 | "$text" 72 | ], 73 | "qDerivedFields": [ ] 74 | }, 75 | { 76 | "qName": "Account Billing City", 77 | "qOriginalFields": [ ], 78 | "qPresent": true, 79 | "qHasDuplicates": true, 80 | "qInformationDensity": 1, 81 | "qnNonNulls": 8701, 82 | "qnRows": 8701, 83 | "qSubsetRatio": 1, 84 | "qnTotalDistinctValues": 3537, 85 | "qnPresentDistinctValues": 3537, 86 | "qKeyType": "NOT_KEY", 87 | "qTags": [ 88 | "$text" 89 | ], 90 | "qDerivedFields": [ ] 91 | }, 92 | { 93 | "qName": "Account Billing State", 94 | "qOriginalFields": [ ], 95 | "qPresent": true, 96 | "qHasDuplicates": true, 97 | "qInformationDensity": 1, 98 | "qnNonNulls": 8701, 99 | "qnRows": 8701, 100 | "qSubsetRatio": 1, 101 | "qnTotalDistinctValues": 740, 102 | "qnPresentDistinctValues": 740, 103 | "qKeyType": "NOT_KEY", 104 | "qTags": [ ], 105 | "qDerivedFields": [ ] 106 | }, 107 | { 108 | "qName": "Account Billing Zip", 109 | "qOriginalFields": [ ], 110 | "qPresent": true, 111 | "qHasDuplicates": true, 112 | "qInformationDensity": 1, 113 | "qnNonNulls": 8701, 114 | "qnRows": 8701, 115 | "qSubsetRatio": 1, 116 | "qnTotalDistinctValues": 6562, 117 | "qnPresentDistinctValues": 6562, 118 | "qKeyType": "NOT_KEY", 119 | "qTags": [ 120 | "$text" 121 | ], 122 | "qDerivedFields": [ ] 123 | }, 124 | { 125 | "qName": "Account Billing Country", 126 | "qOriginalFields": [ ], 127 | "qPresent": true, 128 | "qHasDuplicates": true, 129 | "qInformationDensity": 1, 130 | "qnNonNulls": 8701, 131 | "qnRows": 8701, 132 | "qSubsetRatio": 1, 133 | "qnTotalDistinctValues": 10, 134 | "qnPresentDistinctValues": 10, 135 | "qKeyType": "NOT_KEY", 136 | "qTags": [ 137 | "$ascii", 138 | "$text" 139 | ], 140 | "qDerivedFields": [ ] 141 | }, 142 | { 143 | "qName": "Account Industry", 144 | "qOriginalFields": [ ], 145 | "qPresent": true, 146 | "qHasDuplicates": true, 147 | "qInformationDensity": 1, 148 | "qnNonNulls": 8701, 149 | "qnRows": 8701, 150 | "qSubsetRatio": 1, 151 | "qnTotalDistinctValues": 338, 152 | "qnPresentDistinctValues": 338, 153 | "qKeyType": "NOT_KEY", 154 | "qTags": [ 155 | "$text" 156 | ], 157 | "qDerivedFields": [ ] 158 | } 159 | ], 160 | "qPos": { 161 | "qx": 0, 162 | "qy": 0 163 | } 164 | }, 165 | { 166 | "qName": "opportunity", 167 | "qNoOfRows": 13229, 168 | "qFields": [ 169 | { 170 | "qName": "AccountId", 171 | "qOriginalFields": [ ], 172 | "qPresent": true, 173 | "qHasDuplicates": true, 174 | "qInformationDensity": 1, 175 | "qnNonNulls": 13229, 176 | "qnRows": 13229, 177 | "qSubsetRatio": 0.9987357774968394, 178 | "qnTotalDistinctValues": 8701, 179 | "qnPresentDistinctValues": 8690, 180 | "qKeyType": "ANY_KEY", 181 | "qTags": [ 182 | "$key", 183 | "$ascii", 184 | "$text" 185 | ], 186 | "qDerivedFields": [ ] 187 | }, 188 | { 189 | "qName": "OpportunityId", 190 | "qOriginalFields": [ ], 191 | "qPresent": true, 192 | "qInformationDensity": 1, 193 | "qnNonNulls": 13229, 194 | "qnRows": 13229, 195 | "qSubsetRatio": 0.9981890892628085, 196 | "qnTotalDistinctValues": 13253, 197 | "qnPresentDistinctValues": 13229, 198 | "qKeyType": "ANY_KEY", 199 | "qTags": [ 200 | "$key", 201 | "$ascii", 202 | "$text" 203 | ], 204 | "qDerivedFields": [ ] 205 | }, 206 | { 207 | "qName": "User Id", 208 | "qOriginalFields": [ ], 209 | "qPresent": true, 210 | "qHasDuplicates": true, 211 | "qInformationDensity": 1, 212 | "qnNonNulls": 13229, 213 | "qnRows": 13229, 214 | "qSubsetRatio": 0.9974226804123711, 215 | "qnTotalDistinctValues": 388, 216 | "qnPresentDistinctValues": 387, 217 | "qKeyType": "ANY_KEY", 218 | "qTags": [ 219 | "$key", 220 | "$ascii", 221 | "$text" 222 | ], 223 | "qDerivedFields": [ ] 224 | }, 225 | { 226 | "qName": "OpportunityIdCount", 227 | "qOriginalFields": [ ], 228 | "qPresent": true, 229 | "qInformationDensity": 1, 230 | "qnNonNulls": 13229, 231 | "qnRows": 13229, 232 | "qSubsetRatio": 1, 233 | "qnTotalDistinctValues": 13229, 234 | "qnPresentDistinctValues": 13229, 235 | "qKeyType": "NOT_KEY", 236 | "qTags": [ 237 | "$ascii", 238 | "$text" 239 | ], 240 | "qDerivedFields": [ ] 241 | }, 242 | { 243 | "qName": "Opportunity_Count", 244 | "qOriginalFields": [ ], 245 | "qPresent": true, 246 | "qHasDuplicates": true, 247 | "qInformationDensity": 1, 248 | "qnNonNulls": 13229, 249 | "qnRows": 13229, 250 | "qSubsetRatio": 1, 251 | "qnTotalDistinctValues": 1, 252 | "qnPresentDistinctValues": 1, 253 | "qKeyType": "NOT_KEY", 254 | "qTags": [ 255 | "$numeric", 256 | "$integer" 257 | ], 258 | "qDerivedFields": [ ] 259 | }, 260 | { 261 | "qName": "Opportunity Name", 262 | "qOriginalFields": [ ], 263 | "qPresent": true, 264 | "qInformationDensity": 1, 265 | "qnNonNulls": 13229, 266 | "qnRows": 13229, 267 | "qSubsetRatio": 1, 268 | "qnTotalDistinctValues": 13229, 269 | "qnPresentDistinctValues": 13229, 270 | "qKeyType": "NOT_KEY", 271 | "qTags": [ 272 | "$ascii", 273 | "$text" 274 | ], 275 | "qDerivedFields": [ ] 276 | }, 277 | { 278 | "qName": "Customer Account Id", 279 | "qOriginalFields": [ ], 280 | "qPresent": true, 281 | "qHasNull": true, 282 | "qHasDuplicates": true, 283 | "qInformationDensity": 0.45800891979741476, 284 | "qnNonNulls": 6059, 285 | "qnRows": 13229, 286 | "qSubsetRatio": 1, 287 | "qnTotalDistinctValues": 3797, 288 | "qnPresentDistinctValues": 3797, 289 | "qKeyType": "NOT_KEY", 290 | "qTags": [ 291 | "$ascii", 292 | "$text" 293 | ], 294 | "qDerivedFields": [ ] 295 | }, 296 | { 297 | "qName": "Opportunity Stage", 298 | "qOriginalFields": [ ], 299 | "qPresent": true, 300 | "qHasDuplicates": true, 301 | "qInformationDensity": 1, 302 | "qnNonNulls": 13229, 303 | "qnRows": 13229, 304 | "qSubsetRatio": 1, 305 | "qnTotalDistinctValues": 7, 306 | "qnPresentDistinctValues": 7, 307 | "qKeyType": "NOT_KEY", 308 | "qTags": [ 309 | "$ascii", 310 | "$text" 311 | ], 312 | "qDerivedFields": [ ] 313 | }, 314 | { 315 | "qName": "Opportunity Amount", 316 | "qOriginalFields": [ ], 317 | "qPresent": true, 318 | "qHasDuplicates": true, 319 | "qInformationDensity": 1, 320 | "qnNonNulls": 13229, 321 | "qnRows": 13229, 322 | "qSubsetRatio": 1, 323 | "qnTotalDistinctValues": 6671, 324 | "qnPresentDistinctValues": 6671, 325 | "qKeyType": "NOT_KEY", 326 | "qTags": [ 327 | "$numeric" 328 | ], 329 | "qDerivedFields": [ ] 330 | }, 331 | { 332 | "qName": "ExRate_ARS", 333 | "qOriginalFields": [ ], 334 | "qPresent": true, 335 | "qHasDuplicates": true, 336 | "qInformationDensity": 1, 337 | "qnNonNulls": 13229, 338 | "qnRows": 13229, 339 | "qSubsetRatio": 1, 340 | "qnTotalDistinctValues": 6, 341 | "qnPresentDistinctValues": 6, 342 | "qKeyType": "NOT_KEY", 343 | "qTags": [ 344 | "$numeric" 345 | ], 346 | "qDerivedFields": [ ] 347 | }, 348 | { 349 | "qName": "ExRate_AUD", 350 | "qOriginalFields": [ ], 351 | "qPresent": true, 352 | "qHasDuplicates": true, 353 | "qInformationDensity": 1, 354 | "qnNonNulls": 13229, 355 | "qnRows": 13229, 356 | "qSubsetRatio": 1, 357 | "qnTotalDistinctValues": 6, 358 | "qnPresentDistinctValues": 6, 359 | "qKeyType": "NOT_KEY", 360 | "qTags": [ 361 | "$numeric" 362 | ], 363 | "qDerivedFields": [ ] 364 | }, 365 | { 366 | "qName": "ExRate_BRL", 367 | "qOriginalFields": [ ], 368 | "qPresent": true, 369 | "qHasDuplicates": true, 370 | "qInformationDensity": 1, 371 | "qnNonNulls": 13229, 372 | "qnRows": 13229, 373 | "qSubsetRatio": 1, 374 | "qnTotalDistinctValues": 6, 375 | "qnPresentDistinctValues": 6, 376 | "qKeyType": "NOT_KEY", 377 | "qTags": [ 378 | "$numeric" 379 | ], 380 | "qDerivedFields": [ ] 381 | }, 382 | { 383 | "qName": "ExRate_CAD", 384 | "qOriginalFields": [ ], 385 | "qPresent": true, 386 | "qHasDuplicates": true, 387 | "qInformationDensity": 1, 388 | "qnNonNulls": 13229, 389 | "qnRows": 13229, 390 | "qSubsetRatio": 1, 391 | "qnTotalDistinctValues": 6, 392 | "qnPresentDistinctValues": 6, 393 | "qKeyType": "NOT_KEY", 394 | "qTags": [ 395 | "$numeric" 396 | ], 397 | "qDerivedFields": [ ] 398 | }, 399 | { 400 | "qName": "ExRate_CHF", 401 | "qOriginalFields": [ ], 402 | "qPresent": true, 403 | "qHasDuplicates": true, 404 | "qInformationDensity": 1, 405 | "qnNonNulls": 13229, 406 | "qnRows": 13229, 407 | "qSubsetRatio": 1, 408 | "qnTotalDistinctValues": 6, 409 | "qnPresentDistinctValues": 6, 410 | "qKeyType": "NOT_KEY", 411 | "qTags": [ 412 | "$numeric" 413 | ], 414 | "qDerivedFields": [ ] 415 | }, 416 | { 417 | "qName": "ExRate_DKK", 418 | "qOriginalFields": [ ], 419 | "qPresent": true, 420 | "qHasDuplicates": true, 421 | "qInformationDensity": 1, 422 | "qnNonNulls": 13229, 423 | "qnRows": 13229, 424 | "qSubsetRatio": 1, 425 | "qnTotalDistinctValues": 6, 426 | "qnPresentDistinctValues": 6, 427 | "qKeyType": "NOT_KEY", 428 | "qTags": [ 429 | "$numeric" 430 | ], 431 | "qDerivedFields": [ ] 432 | }, 433 | { 434 | "qName": "ExRate_EUR", 435 | "qOriginalFields": [ ], 436 | "qPresent": true, 437 | "qHasDuplicates": true, 438 | "qInformationDensity": 1, 439 | "qnNonNulls": 13229, 440 | "qnRows": 13229, 441 | "qSubsetRatio": 1, 442 | "qnTotalDistinctValues": 6, 443 | "qnPresentDistinctValues": 6, 444 | "qKeyType": "NOT_KEY", 445 | "qTags": [ 446 | "$numeric" 447 | ], 448 | "qDerivedFields": [ ] 449 | }, 450 | { 451 | "qName": "ExRate_GBP", 452 | "qOriginalFields": [ ], 453 | "qPresent": true, 454 | "qHasDuplicates": true, 455 | "qInformationDensity": 1, 456 | "qnNonNulls": 13229, 457 | "qnRows": 13229, 458 | "qSubsetRatio": 1, 459 | "qnTotalDistinctValues": 6, 460 | "qnPresentDistinctValues": 6, 461 | "qKeyType": "NOT_KEY", 462 | "qTags": [ 463 | "$numeric" 464 | ], 465 | "qDerivedFields": [ ] 466 | }, 467 | { 468 | "qName": "ExRate_HKD", 469 | "qOriginalFields": [ ], 470 | "qPresent": true, 471 | "qHasDuplicates": true, 472 | "qInformationDensity": 1, 473 | "qnNonNulls": 13229, 474 | "qnRows": 13229, 475 | "qSubsetRatio": 1, 476 | "qnTotalDistinctValues": 6, 477 | "qnPresentDistinctValues": 6, 478 | "qKeyType": "NOT_KEY", 479 | "qTags": [ 480 | "$numeric" 481 | ], 482 | "qDerivedFields": [ ] 483 | }, 484 | { 485 | "qName": "ExRate_INR", 486 | "qOriginalFields": [ ], 487 | "qPresent": true, 488 | "qHasDuplicates": true, 489 | "qInformationDensity": 1, 490 | "qnNonNulls": 13229, 491 | "qnRows": 13229, 492 | "qSubsetRatio": 1, 493 | "qnTotalDistinctValues": 6, 494 | "qnPresentDistinctValues": 6, 495 | "qKeyType": "NOT_KEY", 496 | "qTags": [ 497 | "$numeric" 498 | ], 499 | "qDerivedFields": [ ] 500 | }, 501 | { 502 | "qName": "ExRate_JPY", 503 | "qOriginalFields": [ ], 504 | "qPresent": true, 505 | "qHasDuplicates": true, 506 | "qInformationDensity": 1, 507 | "qnNonNulls": 13229, 508 | "qnRows": 13229, 509 | "qSubsetRatio": 1, 510 | "qnTotalDistinctValues": 6, 511 | "qnPresentDistinctValues": 6, 512 | "qKeyType": "NOT_KEY", 513 | "qTags": [ 514 | "$numeric" 515 | ], 516 | "qDerivedFields": [ ] 517 | }, 518 | { 519 | "qName": "ExRate_MXN", 520 | "qOriginalFields": [ ], 521 | "qPresent": true, 522 | "qHasDuplicates": true, 523 | "qInformationDensity": 1, 524 | "qnNonNulls": 13229, 525 | "qnRows": 13229, 526 | "qSubsetRatio": 1, 527 | "qnTotalDistinctValues": 6, 528 | "qnPresentDistinctValues": 6, 529 | "qKeyType": "NOT_KEY", 530 | "qTags": [ 531 | "$numeric" 532 | ], 533 | "qDerivedFields": [ ] 534 | }, 535 | { 536 | "qName": "ExRate_NOK", 537 | "qOriginalFields": [ ], 538 | "qPresent": true, 539 | "qHasDuplicates": true, 540 | "qInformationDensity": 1, 541 | "qnNonNulls": 13229, 542 | "qnRows": 13229, 543 | "qSubsetRatio": 1, 544 | "qnTotalDistinctValues": 6, 545 | "qnPresentDistinctValues": 6, 546 | "qKeyType": "NOT_KEY", 547 | "qTags": [ 548 | "$numeric" 549 | ], 550 | "qDerivedFields": [ ] 551 | }, 552 | { 553 | "qName": "ExRate_SEK", 554 | "qOriginalFields": [ ], 555 | "qPresent": true, 556 | "qHasDuplicates": true, 557 | "qInformationDensity": 1, 558 | "qnNonNulls": 13229, 559 | "qnRows": 13229, 560 | "qSubsetRatio": 1, 561 | "qnTotalDistinctValues": 6, 562 | "qnPresentDistinctValues": 6, 563 | "qKeyType": "NOT_KEY", 564 | "qTags": [ 565 | "$numeric" 566 | ], 567 | "qDerivedFields": [ ] 568 | }, 569 | { 570 | "qName": "ExRate_SGD", 571 | "qOriginalFields": [ ], 572 | "qPresent": true, 573 | "qHasDuplicates": true, 574 | "qInformationDensity": 1, 575 | "qnNonNulls": 13229, 576 | "qnRows": 13229, 577 | "qSubsetRatio": 1, 578 | "qnTotalDistinctValues": 6, 579 | "qnPresentDistinctValues": 6, 580 | "qKeyType": "NOT_KEY", 581 | "qTags": [ 582 | "$numeric" 583 | ], 584 | "qDerivedFields": [ ] 585 | }, 586 | { 587 | "qName": "ExRate_USD", 588 | "qOriginalFields": [ ], 589 | "qPresent": true, 590 | "qHasDuplicates": true, 591 | "qInformationDensity": 1, 592 | "qnNonNulls": 13229, 593 | "qnRows": 13229, 594 | "qSubsetRatio": 1, 595 | "qnTotalDistinctValues": 6, 596 | "qnPresentDistinctValues": 6, 597 | "qKeyType": "NOT_KEY", 598 | "qTags": [ 599 | "$numeric" 600 | ], 601 | "qDerivedFields": [ ] 602 | }, 603 | { 604 | "qName": "Opportunity Probability", 605 | "qOriginalFields": [ ], 606 | "qPresent": true, 607 | "qHasDuplicates": true, 608 | "qInformationDensity": 1, 609 | "qnNonNulls": 13229, 610 | "qnRows": 13229, 611 | "qSubsetRatio": 1, 612 | "qnTotalDistinctValues": 17, 613 | "qnPresentDistinctValues": 17, 614 | "qKeyType": "NOT_KEY", 615 | "qTags": [ 616 | "$numeric" 617 | ], 618 | "qDerivedFields": [ ] 619 | }, 620 | { 621 | "qName": "Opportunity Close Date", 622 | "qOriginalFields": [ ], 623 | "qPresent": true, 624 | "qHasDuplicates": true, 625 | "qInformationDensity": 1, 626 | "qnNonNulls": 13229, 627 | "qnRows": 13229, 628 | "qSubsetRatio": 1, 629 | "qnTotalDistinctValues": 847, 630 | "qnPresentDistinctValues": 847, 631 | "qKeyType": "NOT_KEY", 632 | "qTags": [ 633 | "$numeric", 634 | "$integer", 635 | "$timestamp", 636 | "$date" 637 | ], 638 | "qDerivedFields": [ ] 639 | }, 640 | { 641 | "qName": "Opportunity Close Month", 642 | "qOriginalFields": [ ], 643 | "qPresent": true, 644 | "qHasDuplicates": true, 645 | "qInformationDensity": 1, 646 | "qnNonNulls": 13229, 647 | "qnRows": 13229, 648 | "qSubsetRatio": 1, 649 | "qnTotalDistinctValues": 12, 650 | "qnPresentDistinctValues": 12, 651 | "qKeyType": "NOT_KEY", 652 | "qTags": [ 653 | "$numeric", 654 | "$integer" 655 | ], 656 | "qDerivedFields": [ ] 657 | }, 658 | { 659 | "qName": "Opportunity Close Year", 660 | "qOriginalFields": [ ], 661 | "qPresent": true, 662 | "qHasDuplicates": true, 663 | "qInformationDensity": 1, 664 | "qnNonNulls": 13229, 665 | "qnRows": 13229, 666 | "qSubsetRatio": 1, 667 | "qnTotalDistinctValues": 3, 668 | "qnPresentDistinctValues": 3, 669 | "qKeyType": "NOT_KEY", 670 | "qTags": [ 671 | "$numeric", 672 | "$integer" 673 | ], 674 | "qDerivedFields": [ ] 675 | }, 676 | { 677 | "qName": "Opportunity Close Month/Year", 678 | "qOriginalFields": [ ], 679 | "qPresent": true, 680 | "qHasDuplicates": true, 681 | "qInformationDensity": 1, 682 | "qnNonNulls": 13229, 683 | "qnRows": 13229, 684 | "qSubsetRatio": 1, 685 | "qnTotalDistinctValues": 36, 686 | "qnPresentDistinctValues": 36, 687 | "qKeyType": "NOT_KEY", 688 | "qTags": [ 689 | "$numeric", 690 | "$integer", 691 | "$timestamp", 692 | "$date" 693 | ], 694 | "qDerivedFields": [ ] 695 | }, 696 | { 697 | "qName": "Opportunity Close Quarter", 698 | "qOriginalFields": [ ], 699 | "qPresent": true, 700 | "qHasDuplicates": true, 701 | "qInformationDensity": 1, 702 | "qnNonNulls": 13229, 703 | "qnRows": 13229, 704 | "qSubsetRatio": 1, 705 | "qnTotalDistinctValues": 4, 706 | "qnPresentDistinctValues": 4, 707 | "qKeyType": "NOT_KEY", 708 | "qTags": [ 709 | "$ascii", 710 | "$text" 711 | ], 712 | "qDerivedFields": [ ] 713 | }, 714 | { 715 | "qName": "Opportunity Close Quarter/Year", 716 | "qOriginalFields": [ ], 717 | "qPresent": true, 718 | "qHasDuplicates": true, 719 | "qInformationDensity": 1, 720 | "qnNonNulls": 13229, 721 | "qnRows": 13229, 722 | "qSubsetRatio": 1, 723 | "qnTotalDistinctValues": 12, 724 | "qnPresentDistinctValues": 12, 725 | "qKeyType": "NOT_KEY", 726 | "qTags": [ 727 | "$numeric", 728 | "$integer", 729 | "$timestamp", 730 | "$date" 731 | ], 732 | "qDerivedFields": [ ] 733 | }, 734 | { 735 | "qName": "Opportunity Close YearQuarter", 736 | "qOriginalFields": [ ], 737 | "qPresent": true, 738 | "qHasDuplicates": true, 739 | "qInformationDensity": 1, 740 | "qnNonNulls": 13229, 741 | "qnRows": 13229, 742 | "qSubsetRatio": 1, 743 | "qnTotalDistinctValues": 12, 744 | "qnPresentDistinctValues": 12, 745 | "qKeyType": "NOT_KEY", 746 | "qTags": [ 747 | "$numeric", 748 | "$integer" 749 | ], 750 | "qDerivedFields": [ ] 751 | }, 752 | { 753 | "qName": "Opportunity Type", 754 | "qOriginalFields": [ ], 755 | "qPresent": true, 756 | "qHasDuplicates": true, 757 | "qInformationDensity": 1, 758 | "qnNonNulls": 13229, 759 | "qnRows": 13229, 760 | "qSubsetRatio": 1, 761 | "qnTotalDistinctValues": 2, 762 | "qnPresentDistinctValues": 2, 763 | "qKeyType": "NOT_KEY", 764 | "qTags": [ 765 | "$ascii", 766 | "$text" 767 | ], 768 | "qDerivedFields": [ ] 769 | }, 770 | { 771 | "qName": "Opportunity Is Closed?", 772 | "qOriginalFields": [ ], 773 | "qPresent": true, 774 | "qHasDuplicates": true, 775 | "qInformationDensity": 1, 776 | "qnNonNulls": 13229, 777 | "qnRows": 13229, 778 | "qSubsetRatio": 1, 779 | "qnTotalDistinctValues": 2, 780 | "qnPresentDistinctValues": 2, 781 | "qKeyType": "NOT_KEY", 782 | "qTags": [ 783 | "$ascii", 784 | "$text" 785 | ], 786 | "qDerivedFields": [ ] 787 | }, 788 | { 789 | "qName": "Opportunity Is Won?", 790 | "qOriginalFields": [ ], 791 | "qPresent": true, 792 | "qHasDuplicates": true, 793 | "qInformationDensity": 1, 794 | "qnNonNulls": 13229, 795 | "qnRows": 13229, 796 | "qSubsetRatio": 1, 797 | "qnTotalDistinctValues": 2, 798 | "qnPresentDistinctValues": 2, 799 | "qKeyType": "NOT_KEY", 800 | "qTags": [ 801 | "$ascii", 802 | "$text" 803 | ], 804 | "qDerivedFields": [ ] 805 | }, 806 | { 807 | "qName": "Opportunity Status", 808 | "qOriginalFields": [ ], 809 | "qPresent": true, 810 | "qHasDuplicates": true, 811 | "qInformationDensity": 1, 812 | "qnNonNulls": 13229, 813 | "qnRows": 13229, 814 | "qSubsetRatio": 1, 815 | "qnTotalDistinctValues": 2, 816 | "qnPresentDistinctValues": 2, 817 | "qKeyType": "NOT_KEY", 818 | "qTags": [ 819 | "$ascii", 820 | "$text" 821 | ], 822 | "qDerivedFields": [ ] 823 | }, 824 | { 825 | "qName": "Opportunity Open_Flag", 826 | "qOriginalFields": [ ], 827 | "qPresent": true, 828 | "qHasDuplicates": true, 829 | "qInformationDensity": 1, 830 | "qnNonNulls": 13229, 831 | "qnRows": 13229, 832 | "qSubsetRatio": 1, 833 | "qnTotalDistinctValues": 2, 834 | "qnPresentDistinctValues": 2, 835 | "qKeyType": "NOT_KEY", 836 | "qTags": [ 837 | "$numeric", 838 | "$integer" 839 | ], 840 | "qDerivedFields": [ ] 841 | }, 842 | { 843 | "qName": "Opportunity Closed_Flag", 844 | "qOriginalFields": [ ], 845 | "qPresent": true, 846 | "qHasDuplicates": true, 847 | "qInformationDensity": 1, 848 | "qnNonNulls": 13229, 849 | "qnRows": 13229, 850 | "qSubsetRatio": 1, 851 | "qnTotalDistinctValues": 2, 852 | "qnPresentDistinctValues": 2, 853 | "qKeyType": "NOT_KEY", 854 | "qTags": [ 855 | "$numeric", 856 | "$integer" 857 | ], 858 | "qDerivedFields": [ ] 859 | }, 860 | { 861 | "qName": "Opportunity Won/Lost", 862 | "qOriginalFields": [ ], 863 | "qPresent": true, 864 | "qHasNull": true, 865 | "qHasDuplicates": true, 866 | "qInformationDensity": 0.8929624310227531, 867 | "qnNonNulls": 11813, 868 | "qnRows": 13229, 869 | "qSubsetRatio": 1, 870 | "qnTotalDistinctValues": 2, 871 | "qnPresentDistinctValues": 2, 872 | "qKeyType": "NOT_KEY", 873 | "qTags": [ 874 | "$ascii", 875 | "$text" 876 | ], 877 | "qDerivedFields": [ ] 878 | }, 879 | { 880 | "qName": "Opportunity Won_Flag", 881 | "qOriginalFields": [ ], 882 | "qPresent": true, 883 | "qHasNull": true, 884 | "qHasDuplicates": true, 885 | "qInformationDensity": 0.8929624310227531, 886 | "qnNonNulls": 11813, 887 | "qnRows": 13229, 888 | "qSubsetRatio": 1, 889 | "qnTotalDistinctValues": 2, 890 | "qnPresentDistinctValues": 2, 891 | "qKeyType": "NOT_KEY", 892 | "qTags": [ 893 | "$numeric", 894 | "$integer" 895 | ], 896 | "qDerivedFields": [ ] 897 | }, 898 | { 899 | "qName": "Opportunity Triphase", 900 | "qOriginalFields": [ ], 901 | "qPresent": true, 902 | "qHasNull": true, 903 | "qHasDuplicates": true, 904 | "qInformationDensity": 0.9953889182855847, 905 | "qnNonNulls": 13168, 906 | "qnRows": 13229, 907 | "qSubsetRatio": 1, 908 | "qnTotalDistinctValues": 3, 909 | "qnPresentDistinctValues": 3, 910 | "qKeyType": "NOT_KEY", 911 | "qTags": [ 912 | "$numeric", 913 | "$integer" 914 | ], 915 | "qDerivedFields": [ ] 916 | }, 917 | { 918 | "qName": "Opportunity Forecast Category", 919 | "qOriginalFields": [ ], 920 | "qPresent": true, 921 | "qHasDuplicates": true, 922 | "qInformationDensity": 1, 923 | "qnNonNulls": 13229, 924 | "qnRows": 13229, 925 | "qSubsetRatio": 1, 926 | "qnTotalDistinctValues": 5, 927 | "qnPresentDistinctValues": 5, 928 | "qKeyType": "NOT_KEY", 929 | "qTags": [ 930 | "$ascii", 931 | "$text" 932 | ], 933 | "qDerivedFields": [ ] 934 | }, 935 | { 936 | "qName": "Opportunity Created Date", 937 | "qOriginalFields": [ ], 938 | "qPresent": true, 939 | "qHasDuplicates": true, 940 | "qInformationDensity": 1, 941 | "qnNonNulls": 13229, 942 | "qnRows": 13229, 943 | "qSubsetRatio": 1, 944 | "qnTotalDistinctValues": 13228, 945 | "qnPresentDistinctValues": 13228, 946 | "qKeyType": "NOT_KEY", 947 | "qTags": [ 948 | "$numeric", 949 | "$timestamp" 950 | ], 951 | "qDerivedFields": [ ] 952 | } 953 | ], 954 | "qPos": { 955 | "qx": 0, 956 | "qy": 0 957 | } 958 | }, 959 | { 960 | "qName": "opportunitylineitem", 961 | "qNoOfRows": 23585, 962 | "qFields": [ 963 | { 964 | "qName": "OpportunityId", 965 | "qOriginalFields": [ ], 966 | "qPresent": true, 967 | "qHasDuplicates": true, 968 | "qInformationDensity": 1, 969 | "qnNonNulls": 23585, 970 | "qnRows": 23585, 971 | "qSubsetRatio": 1, 972 | "qnTotalDistinctValues": 13253, 973 | "qnPresentDistinctValues": 13253, 974 | "qKeyType": "ANY_KEY", 975 | "qTags": [ 976 | "$key", 977 | "$ascii", 978 | "$text" 979 | ], 980 | "qDerivedFields": [ ] 981 | }, 982 | { 983 | "qName": "PricebookEntryId", 984 | "qOriginalFields": [ ], 985 | "qPresent": true, 986 | "qHasDuplicates": true, 987 | "qInformationDensity": 1, 988 | "qnNonNulls": 23585, 989 | "qnRows": 23585, 990 | "qSubsetRatio": 1, 991 | "qnTotalDistinctValues": 183, 992 | "qnPresentDistinctValues": 183, 993 | "qKeyType": "ANY_KEY", 994 | "qTags": [ 995 | "$key", 996 | "$ascii", 997 | "$text" 998 | ], 999 | "qDerivedFields": [ ] 1000 | }, 1001 | { 1002 | "qName": "Opportunity Qty", 1003 | "qOriginalFields": [ ], 1004 | "qPresent": true, 1005 | "qHasDuplicates": true, 1006 | "qInformationDensity": 1, 1007 | "qnNonNulls": 23585, 1008 | "qnRows": 23585, 1009 | "qSubsetRatio": 1, 1010 | "qnTotalDistinctValues": 262, 1011 | "qnPresentDistinctValues": 262, 1012 | "qKeyType": "NOT_KEY", 1013 | "qTags": [ 1014 | "$numeric" 1015 | ], 1016 | "qDerivedFields": [ ] 1017 | } 1018 | ], 1019 | "qPos": { 1020 | "qx": 0, 1021 | "qy": 0 1022 | } 1023 | }, 1024 | { 1025 | "qName": "pricebookentry", 1026 | "qNoOfRows": 183, 1027 | "qFields": [ 1028 | { 1029 | "qName": "PricebookEntryId", 1030 | "qOriginalFields": [ ], 1031 | "qPresent": true, 1032 | "qInformationDensity": 1, 1033 | "qnNonNulls": 183, 1034 | "qnRows": 183, 1035 | "qSubsetRatio": 1, 1036 | "qnTotalDistinctValues": 183, 1037 | "qnPresentDistinctValues": 183, 1038 | "qKeyType": "ANY_KEY", 1039 | "qTags": [ 1040 | "$key", 1041 | "$ascii", 1042 | "$text" 1043 | ], 1044 | "qDerivedFields": [ ] 1045 | }, 1046 | { 1047 | "qName": "Product2Id", 1048 | "qOriginalFields": [ ], 1049 | "qPresent": true, 1050 | "qHasDuplicates": true, 1051 | "qInformationDensity": 1, 1052 | "qnNonNulls": 183, 1053 | "qnRows": 183, 1054 | "qSubsetRatio": 1, 1055 | "qnTotalDistinctValues": 11, 1056 | "qnPresentDistinctValues": 11, 1057 | "qKeyType": "ANY_KEY", 1058 | "qTags": [ 1059 | "$key", 1060 | "$ascii", 1061 | "$text" 1062 | ], 1063 | "qDerivedFields": [ ] 1064 | } 1065 | ], 1066 | "qPos": { 1067 | "qx": 0, 1068 | "qy": 0 1069 | } 1070 | }, 1071 | { 1072 | "qName": "product", 1073 | "qNoOfRows": 11, 1074 | "qFields": [ 1075 | { 1076 | "qName": "Product2Id", 1077 | "qOriginalFields": [ ], 1078 | "qPresent": true, 1079 | "qInformationDensity": 1, 1080 | "qnNonNulls": 11, 1081 | "qnRows": 11, 1082 | "qSubsetRatio": 1, 1083 | "qnTotalDistinctValues": 11, 1084 | "qnPresentDistinctValues": 11, 1085 | "qKeyType": "ANY_KEY", 1086 | "qTags": [ 1087 | "$key", 1088 | "$ascii", 1089 | "$text" 1090 | ], 1091 | "qDerivedFields": [ ] 1092 | }, 1093 | { 1094 | "qName": "Product Name", 1095 | "qOriginalFields": [ ], 1096 | "qPresent": true, 1097 | "qHasDuplicates": true, 1098 | "qInformationDensity": 1, 1099 | "qnNonNulls": 11, 1100 | "qnRows": 11, 1101 | "qSubsetRatio": 1, 1102 | "qnTotalDistinctValues": 10, 1103 | "qnPresentDistinctValues": 10, 1104 | "qKeyType": "NOT_KEY", 1105 | "qTags": [ 1106 | "$ascii", 1107 | "$text" 1108 | ], 1109 | "qDerivedFields": [ ] 1110 | } 1111 | ], 1112 | "qPos": { 1113 | "qx": 0, 1114 | "qy": 0 1115 | } 1116 | }, 1117 | { 1118 | "qName": "user", 1119 | "qNoOfRows": 388, 1120 | "qFields": [ 1121 | { 1122 | "qName": "User Id", 1123 | "qOriginalFields": [ ], 1124 | "qPresent": true, 1125 | "qInformationDensity": 1, 1126 | "qnNonNulls": 388, 1127 | "qnRows": 388, 1128 | "qSubsetRatio": 1, 1129 | "qnTotalDistinctValues": 388, 1130 | "qnPresentDistinctValues": 388, 1131 | "qKeyType": "ANY_KEY", 1132 | "qTags": [ 1133 | "$key", 1134 | "$ascii", 1135 | "$text" 1136 | ], 1137 | "qDerivedFields": [ ] 1138 | }, 1139 | { 1140 | "qName": "User Full Name", 1141 | "qOriginalFields": [ ], 1142 | "qPresent": true, 1143 | "qInformationDensity": 1, 1144 | "qnNonNulls": 388, 1145 | "qnRows": 388, 1146 | "qSubsetRatio": 1, 1147 | "qnTotalDistinctValues": 388, 1148 | "qnPresentDistinctValues": 388, 1149 | "qKeyType": "NOT_KEY", 1150 | "qTags": [ 1151 | "$ascii", 1152 | "$text" 1153 | ], 1154 | "qDerivedFields": [ ] 1155 | }, 1156 | { 1157 | "qName": "Quota", 1158 | "qOriginalFields": [ ], 1159 | "qPresent": true, 1160 | "qHasDuplicates": true, 1161 | "qInformationDensity": 1, 1162 | "qnNonNulls": 388, 1163 | "qnRows": 388, 1164 | "qSubsetRatio": 1, 1165 | "qnTotalDistinctValues": 281, 1166 | "qnPresentDistinctValues": 281, 1167 | "qKeyType": "NOT_KEY", 1168 | "qTags": [ 1169 | "$numeric" 1170 | ], 1171 | "qDerivedFields": [ ] 1172 | } 1173 | ], 1174 | "qPos": { 1175 | "qx": 0, 1176 | "qy": 0 1177 | } 1178 | }, 1179 | { 1180 | "qName": "oppname", 1181 | "qNoOfRows": 16, 1182 | "qFields": [ 1183 | { 1184 | "qName": "CURRENCY", 1185 | "qOriginalFields": [ ], 1186 | "qPresent": true, 1187 | "qInformationDensity": 1, 1188 | "qnNonNulls": 16, 1189 | "qnRows": 16, 1190 | "qSubsetRatio": 1, 1191 | "qnTotalDistinctValues": 16, 1192 | "qnPresentDistinctValues": 16, 1193 | "qKeyType": "NOT_KEY", 1194 | "qTags": [ 1195 | "$ascii", 1196 | "$text" 1197 | ], 1198 | "qDerivedFields": [ ] 1199 | } 1200 | ], 1201 | "qPos": { 1202 | "qx": 0, 1203 | "qy": 0 1204 | } 1205 | } 1206 | ], 1207 | "qk": [ 1208 | { 1209 | "qKeyFields": [ 1210 | "AccountId" 1211 | ], 1212 | "qTables": [ 1213 | "account", 1214 | "opportunity" 1215 | ] 1216 | }, 1217 | { 1218 | "qKeyFields": [ 1219 | "OpportunityId" 1220 | ], 1221 | "qTables": [ 1222 | "opportunity", 1223 | "opportunitylineitem" 1224 | ] 1225 | }, 1226 | { 1227 | "qKeyFields": [ 1228 | "User Id" 1229 | ], 1230 | "qTables": [ 1231 | "opportunity", 1232 | "user" 1233 | ] 1234 | }, 1235 | { 1236 | "qKeyFields": [ 1237 | "PricebookEntryId" 1238 | ], 1239 | "qTables": [ 1240 | "opportunitylineitem", 1241 | "pricebookentry" 1242 | ] 1243 | }, 1244 | { 1245 | "qKeyFields": [ 1246 | "Product2Id" 1247 | ], 1248 | "qTables": [ 1249 | "pricebookentry", 1250 | "product" 1251 | ] 1252 | } 1253 | ] 1254 | } 1255 | -------------------------------------------------------------------------------- /test/integration/00-api-docs.specs.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanwalther/qix-graphql/e262c10d464e0c4622890453ff30f9dbc93a61af/test/integration/00-api-docs.specs.js -------------------------------------------------------------------------------- /test/integration/00-app-server.spec.js: -------------------------------------------------------------------------------- 1 | const superTest = require('supertest'); 2 | const AppServer = require('./../../src/app-server'); 3 | const config = require('./../../src/config/config'); 4 | 5 | describe('INTEGRATION => appServer', () => { 6 | 7 | let server; 8 | let appServer; 9 | 10 | beforeEach(async () => { 11 | appServer = new AppServer(); 12 | await appServer.start(); 13 | server = superTest(appServer.server); 14 | }); 15 | 16 | afterEach(async () => { 17 | await appServer.stop(); 18 | }); 19 | 20 | it('should be instantiated', () => { 21 | expect(appServer.server).to.exist; 22 | }); 23 | 24 | it('should use default config', () => { 25 | expect(appServer.config).to.exist; 26 | expect(appServer.config.HOST).to.equal(config.HOST); 27 | expect(appServer.config.PORT).to.equal(config.PORT); 28 | expect(appServer.config.NODE_ENV).to.equal(config.NODE_ENV); 29 | expect(appServer.config.QIX_HOST).to.equal(config.QIX_HOST); 30 | expect(appServer.config.QIX_PORT).to.equal(config.QIX_PORT); 31 | }); 32 | 33 | }); 34 | 35 | 36 | -------------------------------------------------------------------------------- /test/integration/00b-app-server.config.spec.js: -------------------------------------------------------------------------------- 1 | const AppServer = require('./../../src/app-server'); 2 | const config = require('./../../src/config/config'); 3 | 4 | describe('INTEGRATION => appServer', () => { 5 | 6 | let appServer; 7 | const customConfig = { 8 | QIX_HOST: 'qix-local', 9 | QIX_PORT: 5555 10 | }; 11 | 12 | beforeEach(async () => { 13 | appServer = new AppServer(customConfig); 14 | }); 15 | 16 | afterEach(async () => { 17 | await appServer.stop(); 18 | }); 19 | 20 | it('allows to pass custom config to the constructor', async () => { 21 | 22 | await appServer.start(); 23 | 24 | // custom configs 25 | expect(appServer.config.QIX_HOST).to.equal(customConfig.QIX_HOST); 26 | expect(appServer.config.QIX_PORT).to.equal(customConfig.QIX_PORT); 27 | 28 | // default configs 29 | expect(appServer.config.PORT).to.equal(config.PORT); 30 | expect(appServer.config.NODE_ENV).to.equal(config.NODE_ENV); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/integration/00c-app-server.routes.spec.js: -------------------------------------------------------------------------------- 1 | const superTest = require('supertest'); 2 | const AppServer = require('./../../src/app-server'); 3 | const HttpStatus = require('http-status-codes'); 4 | const config = require('./../../src/config/config'); 5 | 6 | describe('INTEGRATION => appServer', () => { 7 | 8 | let server; 9 | let appServer; 10 | 11 | beforeEach(async () => { 12 | appServer = new AppServer(); 13 | await appServer.start(); 14 | server = superTest(appServer.server); 15 | }); 16 | 17 | afterEach(async () => { 18 | await appServer.stop(); 19 | }); 20 | 21 | it('should have a nice fallback route', async () => { 22 | 23 | await server 24 | .get('/foo') 25 | .expect(HttpStatus.OK) 26 | .then(result => { 27 | expect(result.body).to.have.property('_links'); 28 | expect(result.body._links).to.have.property('_self').to.be.equal(`http://${config.HOST}:${config.PORT}`) 29 | }) 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/integration/01-health-check.spec.js: -------------------------------------------------------------------------------- 1 | const superTest = require('supertest'); 2 | const HttpStatus = require('http-status-codes'); 3 | const AppServer = require('./../../src/app-server'); 4 | 5 | const pkg = require('./../../package.json'); 6 | 7 | describe('INTEGRATION => health-check', () => { 8 | 9 | let server; 10 | let appServer; 11 | 12 | beforeEach(async () => { 13 | appServer = new AppServer(); 14 | await appServer.start(); 15 | server = superTest(appServer.server); 16 | }); 17 | 18 | afterEach(async () => { 19 | await appServer.stop(); 20 | }); 21 | 22 | it('returns OK and a timestamp', async () => { 23 | await server 24 | .get('/health-check') 25 | .expect(HttpStatus.OK) 26 | .then(result => { 27 | expect(result).to.exist; 28 | expect(result).to.have.property('body'); 29 | expect(result.body).to.have.property('ts').to.exist; 30 | expect(result.body).to.have.property('version').to.be.equal(pkg.version); 31 | expect(result.body).to.have.property('name').to.be.equal(pkg.name); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/integration/02-global-scope.spec.js: -------------------------------------------------------------------------------- 1 | const superTest = require('supertest'); 2 | const AppServer = require('./../../src/app-server'); 3 | const HttpStatusCodes = require('http-status-codes'); 4 | const ql = require('superagent-graphql'); 5 | const config = require('./../../src/config/config'); 6 | const _ = require('lodash'); 7 | 8 | describe('INTEGRATION => global scope ', () => { 9 | 10 | let server; 11 | let appServer; 12 | 13 | beforeEach(async () => { 14 | appServer = new AppServer(); 15 | await appServer.start(); 16 | server = superTest(appServer.server); 17 | }); 18 | 19 | afterEach(async () => { 20 | await appServer.stop(); 21 | appServer = null; 22 | }); 23 | 24 | it('allows to query docs', async () => { 25 | 26 | const query = `{ 27 | docs { 28 | qDocName 29 | qDocId 30 | } 31 | }`; 32 | const vars = {}; 33 | 34 | await server 35 | .post('/global/graphql') 36 | .use(ql(query, vars)) 37 | .expect(HttpStatusCodes.OK) 38 | .then(result => { 39 | expect(result).to.exist; 40 | expect(result.body.errors).to.not.exist; 41 | expect(result).to.have.a.property('body').to.have.a.property('data'); 42 | expect(result.body.data).to.have.a.property('docs').to.be.an('array'); 43 | }) 44 | }); 45 | 46 | it('allows to query a single doc', async () => { 47 | 48 | const query = `{ 49 | doc(qDocId: "/docs/CRM.qvf") { 50 | qDocName 51 | } 52 | }`; 53 | const vars = {}; 54 | 55 | await server 56 | .post('/global/graphql') 57 | .use(ql(query, vars)) 58 | .expect(HttpStatusCodes.OK) 59 | .then(result => { 60 | expect(result).to.exist; 61 | expect(result.body.errors).to.not.exist; 62 | expect(result).to.have.a.property('body').to.have.a.property('data'); 63 | expect(result.body.data).to.have.a.property('doc').to.have.a.property('qDocName').to.equal('CRM.qvf') 64 | }) 65 | }); 66 | 67 | it('provides to query the env', async () => { 68 | const query = `{ 69 | env { 70 | HOST 71 | PORT 72 | QIX_HOST 73 | QIX_PORT 74 | } 75 | }`; 76 | const vars = {}; 77 | 78 | await server 79 | .post('/global/graphql') 80 | .use(ql(query, vars)) 81 | .expect(HttpStatusCodes.OK) 82 | .then(result => { 83 | expect(result).to.exist; 84 | expect(result.body.errors).to.not.exist; 85 | expect(result).to.have.a.property('body').to.have.a.property('data').to.have.a.property('env'); 86 | expect(result.body.data.env).to.have.a.property('HOST').to.equal(config.HOST); 87 | expect(result.body.data.env).to.have.a.property('PORT').to.equal(config.PORT); 88 | expect(result.body.data.env).to.have.a.property('QIX_HOST').to.equal(config.QIX_HOST); 89 | expect(result.body.data.env).to.have.a.property('QIX_PORT').to.equal(_.toNumber(config.QIX_PORT)); 90 | }) 91 | }) 92 | 93 | }); 94 | -------------------------------------------------------------------------------- /test/integration/03-qix-lib.spec.js: -------------------------------------------------------------------------------- 1 | const superTest = require('supertest'); 2 | const AppServer = require('./../../src/app-server'); 3 | const QixLib = require('./../../src/lib/qix-lib'); 4 | 5 | describe('Schema generator ', () => { 6 | 7 | let server; 8 | let appServer; 9 | 10 | beforeEach(async () => { 11 | appServer = new AppServer(); 12 | await appServer.start(); 13 | server = superTest(appServer.server); 14 | }); 15 | 16 | afterEach(async () => { 17 | await appServer.stop(); 18 | }); 19 | 20 | it('fetches tables_and_keys (for /docs/CRM.qvf)', async() => { 21 | const options = { 22 | "qDocName": "/docs/CRM.qvf" 23 | }; 24 | let table_and_keys = await QixLib.getTablesAndKeys(options); 25 | expect(table_and_keys).to.exist; 26 | }); 27 | 28 | }); 29 | -------------------------------------------------------------------------------- /test/integration/04-doc-scope.spec.js: -------------------------------------------------------------------------------- 1 | const superTest = require('supertest'); 2 | const AppServer = require('./../../src/app-server'); 3 | const HttpStatusCodes = require('http-status-codes'); 4 | const ql = require('superagent-graphql'); 5 | 6 | describe('Integration tests: In APP mode', () => { 7 | 8 | let server; 9 | let appServer; 10 | 11 | beforeEach(async () => { 12 | appServer = new AppServer(); 13 | await appServer.start(); 14 | server = superTest(appServer.server); 15 | }); 16 | 17 | afterEach(async () => { 18 | await appServer.stop(); 19 | }); 20 | 21 | it('allows to fetch data from a doc (CRM.qvf)', async() => { 22 | 23 | const query = `{ 24 | account { 25 | AccountId 26 | } 27 | }`; 28 | const vars = {}; 29 | 30 | await server 31 | .post('/doc/%2Fdocs%2FCRM.qvf/graphql') 32 | .use(ql(query, vars)) 33 | .expect(HttpStatusCodes.OK) 34 | .then(result => { 35 | expect(result).to.exist; 36 | expect(result.body.errors).to.not.exist; 37 | expect(result).to.have.a.property('body').to.have.a.property('data'); 38 | expect(result.body.data).to.have.a.property('account').to.be.an('array').of.length.greaterThan(9); 39 | expect(result.body.data.account[0]).to.have.a.property('AccountId'); 40 | }) 41 | 42 | }); 43 | 44 | it('returns an error if the app-schema cannot be created', async () => { 45 | 46 | const query = `{ 47 | table_does_not_exist { 48 | field_does_not_exist 49 | } 50 | }`; 51 | const vars = {}; 52 | 53 | await server 54 | .post('/doc/%2Fdocs%2FCRM.qvf/graphql') 55 | .use(ql(query, vars)) 56 | .expect(HttpStatusCodes.BAD_REQUEST) 57 | .then(result => { 58 | expect(result).to.exist; 59 | expect(result.body.errors[0]).to.deep.contain({'message': 'Cannot query field "table_does_not_exist" on type "Tables".'}); 60 | }); 61 | }); 62 | 63 | }); 64 | -------------------------------------------------------------------------------- /test/integration/05-qix-resolver.spec.js: -------------------------------------------------------------------------------- 1 | const docResolver = require('../../src/modules/doc/doc.resolvers'); 2 | const mockTablesAndKeys = require('./../fixtures/TablesAndKeys-CRM.json'); 3 | 4 | describe('qixResolver', () => { 5 | xdescribe('resolveTable', () => { 6 | 7 | it('throws an error without context', () => { 8 | let fn = () => { 9 | docResolver.resolveTable(); 10 | }; 11 | expect(fn).to.throw(Error, "Cannot read property 'config' of undefined"); 12 | }); 13 | 14 | // Todo: we have new params there, so this all needs to be fixed 15 | // when working on this functionality 16 | xit('resolves the table', async () => { 17 | let ctx = { 18 | config: { 19 | QIX_HOST: 'localhost' 20 | }, 21 | tables_and_keys: mockTablesAndKeys 22 | }; 23 | let result = await docResolver.resolveTable(null, null, ctx); // Todo: pass variables in here 24 | expect(result).to.exist; 25 | expect(result).to.be.an('array'); 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/integration/enigma.spec.js: -------------------------------------------------------------------------------- 1 | const enigma = require('enigma.js'); 2 | const WebSocket = require('ws'); 3 | // Todo: verify the schema, or probably even better: make this a global config 4 | const qixSchema = require('enigma.js/schemas/12.20.0.json'); 5 | 6 | const config = require('./../../src/config/config'); 7 | 8 | xdescribe('INTEGRATION => enigma.js tests ==>', () => { 9 | 10 | describe('SESSION', () => { 11 | 12 | let session = null; 13 | 14 | beforeEach(() => { 15 | session = enigma.create({ 16 | schema: qixSchema, 17 | url: `ws://${config.QIX_HOST}:${config.QIX_PORT}/app/engineData`, 18 | createSocket: url => new WebSocket(url) 19 | }); 20 | }); 21 | 22 | afterEach(async () => { 23 | if (session) { 24 | await session.close(); 25 | } 26 | }); 27 | 28 | it('open a session', () => { 29 | //session.on('opened', data => console.log('session opened')); 30 | //session.on('closed', data => console.log('session closed')); 31 | //session.on('traffic:sent', data => console.log('sent:', data)); 32 | //session.on('traffic:received', data => console.log('received:', data)); 33 | 34 | return session.open() 35 | .then(global => { 36 | expect(global).to.exist; 37 | }); 38 | }); 39 | }); 40 | 41 | describe('DOC', () => { 42 | let session = null; 43 | 44 | beforeEach(() => { 45 | session = enigma.create({ 46 | schema: qixSchema, 47 | url: `ws://${config.QIX_HOST}:${config.QIX_PORT}/app/engineData`, 48 | createSocket: url => new WebSocket(url) 49 | }); 50 | }); 51 | 52 | afterEach(async () => { 53 | if (session) { 54 | await session.close(); 55 | } 56 | }); 57 | 58 | it('open a doc', () => { 59 | 60 | return session.open() 61 | // Todo (AAA): Replace hardcoded value and make this dynamic 62 | .then(global => global.openDoc({qDocName: '/docs/CRM.qvf', qNoData: false})) 63 | .then(doc => doc.getTablesAndKeys({qIncludeSysVars: true})) 64 | .catch(err => { 65 | console.error('Err in getTablesAndKeys', err); 66 | throw err; 67 | }, err => { 68 | console.log('There is another error here', err); 69 | throw err; 70 | }); 71 | 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /test/mocha.conf.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'test'; 2 | if (process.env.CIRCLECI !== 'true') { 3 | // Whatever we need here 4 | } 5 | if (process.env.NODE_ENV === 'test') { 6 | process.env.WINSTER_SUPRESS_LOGGING = 'true'; 7 | } 8 | 9 | const chai = require('chai'); 10 | const chaiSubset = require('chai-subset'); 11 | chai.use(chaiSubset); 12 | global.expect = chai.expect; 13 | 14 | -------------------------------------------------------------------------------- /test/unit/graphql-generator.spec.js: -------------------------------------------------------------------------------- 1 | const DocSchemaGenerator = require('./../../src/modules/doc/doc-schema-generator'); 2 | const mockTablesAndKeys = require('./../fixtures/TablesAndKeys-CRM.json'); 3 | 4 | describe('UNIT => qix-graphql-generator', () => { 5 | 6 | describe('=> ctor', () => { 7 | 8 | it('should throw an error if options.qDocId is not provided', () => { 9 | let fn = () => { 10 | new DocSchemaGenerator({}); 11 | }; 12 | expect(fn).to.throw(Error, 'qDocId is missing'); 13 | }); 14 | 15 | it('should throw an error if options.qDocId is not provided', () => { 16 | let fn = () => { 17 | new DocSchemaGenerator({ 18 | qDocId: 'foo' 19 | }); 20 | }; 21 | expect(fn).to.throw(Error, 'tables_and_keys is missing'); 22 | }); 23 | 24 | it('should throw an error if options.qDocId is not provided', () => { 25 | let fn = () => { 26 | new DocSchemaGenerator({ 27 | qDocId: 'foo', 28 | tables_and_keys: {} 29 | }); 30 | }; 31 | expect(fn).to.throw(Error, 'tables_and_keys.qtr is missing'); 32 | }); 33 | 34 | it('should NOT throw an error if everything is provided as expected', () => { 35 | let fn = () => { 36 | new DocSchemaGenerator({ 37 | qDocId: 'foo', 38 | tables_and_keys: { 39 | qtr: [] 40 | } 41 | }); 42 | }; 43 | expect(fn).to.not.throw(); 44 | }); 45 | }); 46 | 47 | describe('=> _generateTypes', () => { 48 | it('should succeed', () => { 49 | try { 50 | let g = new DocSchemaGenerator({ 51 | qDocId: 'foo', 52 | tables_and_keys: mockTablesAndKeys 53 | }); 54 | g._initTypes(); 55 | expect(g._types).to.exist; 56 | expect(g._types).to.contain.property('account'); 57 | } catch (e) { 58 | expect(e).to.not.exist; 59 | } 60 | }); 61 | }); 62 | 63 | }); 64 | 65 | -------------------------------------------------------------------------------- /test/unit/lib.spec.js: -------------------------------------------------------------------------------- 1 | const lib = require('./../../src/lib/lib'); 2 | 3 | describe('lib', () => { 4 | 5 | it('exposes a method sanitize', () => { 6 | expect(lib).to.have.a.property('sanitize'); 7 | }); 8 | 9 | describe('sanitize', () => { 10 | it('removes whitespaces', () => { 11 | expect(lib.sanitize('foo bar')).to.be.equal('foo_bar'); 12 | }); 13 | it('removes special characters', () => { 14 | const s = '"$%&/()?'; 15 | const s_sanitized = '________'; 16 | expect(lib.sanitize(s)).to.be.equal(s_sanitized); 17 | }) 18 | }); 19 | 20 | }); 21 | -------------------------------------------------------------------------------- /test/unit/schema-cache.spec.js: -------------------------------------------------------------------------------- 1 | const schemaCache = require('../../src/lib/schema-cache'); 2 | 3 | describe('UNIT => schema-cache', () => { 4 | 5 | beforeEach(() => { 6 | schemaCache.reset(); 7 | }); 8 | 9 | it('exists', () => { 10 | expect(schemaCache).to.exist; 11 | }); 12 | 13 | it('has a property cache', () => { 14 | expect(schemaCache).to.have.property('cache').to.exist; 15 | }); 16 | 17 | it('has zero cache objects by default', () => { 18 | expect(schemaCache.count()).to.equal(0); 19 | }); 20 | 21 | it('allows to add an object to the cache', () => { 22 | const qDocId = 'foo'; 23 | const obj = { 24 | foo: 'bar' 25 | }; 26 | expect(schemaCache.count()).to.be.equal(0); 27 | schemaCache.add(qDocId, obj); 28 | expect(schemaCache.count()).to.be.equal(1); 29 | expect(schemaCache.find(qDocId)).to.be.equal(obj); 30 | }); 31 | 32 | it('allows to check if an item exists', () => { 33 | const qDocId = 'foo'; 34 | const obj = { 35 | foo: 'bar' 36 | }; 37 | expect(schemaCache.count()).to.be.equal(0); 38 | schemaCache.add(qDocId, obj); 39 | expect(schemaCache.count()).to.be.equal(1); 40 | expect(schemaCache.find(qDocId)).to.be.equal(obj); 41 | expect(schemaCache.exists(qDocId)).to.be.true; 42 | }); 43 | 44 | }); 45 | --------------------------------------------------------------------------------