├── .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 | [](https://github.com/stefanwalther/{%=name%})
7 | [](https://codecov.io/gh/stefanwalther/{%=name%})
8 | [](https://circleci.com/gh/stefanwalther/{%=name%}/tree/master)
9 |
10 | 
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 | [](https://www.npmjs.com/package/qix-graphql)
6 | [](https://github.com/stefanwalther/qix-graphql)
7 | [](https://codecov.io/gh/stefanwalther/qix-graphql)
8 | [](https://circleci.com/gh/stefanwalther/qix-graphql/tree/master)
9 |
10 | 
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 | 
78 |
79 | We can also easily get a list of all documents in the IDE:
80 |
81 | 
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 | 
99 |
100 | So let's query one of those tables (in this example the table `account` on the doc `CRM.qvf`:
101 |
102 | 
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 | 
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 | 
14 |
15 | ## Built-In Documentation
16 |
17 | 
18 |
19 | ## Intellisense
20 |
21 | Press `Alt` + `Space` and you'll get the intellisense experience:
22 |
23 | 
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 | 
38 |
39 | We can also easily get a list of all documents in the IDE:
40 |
41 | 
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 | 
59 |
60 | So let's query one of those tables (in this example the table `account` on the doc `CRM.qvf`:
61 |
62 | 
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 | 
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 |
--------------------------------------------------------------------------------