├── book.json
├── .npmignore
├── test
├── e2e
│ ├── config.json
│ ├── test-model
│ │ └── iris.qvf
│ ├── e2e.spec.js
│ ├── field.test.js
│ ├── global.test.js
│ ├── connectSession.test.js
│ ├── genericBookmark.test.js
│ ├── suspend.test.js
│ ├── closeSession.test.js
│ ├── utilityOperators.test.js
│ ├── genericObject.test.js
│ ├── doc.test.js
│ ├── notification.test.js
│ └── delta.test.js
├── util
│ ├── isObservable.js
│ ├── mock-qix-engine.js
│ └── create-container.js
└── unit
│ ├── $fromQix.spec.js
│ ├── connectSession.spec.js
│ ├── Session.spec.js
│ └── Handle.spec.js
├── docs
├── basics
│ ├── closing-a-session.md
│ ├── notifications.md
│ ├── suspending-invalidations.md
│ ├── connecting.md
│ ├── delta.md
│ ├── handle-invalidations.md
│ ├── reusing-handles.md
│ ├── making-api-calls.md
│ └── shortcut-operators.md
├── recipes
│ ├── connect.md
│ ├── open-an-app.md
│ ├── engine-version.md
│ ├── shortcut-operators.md
│ ├── calc-time.md
│ ├── read-app-props.md
│ ├── multiple-global-calls.md
│ ├── make-selections-on-app-open.md
│ ├── gen-obj-value.md
│ ├── current-selections.md
│ ├── set-app-props.md
│ ├── get-data-page-on-invalid.md
│ ├── batch-invalidations.md
│ ├── make-a-selection.md
│ ├── toggle-sessions.md
│ ├── loop-selections.md
│ ├── get-pages-in-sequence.md
│ ├── get-pages-in-parallel.md
│ ├── make-lb-selections.md
│ ├── search-lb.md
│ ├── offline.md
│ └── apply-patches.md
├── README.md
├── introduction
│ ├── rxq-vs-enigma.md
│ ├── why-rx.md
│ └── core-concepts.md
└── SUMMARY.md
├── src
├── operators
│ ├── qAsk.js
│ ├── qAskReplay.js
│ ├── invalidations.js
│ └── suspendUntilCompleted.js
├── util
│ ├── index.js
│ ├── map-qix-return.js
│ ├── connectWS.js
│ ├── qix-connect-options.js
│ └── qix-connect-string.js
├── connect
│ └── connectSession.js
├── handle.js
├── index.js
├── session.js
└── schema
│ └── schema.js
├── .gitignore
├── index.js
├── CHANGELOG.md
├── scripts
├── compile_esm2015.js
├── compile_esm5.js
├── compile_cjs.js
├── make-packages.js
├── build-qix-methods.js
└── build-browser.js
├── package.json
├── README.md
└── LICENSE
/book.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": "./docs"
3 | }
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | examples
2 | rxqap-build.zip
3 | sandbox
4 | .tmp
--------------------------------------------------------------------------------
/test/e2e/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "port": "9079",
3 | "image": "qlikcore/engine:12.329.0"
4 | }
5 |
--------------------------------------------------------------------------------
/test/e2e/test-model/iris.qvf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skokenes/RxQ/HEAD/test/e2e/test-model/iris.qvf
--------------------------------------------------------------------------------
/test/util/isObservable.js:
--------------------------------------------------------------------------------
1 | module.exports = toCheck => typeof toCheck["@@observable"] === "function";
2 |
--------------------------------------------------------------------------------
/docs/basics/closing-a-session.md:
--------------------------------------------------------------------------------
1 | # Closing a Session
2 | Sessions can be closed using the `session.close()` function. This function will close the WebSocket and complete all Observables within RxQ.
--------------------------------------------------------------------------------
/src/operators/qAsk.js:
--------------------------------------------------------------------------------
1 | import { switchMap } from "rxjs/operators";
2 |
3 | const qAsk = (methodname, ...params) => handle$ =>
4 | handle$.pipe(switchMap(handle => handle.ask(methodname, ...params)));
5 |
6 | export default qAsk;
7 |
--------------------------------------------------------------------------------
/src/util/index.js:
--------------------------------------------------------------------------------
1 | import connectWS from "./connectWS.js";
2 | import qixConnectOptions from "./qix-connect-options.js";
3 | import qixConnectString from "./qix-connect-string.js";
4 |
5 | export {
6 | connectWS,
7 | qixConnectOptions,
8 | qixConnectString
9 | };
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist
3 | src/global
4 | src/doc
5 | src/genericbookmark
6 | src/field
7 | src/genericderivedfields
8 | src/genericdimension
9 | src/genericmeasure
10 | src/genericobject
11 | src/genericvariable
12 | src/variable
13 | _book
14 | .tmp
15 | .DS_Store
--------------------------------------------------------------------------------
/src/operators/qAskReplay.js:
--------------------------------------------------------------------------------
1 | import qAsk from "./qAsk";
2 | import { publishReplay, refCount } from "rxjs/operators";
3 |
4 | const qAskReplay = (methodname, ...params) => handle$ =>
5 | handle$.pipe(
6 | qAsk(methodname, ...params),
7 | publishReplay(1),
8 | refCount()
9 | );
10 |
11 | export default qAskReplay;
12 |
--------------------------------------------------------------------------------
/src/operators/invalidations.js:
--------------------------------------------------------------------------------
1 | import { switchMap, startWith } from "rxjs/operators";
2 |
3 | const invalidations = (startWithInvalidation = false) => handle$ =>
4 | handle$.pipe(
5 | switchMap(
6 | handle =>
7 | startWithInvalidation
8 | ? startWith(handle)(handle.invalidated$)
9 | : handle.invalidated$
10 | )
11 | );
12 |
13 | export default invalidations;
14 |
--------------------------------------------------------------------------------
/src/operators/suspendUntilCompleted.js:
--------------------------------------------------------------------------------
1 | import { Observable } from "rxjs";
2 |
3 | export default session => src$ => {
4 | return Observable.create(observer => {
5 | session.suspend();
6 | src$.subscribe({
7 | next: n => observer.next(n),
8 | error: err => observer.error(err),
9 | complete: () => {
10 | session.unsuspend();
11 | observer.complete();
12 | }
13 | });
14 | });
15 | };
16 |
--------------------------------------------------------------------------------
/src/connect/connectSession.js:
--------------------------------------------------------------------------------
1 | import Session from "../session";
2 | import { defer } from "rxjs";
3 |
4 | export default function connectSession(config) {
5 | const session = new Session(config);
6 |
7 | return {
8 | global$: session.global(),
9 | notifications$: session.notifications$,
10 | close: () => session.close(),
11 | suspend: () => session.suspended$.next(true),
12 | unsuspend: () => session.suspended$.next(false)
13 | };
14 | }
15 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | // Polyfills
2 | import "core-js/es6/symbol"; // polyfill symbol to enable iterators in IE
3 | // import "core-js/fn/function/name"; // may be able to remove this with refactor
4 | import "regenerator-runtime/runtime";
5 |
6 | // Observable Factories
7 | import * as src from "./src/index";
8 |
9 | import pack from "./package.json";
10 |
11 | const baseObj = {
12 | version: pack.version,
13 | qixVersion: pack["qix-version"]
14 | };
15 |
16 | const RxQ = Object.assign(baseObj, src);
17 |
18 | export default RxQ;
19 |
--------------------------------------------------------------------------------
/docs/recipes/connect.md:
--------------------------------------------------------------------------------
1 | # Connect to an Engine
2 | [Code Sandbox](https://codesandbox.io/embed/k2wz9px11v)
3 |
4 | ```javascript
5 | // Import the connectSession function
6 | import { connectSession } from "rxq";
7 |
8 | // Define the configuration for your session
9 | const config = {
10 | host: "sense.axisgroup.com",
11 | isSecure: true
12 | };
13 |
14 | // Call connectSession with the config to produce a Session object
15 | const session = connectSession(config);
16 |
17 | // Subscribe to the Global Handle Observable of the Session
18 | session.global$.subscribe(console.log);
19 | ```
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # RxQ
2 | **RxQ** is a JavaScript library that provides utilities to link [**RxJS**](https://github.com/ReactiveX/rxjs), a JavaScript implementation of Rx, with the Qlik Associative Engine (QAE). RxQ can be used to build complex, interactive solutions on top of QAE using the reactive programming paradigm. The code repository can be found at [https://github.com/axisgroup/rxq](https://github.com/axisgroup/rxq). RxQ can be used in projects via npm like so:
3 | ```
4 | $ npm install rxq
5 | ```
6 |
7 | These docs are for RxQ v2.0.1. For v1 docs, [go here.](https://opensrc.axisgroup.com/rxq/docs-v1)
--------------------------------------------------------------------------------
/src/util/map-qix-return.js:
--------------------------------------------------------------------------------
1 | import Handle from "../handle.js";
2 | import { map } from "rxjs/operators";
3 |
4 | export default (handle, returnParam) => src$ =>
5 | src$.pipe(
6 | map(r => {
7 | var hasQType =
8 | r.hasOwnProperty("qReturn") && r.qReturn.hasOwnProperty("qType");
9 |
10 | if (hasQType) {
11 | var qClass = r.qReturn.qType;
12 | return new Handle(handle.session, r.qReturn.qHandle, qClass);
13 | } else if (returnParam) {
14 | return r[returnParam];
15 | } else {
16 | return r;
17 | }
18 | })
19 | );
20 |
--------------------------------------------------------------------------------
/test/util/mock-qix-engine.js:
--------------------------------------------------------------------------------
1 | var { WebSocket, Server } = require("mock-socket");
2 |
3 | function mockEngine() {
4 | var stamp = Date.now();
5 | var url = `ws://127.0.0.1:4848/${stamp}`;
6 | const server = new Server(url);
7 | const ws = new WebSocket(url);
8 |
9 | server.on("message", msg => {
10 | const { id, method } = JSON.parse(msg);
11 |
12 | server.send(
13 | JSON.stringify({
14 | jsonrpc: "2.0",
15 | id: id,
16 | result: {
17 | foo: "bar",
18 | method
19 | }
20 | })
21 | );
22 | });
23 |
24 | return { server, ws };
25 | }
26 |
27 | module.exports = mockEngine;
28 |
--------------------------------------------------------------------------------
/src/handle.js:
--------------------------------------------------------------------------------
1 | import { defer } from "rxjs";
2 | import { filter, mapTo, publish, refCount } from "rxjs/operators";
3 |
4 | export default class Handle {
5 | constructor(session, handle, qClass) {
6 | this.session = session;
7 | this.handle = handle;
8 | this.qClass = qClass;
9 |
10 | this.invalidated$ = session.changes$.pipe(
11 | filter(f => f.indexOf(handle) > -1),
12 | mapTo(this),
13 | publish(),
14 | refCount()
15 | );
16 | }
17 |
18 | ask(method, ...args) {
19 | return defer(() =>
20 | this.session.ask({
21 | method: method,
22 | handle: this.handle,
23 | params: args.filter(arg => typeof arg !== "undefined"),
24 | qClass: this.qClass
25 | })
26 | );
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/docs/basics/notifications.md:
--------------------------------------------------------------------------------
1 | # Notifications
2 | RxQ provides an Observable `session.notifications$`. This Observable provides messages related to the inner workings of RxQ and the Engine session. It is especially useful for debugging purposes. It sends messages in the format:
3 | ```
4 | {
5 | type: a string indicating the type of notification
6 | data: any data associated with the notification
7 | }
8 | ```
9 |
10 | The following type of notifications are emitted:
11 | * `"traffic:sent"` – messages received in the WebSocket
12 | * `"traffic:received"` – messages sent through the WebSocket
13 | * `"traffic:change"` – lists of change handles sent by the Engine
14 | * `"traffic:suspend-status"` – the suspense state of the session
15 | * `"socket:close"` – the WebSocket event when the socket is closed
--------------------------------------------------------------------------------
/docs/recipes/open-an-app.md:
--------------------------------------------------------------------------------
1 | # Open an App
2 | [Code Sandbox](https://codesandbox.io/embed/54mj0myvlx)
3 | ```javascript
4 | import { connectSession } from "rxq";
5 | import { OpenDoc } from "rxq/Global";
6 | import { shareReplay, switchMap } from "rxjs/operators";
7 |
8 | // Define the configuration for your session
9 | const config = {
10 | host: "sense.axisgroup.com",
11 | isSecure: true
12 | };
13 |
14 | // Connect the session and share the Global handle
15 | const session = connectSession(config);
16 | const global$ = session.global$;
17 |
18 | // Open an app and share the app handle
19 | const app$ = global$.pipe(
20 | switchMap(h => h.ask(OpenDoc, "aae16724-dfd9-478b-b401-0d8038793adf")),
21 | shareReplay(1)
22 | );
23 |
24 | // Log the app handle to the console
25 | app$.subscribe(console.log);
26 | ```
--------------------------------------------------------------------------------
/docs/recipes/engine-version.md:
--------------------------------------------------------------------------------
1 | # Get the Engine Version
2 | [Code Sandbox](https://codesandbox.io/embed/6j47717pz3)
3 | ```javascript
4 | // Import the connectSession function, engineVersion function, and switchMap operator
5 | import { connectSession } from "rxq";
6 | import { EngineVersion } from "rxq/Global";
7 | import { switchMap } from "rxjs/operators";
8 |
9 | // Define the configuration for your session
10 | const config = {
11 | host: "sense.axisgroup.com",
12 | isSecure: true
13 | };
14 |
15 | // Connect the session
16 | const session = connectSession(config);
17 | const global$ = session.global$;
18 |
19 | // Get the engineVersion
20 | const engVer$ = global$.pipe(
21 | switchMap(h => h.ask(EngineVersion))
22 | );
23 |
24 | // Write the engine version to the DOM
25 | engVer$.subscribe(response => {
26 | document.querySelector("#ver").innerHTML = response.qComponentVersion;
27 | });
28 | ```
--------------------------------------------------------------------------------
/src/util/connectWS.js:
--------------------------------------------------------------------------------
1 | import connectUrl from "./qix-connect-string";
2 | import connectOptions from "./qix-connect-options";
3 |
4 | export default function connectWS(config) {
5 | const IS_NODE =
6 | typeof process !== "undefined" &&
7 | Object.prototype.toString.call(global.process) === "[object process]";
8 |
9 | let _WebSocket;
10 |
11 | if (IS_NODE) {
12 | try {
13 | _WebSocket = require("ws");
14 | } catch (e) {}
15 | } else {
16 | try {
17 | _WebSocket = WebSocket;
18 | } catch (e) {}
19 | }
20 |
21 | const url = typeof config.url === "string" ? config.url : connectUrl(config);
22 | const options = connectOptions(config);
23 |
24 | if (typeof _WebSocket === "function") {
25 | const ws = IS_NODE
26 | ? new _WebSocket(url, null, options)
27 | : new _WebSocket(url);
28 | return ws;
29 | } else {
30 | throw new Error("WebSocket is not defined");
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/docs/recipes/shortcut-operators.md:
--------------------------------------------------------------------------------
1 | # Using the Shortcut Operators for Layout Streams
2 | [Code Sandbox](https://codesandbox.io/embed/71oj6rj10x)
3 | ```javascript
4 | import { connectSession, qAsk, qAskReplay, invalidations } from "rxq";
5 | import { OpenDoc } from "rxq/Global";
6 | import { CreateSessionObject } from "rxq/Doc";
7 | import { GetLayout } from "rxq/GenericObject";
8 |
9 |
10 | const config = {
11 | host: "sense.axisgroup.com",
12 | isSecure: true,
13 | appname: "aae16724-dfd9-478b-b401-0d8038793adf"
14 | };
15 |
16 | const session = connectSession(config);
17 | const global$ = session.global$;
18 |
19 | const app$ = global$.pipe(qAskReplay(OpenDoc, config.appname));
20 |
21 | const object$ = app$.pipe(
22 | qAskReplay(CreateSessionObject, {
23 | qInfo: {
24 | qType: "object"
25 | },
26 | foo: "bar"
27 | })
28 | );
29 |
30 | const layouts$ = object$.pipe(invalidations(true), qAsk(GetLayout));
31 |
32 | layouts$.subscribe(console.log);
33 |
34 | ```
--------------------------------------------------------------------------------
/docs/recipes/calc-time.md:
--------------------------------------------------------------------------------
1 | # Calculate the response time of the API
2 | [Code Sandbox](https://codesandbox.io/embed/3kvj773np5)
3 | ```javascript
4 | import { connectSession } from "rxq";
5 | import { OpenDoc } from "rxq/Global";
6 | import { map, switchMap } from "rxjs/operators";
7 |
8 | const appname = "aae16724-dfd9-478b-b401-0d8038793adf"
9 |
10 | // Define the configuration for your session
11 | const config = {
12 | host: "sense.axisgroup.com",
13 | isSecure: true,
14 | appname
15 | };
16 |
17 | // Connect the session and share the Global handle
18 | const session = connectSession(config);
19 | const global$ = session.global$;
20 |
21 | // Calculate the time it takes to open an app
22 | const appOpenTime$ = global$.pipe(
23 | switchMap(h => {
24 | const start = Date.now();
25 | return h.ask(OpenDoc, appname).pipe(
26 | map(() => Date.now() - start)
27 | );
28 | })
29 | );
30 |
31 | appOpenTime$.subscribe(time => {
32 | document.querySelector("#time").innerHTML = time;
33 | });
34 | ```
--------------------------------------------------------------------------------
/test/e2e/e2e.spec.js:
--------------------------------------------------------------------------------
1 | const chai = require("chai");
2 | chai.use(require("chai-generator"));
3 | const expect = chai.expect;
4 |
5 | var testConnect = require("./connectSession.test.js");
6 | var testGlobal = require("./global.test.js");
7 | const testDoc = require("./doc.test.js");
8 | const testGenericObject = require("./genericObject.test.js");
9 | const testField = require("./field.test.js");
10 | const testGenericBookmark = require("./genericBookmark.test.js");
11 | const testSuspend = require("./suspend.test.js");
12 | const testNotification = require("./notification.test.js");
13 | const testDelta = require("./delta.test.js");
14 | const testClose = require("./closeSession.test.js");
15 | const testUtilityOperators = require("./utilityOperators.test.js");
16 |
17 | describe("Engine E2E test", function() {
18 | testConnect();
19 | testGlobal();
20 | testDoc();
21 | testGenericObject();
22 | testField();
23 | testGenericBookmark();
24 | testSuspend();
25 | testNotification();
26 | testDelta();
27 | testClose();
28 | testUtilityOperators();
29 | });
30 |
--------------------------------------------------------------------------------
/test/unit/$fromQix.spec.js:
--------------------------------------------------------------------------------
1 | const chai = require("chai");
2 | const expect = chai.expect;
3 |
4 | var { Observable } = require("rxjs");
5 | var { shareReplay } = require("rxjs/operators");
6 | const mockEngine = require("../util/mock-qix-engine.js");
7 |
8 | // RxQ
9 | var { connectSession } = require("../../dist");
10 | var global = require("../../dist/global");
11 |
12 | describe("Observable from Qix Calls", function() {
13 | // Mock Engine for Testing
14 | var { server, ws } = mockEngine();
15 | var config = {
16 | ws
17 | };
18 |
19 | const session = connectSession(config);
20 | var eng$ = session.global$.pipe(shareReplay(1));
21 |
22 | describe("Global", function() {
23 | it("should have an EngineVersion enum", function() {
24 | expect(global).to.have.property("EngineVersion");
25 | });
26 |
27 | describe("EngineVersion", function() {
28 | it("should be a string", function() {
29 | expect(global.EngineVersion).to.be.a("string");
30 | });
31 | });
32 | });
33 |
34 | after(function() {
35 | server.stop();
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import * as Field from "./Field";
2 | import * as Variable from "./Variable";
3 | import * as GenericObject from "./GenericObject";
4 | import * as GenericDimension from "./GenericDimension";
5 | import * as GenericBookmark from "./GenericBookmark";
6 | import * as GenericVariable from "./GenericVariable";
7 | import * as GenericMeasure from "./GenericMeasure";
8 | import * as Doc from "./Doc";
9 | import * as Global from "./Global";
10 | import connectSession from "./connect/connectSession";
11 | import qAsk from "./operators/qAsk";
12 | import qAskReplay from "./operators/qAskReplay";
13 | import invalidations from "./operators/invalidations";
14 | import suspendUntilCompleted from "./operators/suspendUntilCompleted";
15 |
16 | export { Field };
17 | export { Variable };
18 | export { GenericObject };
19 | export { GenericDimension };
20 | export { GenericBookmark };
21 | export { GenericVariable };
22 | export { GenericMeasure };
23 | export { Doc };
24 | export { Global };
25 | export { connectSession };
26 | export { qAsk };
27 | export { qAskReplay };
28 | export { invalidations };
29 | export { suspendUntilCompleted };
30 |
--------------------------------------------------------------------------------
/docs/recipes/read-app-props.md:
--------------------------------------------------------------------------------
1 | # Read app properties
2 | [Code Sandbox](https://codesandbox.io/embed/o4jjp82zwy)
3 | ```javascript
4 | import { connectSession } from "rxq";
5 | import { OpenDoc } from "rxq/Global";
6 | import { GetAppProperties } from "rxq/Doc";
7 | import { shareReplay, switchMap } from "rxjs/operators";
8 |
9 | // Define the configuration for your session
10 | const config = {
11 | host: "sense.axisgroup.com",
12 | isSecure: true
13 | };
14 |
15 | // Connect the session and share the Global handle
16 | const session = connectSession(config);
17 | const global$ = session.global$;
18 |
19 | // Open an app and share the app handle
20 | const app$ = global$.pipe(
21 | switchMap(h => h.ask(OpenDoc, "aae16724-dfd9-478b-b401-0d8038793adf")),
22 | shareReplay(1)
23 | );
24 |
25 | // Get the app properties
26 | const appProps$ = app$.pipe(
27 | switchMap(h => h.ask(GetAppProperties))
28 | );
29 |
30 | // Write the app title and modified date to the DOM
31 | appProps$.subscribe(props => {
32 | document.querySelector("#content").innerHTML = `The app ${props.qTitle} was last modified at ${props.modifiedDate}`;
33 | });
34 | ```
--------------------------------------------------------------------------------
/src/util/qix-connect-options.js:
--------------------------------------------------------------------------------
1 | export default function(config) {
2 | var cfg = {};
3 |
4 | // Update prefix
5 | var prefix = config.prefix ? config.prefix : '/';
6 |
7 | if (prefix.slice(0, 1) !== '/') {
8 | prefix = '/' + prefix;
9 | };
10 | if (prefix.split('').pop() !== '/') {
11 | prefix = prefix + '/';
12 | };
13 |
14 | // Copy properties + defaults
15 | if (config) {
16 | cfg.mark = config.mark;
17 | cfg.port = config.port;
18 | cfg.appname = config.appname || false;
19 | cfg.host = config.host;
20 | cfg.prefix = prefix;
21 | cfg.origin = config.origin;
22 | cfg.isSecure = config.isSecure;
23 | cfg.rejectUnauthorized = config.rejectUnauthorized;
24 | cfg.headers = config.headers || {};
25 | cfg.ticket = config.ticket || false;
26 | cfg.key = config.key || null;
27 | cfg.cert = config.cert || null;
28 | cfg.ca = config.ca || null;
29 | cfg.pfx = config.pfx || null;
30 | cfg.passphrase = config.passphrase || null;
31 | cfg.identity = config.identity;
32 | }
33 |
34 | return cfg;
35 | };
--------------------------------------------------------------------------------
/docs/recipes/multiple-global-calls.md:
--------------------------------------------------------------------------------
1 | # Multiple Global Calls
2 | [Code Sandbox](https://codesandbox.io/embed/lpzn20399q)
3 | ```javascript
4 | import { connectSession } from "rxq";
5 | import { EngineVersion, GetDocList } from "rxq/Global";
6 | import { shareReplay, switchMap } from "rxjs/operators";
7 |
8 | // Define the configuration for your session
9 | const config = {
10 | host: "sense.axisgroup.com",
11 | isSecure: true
12 | };
13 |
14 | // Connect the session and share the Global handle
15 | const session = connectSession(config);
16 | const global$ = session.global$;
17 |
18 | // Get the engineVersion
19 | const engVer$ = global$.pipe(
20 | switchMap(h => h.ask(EngineVersion))
21 | );
22 |
23 | // Get the Doc List
24 | const doclist$ = global$.pipe(
25 | switchMap(h => h.ask(GetDocList))
26 | );
27 |
28 | // Write the engine version to the DOM
29 | engVer$.subscribe(response => {
30 | document.querySelector("#ver").innerHTML = response.qComponentVersion;
31 | });
32 |
33 | // Write the doc list to the DOM
34 | doclist$.subscribe(dl => {
35 | document.querySelector("#content").innerHTML += dl.map(doc => `
${doc.qDocName}`).join("");
36 | });
37 | ```
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 2.0.4 (2019-03-07)
2 |
3 | ### Bug Fixes
4 | * Fix for issue where delta mode would mutate data when applying patches Fixes [#51](https://github.com/axisgroup/RxQ/issues/51)
5 |
6 | ## 2.0.3 (2019-01-29)
7 |
8 | ### Bug Fixes
9 | * Fix for issue where engine could not be connectly directed to if an appname wasn't supplied Fixes [#61](https://github.com/axisgroup/RxQ/issues/61)
10 |
11 | ## 2.0.2 (2018-10-29)
12 |
13 | ### Bug Fixes
14 | * Replaced shareReplay with publishReplay, refCount in qAskReplay. Closes [#58](https://github.com/axisgroup/RxQ/issues/58)
15 | * handle.ask() now filters out undefined arguments, rather than sending nulls to the Engine. Closes [#57](https://github.com/axisgroup/RxQ/issues/57)
16 |
17 | ### Features
18 | * Added `url` parameter for config file that allows users to specify the WebSocket url to connect to. Closes [#56](https://github.com/axisgroup/RxQ/issues/56)
19 | * Updated generated qix methods to Engine API 12.260.0
20 |
21 | ## 2.0.1 (2018-07-26)
22 |
23 | ### Bug Fixes
24 | * Removed Handle instance checking for `qAsk`, `qInvalidations` operators as temporary fix; was causing issues in other projects and isn't really necessary. Closes [#55](https://github.com/axisgroup/RxQ/issues/55)
25 | * Fixed issue where not defining an appname in the connect config would cause an invalid URL
26 |
27 | ### Code Refactoring
28 | * Cleaned up some dead code
29 | * Updated README with testing information
--------------------------------------------------------------------------------
/docs/recipes/make-selections-on-app-open.md:
--------------------------------------------------------------------------------
1 | # Make Selections on App Open
2 | [Code Sandbox](https://codesandbox.io/embed/18nnwv6xol)
3 | ```javascript
4 | import { connectSession } from "rxq";
5 | import { OpenDoc } from "rxq/Global";
6 | import { GetField } from "rxq/Doc";
7 | import { Select } from "rxq/Field";
8 | import { forkJoin } from "rxjs";
9 | import { mapTo, shareReplay, switchMap } from "rxjs/operators";
10 |
11 | const appname = "aae16724-dfd9-478b-b401-0d8038793adf"
12 |
13 | // Define the configuration for your session
14 | const config = {
15 | host: "sense.axisgroup.com",
16 | isSecure: true,
17 | appname
18 | };
19 |
20 | // Connect the session and share the Global handle
21 | const session = connectSession(config);
22 | const global$ = session.global$;
23 |
24 | // Open an app, get the handle, make a few selections, and then multicast it
25 | const app$ = global$.pipe(
26 | switchMap(h => h.ask(OpenDoc, appname)),
27 | switchMap(h => {
28 | const defaultSelection1$ = h.ask(GetField, "species").pipe(
29 | switchMap(fldH => fldH.ask(Select, "setosa"))
30 | );
31 |
32 | const defaultSelection2$ = h.ask(GetField, "petal_length").pipe(
33 | switchMap(fldH => fldH.ask(Select, ">2"))
34 | );
35 |
36 | return forkJoin(defaultSelection1$, defaultSelection2$).pipe(
37 | mapTo(h)
38 | );
39 |
40 | }),
41 | shareReplay(1)
42 | );
43 |
44 | app$.subscribe(console.log);
45 | ```
--------------------------------------------------------------------------------
/docs/recipes/gen-obj-value.md:
--------------------------------------------------------------------------------
1 | # Calculate a value with a Generic Object
2 | [Code Sandbox](https://codesandbox.io/embed/qz3y4jvm4j)
3 | ```javascript
4 | import { connectSession } from "rxq";
5 | import { OpenDoc } from "rxq/Global";
6 | import { CreateSessionObject } from "rxq/Doc";
7 | import { GetLayout } from "rxq/GenericObject";
8 | import { shareReplay, switchMap } from "rxjs/operators";
9 |
10 | const appname = "aae16724-dfd9-478b-b401-0d8038793adf"
11 |
12 | // Define the configuration for your session
13 | const config = {
14 | host: "sense.axisgroup.com",
15 | isSecure: true,
16 | appname
17 | };
18 |
19 | // Connect the session and share the Global handle
20 | const session = connectSession(config);
21 | const global$ = session.global$;
22 |
23 | // Open an app and share the app handle
24 | const app$ = global$.pipe(
25 | switchMap(h => h.ask(OpenDoc, appname)),
26 | shareReplay(1)
27 | );
28 |
29 | // Create a Generic Object with a formula
30 | const obj$ = app$.pipe(
31 | switchMap(h => h.ask(CreateSessionObject, {
32 | "qInfo": {
33 | "qType": "my-object"
34 | },
35 | "myValue": {
36 | "qValueExpression": "=avg(petal_length)"
37 | }
38 | })),
39 | shareReplay(1)
40 | );
41 |
42 | // Get the layout of the Generic Object to calculate the value
43 | const value$ = obj$.pipe(
44 | switchMap(h => h.ask(GetLayout))
45 | );
46 |
47 | // Write the value to the DOM
48 | value$.subscribe(layout => {
49 | document.querySelector("#val").innerHTML = layout.myValue;
50 | });
51 | ```
--------------------------------------------------------------------------------
/test/util/create-container.js:
--------------------------------------------------------------------------------
1 | var Docker = require("dockerode");
2 | var { Observable } = require("rxjs");
3 | var { publishReplay, refCount } = require("rxjs/operators");
4 | var path = require("path");
5 |
6 | var modelFolder = "/test/e2e/test-model";
7 |
8 | function createContainer(image, port) {
9 | // launch a new container
10 | var container$ = Observable.create(observer => {
11 | var docker = new Docker();
12 |
13 | docker.createContainer(
14 | {
15 | Image: image,
16 | Cmd: ["-S", `DocumentDirectory=${modelFolder}`, "-S", "AcceptEULA=yes"],
17 | ExposedPorts: {
18 | "9076/tcp": {}
19 | },
20 | HostConfig: {
21 | RestartPolicy: {
22 | Name: "always"
23 | },
24 | Binds: [`${path.join(process.cwd(), modelFolder)}:${modelFolder}`], // ["./models:/models"],
25 | PortBindings: {
26 | "9076/tcp": [
27 | {
28 | HostPort: port
29 | }
30 | ]
31 | }
32 | }
33 | },
34 | (err, container) => {
35 | if (err) return observer.error(err);
36 |
37 | container.start((err, data) => {
38 | if (err) return observer.error(err);
39 | setTimeout(() => {
40 | observer.next(container);
41 | observer.complete();
42 | }, 2000);
43 | });
44 | }
45 | );
46 | }).pipe(
47 | publishReplay(1),
48 | refCount()
49 | );
50 |
51 | return container$;
52 | }
53 |
54 | module.exports = createContainer;
55 |
--------------------------------------------------------------------------------
/docs/recipes/current-selections.md:
--------------------------------------------------------------------------------
1 | # Current Selections
2 | [Code Sandbox](https://codesandbox.io/embed/8xk40y9588)
3 | ```javascript
4 | import { connectSession } from "rxq";
5 | import { OpenDoc } from "rxq/Global";
6 | import { CreateSessionObject, GetField } from "rxq/Doc";
7 | import { GetLayout } from "rxq/GenericObject";
8 | import { shareReplay, startWith, switchMap } from "rxjs/operators";
9 |
10 | const appname = "aae16724-dfd9-478b-b401-0d8038793adf"
11 |
12 | // Define the configuration for your session
13 | const config = {
14 | host: "sense.axisgroup.com",
15 | isSecure: true,
16 | appname
17 | };
18 |
19 | // Connect the session and share the Global handle
20 | const session = connectSession(config);
21 | const global$ = session.global$;
22 |
23 | // Open an app and share the app handle
24 | const app$ = global$.pipe(
25 | switchMap(h => h.ask(OpenDoc, appname)),
26 | shareReplay(1)
27 | );
28 |
29 | // Create a Generic Object with the current selections
30 | const obj$ = app$.pipe(
31 | switchMap(h => h.ask(CreateSessionObject, {
32 | "qInfo": {
33 | "qType": "my-object"
34 | },
35 | "qSelectionObjectDef": {}
36 | })),
37 | shareReplay(1)
38 | );
39 |
40 | // Get the latest selections whenever the model changes
41 | const selections$ = obj$.pipe(
42 | switchMap(h => h.invalidated$.pipe(startWith(h))),
43 | switchMap(h => h.ask(GetLayout))
44 | );
45 |
46 | // Print the selections to the DOM
47 | selections$.subscribe(layout => {
48 | document.querySelector("#content").innerHTML = layout.qSelectionObject.qSelections.map(sel => `${sel.qField}: ${sel.qSelected}`).join("")
49 | });
50 | ```
--------------------------------------------------------------------------------
/docs/introduction/rxq-vs-enigma.md:
--------------------------------------------------------------------------------
1 | # RxQ vs. enigma.js
2 | [enigma.js](https://github.com/qlik-oss/enigma.js) is a fantastic library by the Qlik R&D team that has a similar goal to RxQ: it provides a means for working with the Qlik Associative Engine (QAE) in a scalable way. RxQ was largely inspired by the work that was done on enigma.js.
3 |
4 | enigma.js and RxQ take fundamentally different approaches to how they deal with the asynchronous nature of QAE. enigma.js is built around the Promise model. In JavaScript, Promises are objects that model an asynchronous operation that eventually completes with a single value, or fails. Promises have become a standard async method in JavaScript development. They work great for one time asynchronous events, can be chained, and are fairly easy to learn and put to work. Therefore, they are a solid choice for getting started with QAE.
5 |
6 | RxQ does not use Promises in its implementation. Since it is meant to enable reactive programming with QAE, it instead uses the [Observable](https://egghead.io/lessons/javascript-introducing-the-observable) pattern. Observables are the core component of RxJS and provide the benefits described in [Why Rx?](why-rx.md). However, Observables are a newer concept to the JavaScript world and have not been as widely adopted. Furthermore, the learning curve is much steeper than Promises.
7 |
8 | When comparing enigma.js to RxQ, our feeling is that enigma.js has a much lower learning curve and works exceptionally well for basic use cases. While RxQ has a higher learning curve due to the usage of Rx, we believe that those who use Rx via RxQ will find that it shines as you scale up to more advanced applications of QAE.
--------------------------------------------------------------------------------
/src/util/qix-connect-string.js:
--------------------------------------------------------------------------------
1 | export default function(config) {
2 | var cfg = {};
3 | // Copy properties + defaults
4 | if (config) {
5 | cfg.port = config.port;
6 | cfg.appname = config.appname || false;
7 | cfg.host = config.host;
8 | cfg.prefix = config.prefix || false;
9 | cfg.isSecure = config.isSecure;
10 | cfg.identity = config.identity;
11 | cfg.ticket = config.ticket;
12 | }
13 |
14 | return ConnectionString(cfg);
15 | };
16 |
17 | function ConnectionString(config) {
18 |
19 | // Define host
20 | var host = (config && config.host) ? config.host : 'localhost';
21 | // Define port
22 | var port;
23 |
24 | // Configure port if port is undefined
25 | if (config && config.port === undefined) {
26 | port = '';
27 | } else {
28 | port = (config && config.port) ? ':' + config.port : '';
29 | };
30 |
31 | // Define secure vs. unsecure
32 | var isSecure = (config && config.isSecure) ? 'wss://' : 'ws://';
33 |
34 | // Prefix
35 | var prefix = config.prefix ? config.prefix : '/';
36 |
37 | if (prefix.slice(0, 1) !== '/') {
38 | prefix = '/' + prefix;
39 | };
40 | if (prefix.split('').pop() !== '/') {
41 | prefix = prefix + '/';
42 | };
43 |
44 | var suffix = config.appname ? 'app/' + config.appname + '/' : 'app/';
45 | var identity = (config && config.identity) ? 'identity/' + config.identity : '';
46 | var ticket = config.ticket ? '?qlikTicket=' + config.ticket : '';
47 |
48 | var url = isSecure + host + port + prefix + suffix + identity + ticket;
49 |
50 | return url;
51 |
52 | }
--------------------------------------------------------------------------------
/docs/basics/suspending-invalidations.md:
--------------------------------------------------------------------------------
1 | # Suspending Invalidations
2 | The previous section detailed how to hook into Handle invalidation events to create automatic updating streams from Handles. It is common to scale this pattern up when building an interactive dashboard; the dashboard may have several charts on it, all of which are hooking into various layout streams that are auto-updating from invalidation events.
3 |
4 | This pattern works great when simple operations like applying a selection cause invalidation. However, sometimes we want to make several API calls that alter the app before we update everything. This can cause havoc with our auto-updating layouts: they may try to update after every API call, rather than waiting for all the calls to be finished first. RxQ can handle this scenario in two ways.
5 |
6 | ## Manually changing suspense status
7 | The `session` object returned by RxQ contains methods `session.suspend()` and `session.unsuspend()` that can be called to manually change the suspense status of the session.
8 |
9 | ## Using the `suspendUntilCompleted` operator
10 | RxQ provides an operator called `suspendUntilCompleted` that can handle the suspense side-effects for you. This operator can be applied to an Observable that completes. It takes a session as an input. When the resulting Observable is subscribed to, it will pause the invalidation event streams in the session, and then execute the Observable stream. When the Observable completes, it will unpause the session and pass down any invalidation events that occured during the paused period.
11 |
12 | An example of this can be seen in the [Batch Invalidations with Suspend](../recipes/batch-invalidations.html) example.
--------------------------------------------------------------------------------
/docs/recipes/set-app-props.md:
--------------------------------------------------------------------------------
1 | # Set App Properties and Listen for Changes
2 | [Code Sandbox](https://codesandbox.io/embed/n4v0p3m4n4)
3 | ```javascript
4 | import { connectSession } from "rxq";
5 | import { OpenDoc } from "rxq/Global";
6 | import { GetAppProperties, SetAppProperties } from "rxq/Doc";
7 | import { publish, shareReplay, startWith,
8 | switchMap, withLatestFrom } from "rxjs/operators";
9 | import { fromEvent } from "rxjs";
10 |
11 | const appname = "aae16724-dfd9-478b-b401-0d8038793adf"
12 |
13 | // Define the configuration for your session
14 | const config = {
15 | host: "sense.axisgroup.com",
16 | isSecure: true,
17 | appname
18 | };
19 |
20 | // Connect the session and share the Global handle
21 | const session = connectSession(config);
22 | const global$ = session.global$;
23 |
24 | // Open an app and share the app handle
25 | const app$ = global$.pipe(
26 | switchMap(h => h.ask(OpenDoc, appname)),
27 | shareReplay(1)
28 | );
29 |
30 | // Get the app properties any time the app invalidates
31 | const appProps$ = app$.pipe(
32 | switchMap(h => h.invalidated$.pipe(startWith(h))),
33 | switchMap(h => h.ask(GetAppProperties))
34 | );
35 |
36 | // Write the app title and modified date to the DOM
37 | appProps$.subscribe(props => {
38 | document.querySelector("#content").innerHTML = `The app's prop "random" is ${props.random}`;
39 | });
40 |
41 | // Whenever a user clicks the button, update the app props with a random prop
42 | const updateAppProps$ = fromEvent(document.querySelector("#set-random"), "click").pipe(
43 | withLatestFrom(app$),
44 | switchMap(([evt, h]) => h.ask(SetAppProperties, {
45 | "random": Math.random()
46 | })),
47 | publish()
48 | );
49 |
50 | // Connect the app update mechanism
51 | updateAppProps$.connect();
52 | ```
--------------------------------------------------------------------------------
/scripts/compile_esm2015.js:
--------------------------------------------------------------------------------
1 | var path = require("path");
2 | var fs = require("fs");
3 | var rimraf = require("rimraf");
4 | var babel = require("babel-core");
5 |
6 | // Set up dist
7 | mkDir("dist");
8 | // Remove _esm5 if it exists
9 | rimraf.sync("dist/_esm2015", {}, function() {});
10 | // Set up _esm2015
11 | mkDir("dist/_esm2015");
12 |
13 | // Iterate through source code and copy over
14 | var srcFolder = "src";
15 | var tgtFolder = "dist/_esm2015";
16 |
17 | compileFromDir(srcFolder, tgtFolder);
18 |
19 | function compileFromDir(srcFolder, tgtFolder) {
20 | fs.readdir(srcFolder, function(err, files) {
21 | files.forEach(function(file) {
22 | fs.lstat(path.join(srcFolder, file), function(err, stats) {
23 | if (stats.isFile() && file.slice(-3) === ".js") {
24 | //console.log("convert the file " + file);
25 | babel.transformFile(
26 | path.join(srcFolder, file),
27 | {
28 | presets: [],
29 | plugins: ["transform-object-rest-spread", "transform-runtime"]
30 | },
31 | function(err, result) {
32 | //if(err) return console.log(err);
33 | fs.writeFile(path.join(tgtFolder, file), result.code, function(
34 | err
35 | ) {
36 | if (err) return console.log(err);
37 | });
38 | }
39 | );
40 | } else if (stats.isDirectory()) {
41 | mkDir(path.join(tgtFolder, file));
42 | compileFromDir(
43 | path.join(srcFolder, file),
44 | path.join(tgtFolder, file)
45 | );
46 | }
47 | });
48 | });
49 | });
50 | }
51 |
52 | function mkDir(dir) {
53 | if (!fs.existsSync(dir)) {
54 | fs.mkdirSync(dir);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/scripts/compile_esm5.js:
--------------------------------------------------------------------------------
1 | var path = require("path");
2 | var fs = require("fs");
3 | var rimraf = require("rimraf");
4 | var babel = require("babel-core");
5 |
6 | // Set up dist
7 | mkDir("dist");
8 | // Remove _esm5 if it exists
9 | rimraf.sync("dist/_esm5", {}, function() {});
10 | // Set up _esm5
11 | mkDir("dist/_esm5");
12 |
13 | // Iterate through source code and copy over
14 | var srcFolder = "src";
15 | var tgtFolder = "dist/_esm5";
16 |
17 | compileFromDir(srcFolder, tgtFolder);
18 |
19 | function compileFromDir(srcFolder, tgtFolder) {
20 | fs.readdir(srcFolder, function(err, files) {
21 | files.forEach(function(file) {
22 | fs.lstat(path.join(srcFolder, file), function(err, stats) {
23 | if (stats.isFile() && file.slice(-3) === ".js") {
24 | //console.log("convert the file " + file);
25 | babel.transformFile(
26 | path.join(srcFolder, file),
27 | {
28 | presets: [["env", { modules: false }]],
29 | plugins: ["transform-object-rest-spread", "transform-runtime"]
30 | },
31 | function(err, result) {
32 | //if(err) return console.log(err);
33 | fs.writeFile(path.join(tgtFolder, file), result.code, function(
34 | err
35 | ) {
36 | if (err) return console.log(err);
37 | });
38 | }
39 | );
40 | } else if (stats.isDirectory()) {
41 | mkDir(path.join(tgtFolder, file));
42 | compileFromDir(
43 | path.join(srcFolder, file),
44 | path.join(tgtFolder, file)
45 | );
46 | }
47 | });
48 | });
49 | });
50 | }
51 |
52 | function mkDir(dir) {
53 | if (!fs.existsSync(dir)) {
54 | fs.mkdirSync(dir);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/scripts/compile_cjs.js:
--------------------------------------------------------------------------------
1 | var path = require("path");
2 | var fs = require("fs");
3 | var rimraf = require("rimraf");
4 | var babel = require("babel-core");
5 |
6 | // Set up dist
7 | mkDir("dist");
8 |
9 | // Remove _cjs if it exists
10 | rimraf.sync("dist/_cjs", {}, function() {});
11 | // Set up _cjs
12 | mkDir("dist/_cjs");
13 |
14 | // Iterate through source code and copy over
15 | var srcFolder = "src";
16 | var tgtFolder = "dist/_cjs";
17 |
18 | compileFromDir(srcFolder, tgtFolder);
19 |
20 | function compileFromDir(srcFolder, tgtFolder) {
21 | fs.readdir(srcFolder, function(err, files) {
22 | files.forEach(function(file) {
23 | fs.lstat(path.join(srcFolder, file), function(err, stats) {
24 | if (stats.isFile() && file.match(/.js$/g) !== null) {
25 | babel.transformFile(
26 | path.join(srcFolder, file),
27 | {
28 | presets: ["es2015"],
29 | plugins: [
30 | "transform-object-rest-spread",
31 | "add-module-exports",
32 | "transform-es2015-modules-commonjs",
33 | "transform-runtime"
34 | ]
35 | },
36 | function(err, result) {
37 | if (err) return console.log(err);
38 | fs.writeFile(path.join(tgtFolder, file), result.code, function(
39 | err
40 | ) {
41 | if (err) return console.log(err);
42 | });
43 | }
44 | );
45 | } else if (stats.isDirectory()) {
46 | mkDir(path.join(tgtFolder, file));
47 | compileFromDir(
48 | path.join(srcFolder, file),
49 | path.join(tgtFolder, file)
50 | );
51 | }
52 | });
53 | });
54 | });
55 | }
56 |
57 | function mkDir(dir) {
58 | if (!fs.existsSync(dir)) {
59 | fs.mkdirSync(dir);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/scripts/make-packages.js:
--------------------------------------------------------------------------------
1 | var pkg = require("../package.json");
2 | var fs = require("fs");
3 | var path = require("path");
4 |
5 | // Copy package to dist folder
6 | const modPkg = {
7 | ...pkg,
8 | main: "./_cjs/index.js",
9 | module: "./_esm5/index.js",
10 | es2015: "./_esm2015/index.js",
11 | sideEffects: false
12 | };
13 |
14 | fs.writeFile(
15 | path.join(__dirname, "../dist/package.json"),
16 | JSON.stringify(modPkg, null, "\t"),
17 | function(err) {
18 | if (err) console.log(err);
19 | }
20 | );
21 |
22 | // Qlik Class Entrypoints
23 | const classes = [
24 | "Doc",
25 | "Field",
26 | "GenericBookmark",
27 | "GenericDerivedFields",
28 | "GenericDimension",
29 | "GenericMeasure",
30 | "GenericObject",
31 | "GenericVariable",
32 | "Global",
33 | "Variable"
34 | ];
35 | classes.forEach(qClass => {
36 | var operatorFolder = path.join(__dirname, "../src", qClass);
37 | var operatorOutputFolder = path.join(__dirname, "../dist", qClass);
38 | mkDir(operatorOutputFolder);
39 |
40 | fs.readdir(operatorFolder, (err, files) => {
41 | if (err) return console.log(err);
42 | createPackage("index.js", qClass, operatorOutputFolder, 1);
43 | });
44 | });
45 |
46 | function createPackage(filename, folderPath, outputFolder, depth) {
47 | var trail = "../".repeat(depth);
48 |
49 | var pkg = {
50 | main: `${trail}_cjs/${folderPath}/${filename}`,
51 | module: `${trail}_esm5/${folderPath}/${filename}`,
52 | es2015: `${trail}_esm2015/${folderPath}/${filename}`,
53 | sideEffects: false
54 | };
55 |
56 | mkDir(outputFolder);
57 |
58 | fs.writeFile(
59 | path.join(outputFolder, "package.json"),
60 | JSON.stringify(pkg, null, "\t"),
61 | function(err) {
62 | if (err) console.log(err);
63 | }
64 | );
65 | }
66 |
67 | function mkDir(dir) {
68 | if (!fs.existsSync(dir)) {
69 | fs.mkdirSync(dir);
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/docs/basics/connecting.md:
--------------------------------------------------------------------------------
1 | # Connecting
2 | In order to work with QAE, we need to establish a session with the Engine. This session is created by connecting to an Engine over a WebSocket.
3 |
4 | RxQ provides a function called `connectSession` that will establish the session with the Engine. It returns a Session object that contains an Observable for the Global Handle, a `close` function for closing the session imperatively, and an Observable of notifications from the engine and RxQ (useful for debugging):
5 |
6 | Session Object Properties
7 | * **global$** - *(Observable)* An Observable for the Global Handle of the session
8 | * **close** - *(Function)* A function that closes the WebSocket, ending the session
9 | * **notifications$** - *(Observable) An Observable of notifications from the engine and RxQ
10 |
11 | ## Setup
12 | The `connectSession` function takes in a configuration object that determines the session that is created. It can read the following properties:
13 |
14 | * **host** - *(String)* Hostname of server
15 | * **appname** - *(String)* Scoped connection to app.
16 | * **isSecure** - *(Boolean)* If true uses wss and port 443, otherwise ws and port 80
17 | * **port** - *(Integer)* Port of connection, defaults 443/80
18 | * **prefix** - *(String)* Virtual Proxy, defaults to '/'
19 | * **origin** - *(String)* Origin of requests, node only.
20 | * **rejectUnauthorized** - *(Boolean)* False will ignore unauthorized self-signed certs.
21 | * **headers** - *(Object)* HTTP headers
22 | * **ticket** - *(String)* Qlik Sense ticket, consumes ticket on Connect()
23 | * **key** - *(String)* Client Certificate key for QIX connections
24 | * **cert** - *(String)* Client certificate for QIX connections
25 | * **ca** - *(Array of String)* CA root certificates for QIX connections
26 | * **identity** - *(String)* Session identity
27 |
28 | ## Usage
29 | The following example shows how to use RxQ to connect to an Engine behind Qlik Sense Desktop:
30 | ```javascript
31 | import { connectSession } from "rxq";
32 |
33 | const session = connectSession({
34 | host: "localhost",
35 | port: 4848,
36 | isSecure: false
37 | });
38 |
39 | session.global$.subscribe((globalHandle) => {
40 | // Do something with the Global Handle
41 | });
42 | ```
--------------------------------------------------------------------------------
/docs/recipes/get-data-page-on-invalid.md:
--------------------------------------------------------------------------------
1 | # Get a page of data from a HyperCube on invalidation
2 | [Code Sandbox](https://codesandbox.io/embed/j796z127n3)
3 | ```javascript
4 | import { connectSession } from "rxq";
5 | import { OpenDoc } from "rxq/Global";
6 | import { CreateSessionObject } from "rxq/Doc";
7 | import { GetLayout, GetHyperCubeData } from "rxq/GenericObject";
8 | import { shareReplay, startWith, switchMap } from "rxjs/operators";
9 |
10 | const appname = "aae16724-dfd9-478b-b401-0d8038793adf"
11 |
12 | // Define the configuration for your session
13 | const config = {
14 | host: "sense.axisgroup.com",
15 | isSecure: true,
16 | appname
17 | };
18 |
19 | // Connect the session and share the Global handle
20 | const session = connectSession(config);
21 | const global$ = session.global$;
22 |
23 | // Open an app and share the app handle
24 | const app$ = global$.pipe(
25 | switchMap(h => h.ask(OpenDoc, appname)),
26 | shareReplay(1)
27 | );
28 |
29 | // Create a Generic Object with a metric
30 | const obj$ = app$.pipe(
31 | switchMap(h => h.ask(CreateSessionObject, {
32 | "qInfo": {
33 | "qType": "my-object"
34 | },
35 | "qHyperCubeDef": {
36 | "qDimensions": [
37 | {
38 | "qDef": {
39 | "qFieldDefs": ["petal_length"]
40 | }
41 | }
42 | ],
43 | "qMeasures": [
44 | {
45 | "qDef": {
46 | "qDef": "=avg(petal_width)"
47 | }
48 | }
49 | ]
50 | }
51 | })),
52 | shareReplay(1)
53 | );
54 |
55 | // On invalidation, get layout to validate, then request a data page
56 | const data$ = obj$.pipe(
57 | switchMap(h => h.invalidated$.pipe(startWith(h))),
58 | switchMap(h => h.ask(GetLayout), (h, layout) => h),
59 | switchMap(h => h.ask(GetHyperCubeData, "/qHyperCubeDef", [
60 | {
61 | qTop: 0,
62 | qLeft: 0,
63 | qWidth: 2,
64 | qHeight: 100
65 | }
66 | ]))
67 | );
68 |
69 | data$.subscribe(pages => {
70 | const data = pages[0].qMatrix;
71 | document.querySelector("tbody").innerHTML = data.map(row => {
72 | return `
73 | | ${row[0].qText} |
74 | ${row[1].qText} |
75 |
`
76 | }).join("");
77 | });
78 |
79 | ```
--------------------------------------------------------------------------------
/docs/recipes/batch-invalidations.md:
--------------------------------------------------------------------------------
1 | # Batch Invalidations
2 | [Code Sandbox](https://codesandbox.io/embed/7z64r623r6)
3 | ```javascript
4 | import { connectSession, suspendUntilCompleted } from "rxq";
5 | import { OpenDoc } from "rxq/Global";
6 | import { ClearAll, CreateSessionObject, GetField } from "rxq/Doc";
7 | import { GetLayout } from "rxq/GenericObject";
8 | import { LowLevelSelect } from "rxq/Field";
9 | import { fromEvent, concat } from "rxjs";
10 | import { publish, shareReplay, startWith, switchMap, take } from "rxjs/operators";
11 |
12 | const appname = "aae16724-dfd9-478b-b401-0d8038793adf"
13 |
14 | // Define the configuration for your session
15 | const config = {
16 | host: "sense.axisgroup.com",
17 | isSecure: true,
18 | appname
19 | };
20 |
21 | // Connect the session and share the Global handle
22 | const session = connectSession(config);
23 | const global$ = session.global$;
24 |
25 | // Open app in session
26 | const app$ = global$.pipe(
27 | switchMap(h => h.ask(OpenDoc, appname)),
28 | shareReplay(1)
29 | );
30 |
31 | // Get a stream of layouts
32 | const layout$ = app$.pipe(
33 | switchMap(h => h.ask(CreateSessionObject, {
34 | "qInfo": {
35 | "qType": "custom"
36 | },
37 | "value": {
38 | "qValueExpression": "=avg(petal_width)"
39 | }
40 | })),
41 | switchMap(h => h.invalidated$.pipe(startWith(h))),
42 | switchMap(h => h.ask(GetLayout)),
43 | shareReplay(1)
44 | );
45 |
46 | // Log latest layout
47 | layout$.subscribe(layout => {
48 | document.querySelector("#metric").innerHTML = layout.value;
49 | });
50 |
51 | // Clear operation
52 | const clearAll$ = app$.pipe(
53 | switchMap(h => h.ask(ClearAll)),
54 | take(1)
55 | );
56 |
57 | // Filter a field operation
58 | const filterFld$ = app$.pipe(
59 | switchMap(h => h.ask(GetField, "species")),
60 | switchMap(h => h.ask(LowLevelSelect, [0], false)),
61 | take(1)
62 | );
63 |
64 | // Create batched operations sequence with suspension
65 | const batchedOps$ = concat(clearAll$, filterFld$).pipe(
66 | suspendUntilCompleted(session)
67 | );
68 |
69 | // Click stream to trigger the batched operations
70 | const runOps$ = fromEvent(document.querySelector("button"), "click").pipe(
71 | switchMap(() => batchedOps$),
72 | publish()
73 | );
74 |
75 | runOps$.connect();
76 | ```
--------------------------------------------------------------------------------
/docs/SUMMARY.md:
--------------------------------------------------------------------------------
1 | # Table of Contents
2 | * [RxQ v2.0.1](README.md)
3 | * [Introduction]()
4 | * [Why Rx?](/introduction/why-rx.md)
5 | * [RxQ vs. Enigma](introduction/rxq-vs-enigma.md)
6 | * [Core Concepts](introduction/core-concepts.md)
7 | * [Basics]()
8 | * [Connecting](basics/connecting.md)
9 | * [Making API Calls](basics/making-api-calls.md)
10 | * [Reusing Handles](basics/reusing-handles.md)
11 | * [Handle Invalidations](basics/handle-invalidations.md)
12 | * [Suspending Invalidations](basics/suspending-invalidations.md)
13 | * [Closing a Session](basics/closing-a-session.md)
14 | * [Notifications](basics/notifications.md)
15 | * [Delta Mode](basics/delta.md)
16 | * [Shortcut Operators](basics/shortcut-operators.md)
17 | * [Recipes]()
18 | * [01. Connect to an Engine](/recipes/connect.md)
19 | * [02. Get the Engine Version](/recipes/engine-version.md)
20 | * [03. Make Multiple Global Calls](/recipes/multiple-global-calls.md)
21 | * [04. Open an App](/recipes/open-an-app.md)
22 | * [05. Get App Properties](/recipes/read-app-props.md)
23 | * [06. Set App Properties and Listen for Changes](/recipes/set-app-props.md)
24 | * [07. Calculate a Value with a GenericObject](/recipes/gen-obj-value.md)
25 | * [08. Get a stream of Current Selections](/recipes/current-selections.md)
26 | * [09. Make a Selection in a Field](/recipes/make-a-selection.md)
27 | * [10. Loop through Selections in a Field](/recipes/loop-selections.md)
28 | * [11. Make Selections in a Listbox](/recipes/make-lb-selections.md)
29 | * [12. Search in a Listbox](/recipes/search-lb.md)
30 | * [13. Apply Patches to an Object](/recipes/apply-patches.md)
31 | * [14. Get a page of data in a HyperCube on invalidation](/recipes/get-data-page-on-invalid.md)
32 | * [15. Get pages of data in parallel](/recipes/get-pages-in-parallel.md)
33 | * [16. Get pages of data sequentially](/recipes/get-pages-in-sequence.md)
34 | * [17. Make selections on app open](/recipes/make-selections-on-app-open.md)
35 | * [18. Calculate the duration of an API call and response](/recipes/calc-time.md)
36 | * [19. Batch Invalidations with Suspend](/recipes/batch-invalidations.md)
37 | * [20. Toggle Calculations Between Multiple Apps](/recipes/toggle-sessions.md)
38 | * [21. Auto-reconnect Generic Objects after a network drop](/recipes/offline.md)
39 | * [22. Using shortcut operators to get a layout stream](/recipes/shortcut-operators.md)
--------------------------------------------------------------------------------
/test/unit/connectSession.spec.js:
--------------------------------------------------------------------------------
1 | const chai = require("chai");
2 | chai.use(require("chai-generator"));
3 | const expect = chai.expect;
4 |
5 | var { Observable, Subject } = require("rxjs");
6 | var { pluck, take } = require("rxjs/operators");
7 | const mockEngine = require("../util/mock-qix-engine.js");
8 | const isObservable = require("../util/isObservable");
9 |
10 | // RxQ
11 | var { connectSession } = require("../../dist");
12 | var Handle = require("../../dist/_cjs/handle");
13 | var Session = require("../../dist/_cjs/session");
14 |
15 | describe("connectSession", function() {
16 | // Mock Engine for Testing
17 | var { server, ws } = mockEngine();
18 | var config = {
19 | ws
20 | };
21 |
22 | const session = connectSession(config);
23 | var eng$ = session.global$;
24 |
25 | it("should be a function", function() {
26 | expect(connectSession).to.be.a("function");
27 | });
28 |
29 | it("should return an object", function() {
30 | expect(session).to.be.an("object");
31 | });
32 |
33 | describe("notifications$", function() {
34 | const notifications$ = session.notifications$;
35 |
36 | it("should be an Observable", function() {
37 | expect(isObservable(notifications$)).to.equal(true);
38 | });
39 | });
40 |
41 | describe("close", function() {
42 | const close = session.close;
43 |
44 | it("should be a function", function() {
45 | expect(close).to.be.a("function");
46 | });
47 | });
48 |
49 | describe("suspend", function() {
50 | const suspend = session.suspend;
51 |
52 | it("should be a function", function() {
53 | expect(suspend).to.be.a("function");
54 | });
55 | });
56 |
57 | describe("unsuspend", function() {
58 | const unsuspend = session.unsuspend;
59 |
60 | it("should be a function", function() {
61 | expect(unsuspend).to.be.a("function");
62 | });
63 | });
64 |
65 | describe("global$", function() {
66 | it("should be an Observable", function() {
67 | expect(isObservable(eng$)).to.equal(true);
68 | });
69 |
70 | it("should return a Handle", function(done) {
71 | eng$.subscribe(h => {
72 | expect(h).to.be.instanceof(Handle);
73 | done();
74 | });
75 | });
76 |
77 | it("the Handle should be -1", function(done) {
78 | eng$.subscribe(h => {
79 | expect(h.handle).to.equal(-1);
80 | done();
81 | });
82 | });
83 | });
84 |
85 | after(function() {
86 | server.stop();
87 | });
88 | });
89 |
--------------------------------------------------------------------------------
/docs/recipes/make-a-selection.md:
--------------------------------------------------------------------------------
1 | # Make a Selection
2 | [Code Sandbox](https://codesandbox.io/embed/6xq8m44lnr)
3 | ```javascript
4 | import { connectSession } from "rxq";
5 | import { OpenDoc } from "rxq/Global";
6 | import { CreateSessionObject, GetField } from "rxq/Doc";
7 | import { GetLayout } from "rxq/GenericObject";
8 | import { GetCardinal, Select } from "rxq/Field";
9 | import { mapTo, publish, shareReplay, startWith, switchMap, withLatestFrom } from "rxjs/operators";
10 | import { fromEvent, merge } from "rxjs";
11 |
12 | const appname = "aae16724-dfd9-478b-b401-0d8038793adf"
13 |
14 | // Define the configuration for your session
15 | const config = {
16 | host: "sense.axisgroup.com",
17 | isSecure: true,
18 | appname
19 | };
20 |
21 | // Connect the session and share the Global handle
22 | const session = connectSession(config);
23 | const global$ = session.global$;
24 |
25 | // Open an app and share the app handle
26 | const app$ = global$.pipe(
27 | switchMap(h => h.ask(OpenDoc, appname)),
28 | shareReplay(1)
29 | );
30 |
31 | // Create a Generic Object with the current selections
32 | const obj$ = app$.pipe(
33 | switchMap(h => h.ask(CreateSessionObject, {
34 | "qInfo": {
35 | "qType": "my-object"
36 | },
37 | "qSelectionObjectDef": {}
38 | })),
39 | shareReplay(1)
40 | );
41 |
42 | // Get the latest selections whenever the model changes
43 | const selections$ = obj$.pipe(
44 | switchMap(h => h.invalidated$.pipe(startWith(h))),
45 | switchMap(h => h.ask(GetLayout))
46 | );
47 |
48 | // Print the selections to the DOM
49 | selections$.subscribe(layout => {
50 | document.querySelector("#content").innerHTML = layout.qSelectionObject.qSelections.map(sel => `${sel.qField}: ${sel.qSelected}`).join("")
51 | });
52 |
53 | // Get a field
54 | const fld$ = app$.pipe(
55 | switchMap(h => h.ask(GetField, "species")),
56 | shareReplay(1)
57 | );
58 |
59 | // On click, emit the value "setosa"
60 | const selectSetosa$ = fromEvent(document.querySelector("#filter"), "click").pipe(
61 | mapTo("setosa")
62 | );
63 |
64 | // On click, emit an empty string
65 | const clearSelect$ = fromEvent(document.querySelector("#unfilter"), "click").pipe(
66 | mapTo("")
67 | );
68 |
69 | // Create a stream of select actions to the field from the button clicks
70 | const selectValue$ = merge(selectSetosa$, clearSelect$).pipe(
71 | withLatestFrom(fld$),
72 | switchMap(([sel, h]) => h.ask(Select, sel)),
73 | publish()
74 | );
75 |
76 | // Connect the selection stream
77 | selectValue$.connect();
78 | ```
--------------------------------------------------------------------------------
/test/e2e/field.test.js:
--------------------------------------------------------------------------------
1 | const chai = require("chai");
2 | chai.use(require("chai-generator"));
3 | const expect = chai.expect;
4 |
5 | var createContainer = require("../util/create-container");
6 | var {
7 | publish,
8 | publishReplay,
9 | refCount,
10 | shareReplay,
11 | switchMap,
12 | take,
13 | tap,
14 | withLatestFrom
15 | } = require("rxjs/operators");
16 |
17 | var { OpenDoc } = require("../../dist/global");
18 | var { connectSession } = require("../../dist");
19 | var Handle = require("../../dist/_cjs/handle");
20 |
21 | var { GetField } = require("../../dist/doc");
22 | var { GetCardinal, GetNxProperties } = require("../../dist/field");
23 |
24 | var { port, image } = require("./config.json");
25 |
26 | // launch a new container
27 | var container$ = createContainer(image, port);
28 |
29 | var eng$ = container$.pipe(
30 | switchMap(() => {
31 | return connectSession({
32 | host: "localhost",
33 | port: port,
34 | isSecure: false
35 | }).global$;
36 | }),
37 | publishReplay(1),
38 | refCount()
39 | );
40 |
41 | const app$ = eng$.pipe(
42 | switchMap(handle => handle.ask(OpenDoc, "iris.qvf")),
43 | publishReplay(1),
44 | refCount()
45 | );
46 |
47 | const field$ = app$.pipe(
48 | switchMap(handle => handle.ask(GetField, "species")),
49 | shareReplay(1)
50 | );
51 |
52 | function testField() {
53 | describe("Field Class", function() {
54 | before(function(done) {
55 | this.timeout(10000);
56 | container$.subscribe(() => done());
57 | });
58 |
59 | describe("GetNxProperties", function() {
60 | const fldProps$ = field$.pipe(
61 | switchMap(h => h.ask(GetNxProperties)),
62 | shareReplay(1)
63 | );
64 |
65 | it("should return an object", function(done) {
66 | fldProps$.subscribe(props => {
67 | expect(props).to.be.a("object");
68 | done();
69 | });
70 | });
71 | });
72 |
73 | describe("GetCardinal", function() {
74 | const fldCard$ = field$.pipe(
75 | switchMap(h => h.ask(GetCardinal)),
76 | shareReplay(1)
77 | );
78 |
79 | it("should equal 3 for the field 'species'", function(done) {
80 | fldCard$.subscribe(card => {
81 | expect(card).to.equal(3);
82 | done();
83 | });
84 | });
85 | });
86 |
87 | after(function(done) {
88 | container$.subscribe(container =>
89 | container.kill((err, result) => {
90 | container.remove();
91 | done();
92 | })
93 | );
94 | });
95 | });
96 | }
97 |
98 | module.exports = testField;
99 |
--------------------------------------------------------------------------------
/docs/recipes/toggle-sessions.md:
--------------------------------------------------------------------------------
1 | # Toggle Sessions
2 | [Code Sandbox](https://codesandbox.io/embed/nnr22k0nx4)
3 | ```javascript
4 | import { connectSession } from "rxq";
5 | import { GetActiveDoc, OpenDoc } from "rxq/Global";
6 | import { GetAppProperties, GetTablesAndKeys } from "rxq/Doc";
7 | import { fromEvent, forkJoin, combineLatest } from "rxjs";
8 | import { map, pluck, shareReplay, startWith, switchMap, withLatestFrom } from "rxjs/operators";
9 |
10 | // Define the configuration for your session
11 | const configs = [
12 | {
13 | host: "sense.axisgroup.com",
14 | isSecure: true,
15 | appname: "aae16724-dfd9-478b-b401-0d8038793adf"
16 | },
17 | {
18 | host: "sense.axisgroup.com",
19 | isSecure: true,
20 | appname: "3a64c6ff-94b4-43e3-b993-4040cf889c64"
21 | },
22 | ];
23 |
24 | // Create a stream for the current session being viewed
25 | const sessionNo$ = fromEvent(document.querySelector("select"), "change").pipe(
26 | map(evt => parseInt(evt.target.value)),
27 | startWith(0)
28 | );
29 |
30 | // Create an array of sessions
31 | const sessions$ = forkJoin(
32 | configs
33 | .map(config => connectSession(config).global$
34 | .pipe(shareReplay(1))
35 | )
36 | );
37 |
38 | // The current session
39 | const sesh$ = combineLatest(sessions$, sessionNo$).pipe(
40 | map(([engines, no]) => engines[no]),
41 | shareReplay(1)
42 | );
43 |
44 | // When switching sessions, get the doc
45 | const app$ = sesh$.pipe(
46 | withLatestFrom(sessionNo$),
47 | switchMap(([h, no]) => h.ask(OpenDoc, configs[no].appname)),
48 | shareReplay(1)
49 | );
50 |
51 | // Get the current app title
52 | const appTitle$ = app$.pipe(
53 | switchMap(h => h.ask(GetAppProperties)),
54 | pluck("qTitle")
55 | );
56 |
57 | // Print the app title to the DOM
58 | appTitle$.subscribe(title => {
59 | document.querySelector("#app-title").innerHTML = title;
60 | });
61 |
62 | // Get the fields in the current app
63 | const fieldList$ = app$.pipe(
64 | switchMap(h => h.ask(GetTablesAndKeys, { "qcx": 1000, "qcy": 1000 }, { "qcx": 0, "qcy": 0 }, 30, true, false))
65 | );
66 |
67 | // List the fields in the DOM
68 | fieldList$.subscribe(response => {
69 | const tables = response.qtr;
70 | const flds = tables.map(table => {
71 | const tableflds = table.qFields;
72 | return tableflds.map(fld => ({
73 | table: table.qName,
74 | field: fld.qName
75 | }));
76 | })
77 | .reduce((acc, curr) => {
78 | return acc.concat(curr);
79 | }, []);
80 |
81 | document.querySelector("tbody").innerHTML = flds.map(fld => `| ${fld.table} | ${fld.field} |
`).join("");
82 | });
83 | ```
--------------------------------------------------------------------------------
/docs/recipes/loop-selections.md:
--------------------------------------------------------------------------------
1 | # Loop Through Selections in a Field
2 | [Code Sandbox](https://codesandbox.io/embed/nwwrm598r0)
3 | ```javascript
4 | import { connectSession } from "rxq";
5 | import { OpenDoc } from "rxq/Global";
6 | import { CreateSessionObject, GetField } from "rxq/Doc";
7 | import { GetLayout } from "rxq/GenericObject";
8 | import { GetCardinal, LowLevelSelect } from "rxq/Field";
9 | import { map, publish, repeat, shareReplay, startWith, switchMap, take, withLatestFrom } from "rxjs/operators";
10 | import { interval } from "rxjs";
11 |
12 | const appname = "aae16724-dfd9-478b-b401-0d8038793adf"
13 |
14 | // Define the configuration for your session
15 | const config = {
16 | host: "sense.axisgroup.com",
17 | isSecure: true,
18 | appname
19 | };
20 |
21 | // Connect the session and share the Global handle
22 | const session = connectSession(config);
23 | const global$ = session.global$;
24 |
25 |
26 | // Open an app and share the app handle
27 | const app$ = global$.pipe(
28 | switchMap(h => h.ask(OpenDoc, appname)),
29 | shareReplay(1)
30 | );
31 |
32 | // Create a Generic Object with the current selections and a metric
33 | const obj$ = app$.pipe(
34 | switchMap(h => h.ask(CreateSessionObject, {
35 | "qInfo": {
36 | "qType": "my-object"
37 | },
38 | "qSelectionObjectDef": {},
39 | "myValue": {
40 | "qValueExpression": "=avg(petal_length)"
41 | }
42 | })),
43 | shareReplay(1)
44 | );
45 |
46 | // Get the latest selections whenever the model changes
47 | const selections$ = obj$.pipe(
48 | switchMap(h => h.invalidated$.pipe(startWith(h))),
49 | switchMap(h => h.ask(GetLayout))
50 | );
51 |
52 | // Print the selections to the DOM
53 | selections$.subscribe(layout => {
54 | const content = document.querySelector("#content");
55 | content.innerHTML = layout.qSelectionObject.qSelections.map(sel => `${sel.qField}: ${sel.qSelected}`).join("")
56 | content.innerHTML += `
The average petal length is ${layout.myValue}`;
57 | });
58 |
59 | // Get a field
60 | const fld$ = app$.pipe(
61 | switchMap(h => h.ask(GetField, "species")),
62 | shareReplay(1)
63 | );
64 |
65 | // Create a loop of selections by getting the cardinality of the field,
66 | // then running a repeating interval for that cardinality and passing to
67 | // a lowLevelSelect call on the field handle
68 | const selectionLoop$ = fld$.pipe(
69 | switchMap(h => h.ask(GetCardinal), (h, cnt) => ([h, cnt])),
70 | switchMap(([h, cnt]) => interval(1500).pipe(
71 | take(cnt),
72 | map(i => [h, i]),
73 | repeat()
74 | )),
75 | switchMap(([h, i]) => h.ask(LowLevelSelect, [i], false)),
76 | publish()
77 | );
78 |
79 | // Connect the selection loop
80 | selectionLoop$.connect();
81 | ```
--------------------------------------------------------------------------------
/test/e2e/global.test.js:
--------------------------------------------------------------------------------
1 | const chai = require("chai");
2 | chai.use(require("chai-generator"));
3 | const expect = chai.expect;
4 |
5 | var createContainer = require("../util/create-container");
6 | var { publishReplay, refCount, switchMap } = require("rxjs/operators");
7 |
8 | var { EngineVersion, OpenDoc } = require("../../dist/global");
9 | var { connectSession } = require("../../dist");
10 | var Handle = require("../../dist/_cjs/handle");
11 |
12 | var { port, image } = require("./config.json");
13 |
14 | // launch a new container
15 | var container$ = createContainer(image, port);
16 |
17 | var eng$ = container$.pipe(
18 | switchMap(() => {
19 | return connectSession({
20 | host: "localhost",
21 | port: port,
22 | isSecure: false
23 | }).global$;
24 | }),
25 | publishReplay(1),
26 | refCount()
27 | );
28 |
29 | function testGlobal() {
30 | describe("Global Class", function() {
31 | before(function(done) {
32 | this.timeout(10000);
33 | container$.subscribe(() => done());
34 | });
35 |
36 | describe("engineVersion", function() {
37 | const ev$ = eng$.pipe(
38 | switchMap(handle => handle.ask(EngineVersion)),
39 | publishReplay(1),
40 | refCount()
41 | );
42 |
43 | it("should return an object with prop 'qComponentVersion'", function(done) {
44 | ev$.subscribe(ev => {
45 | expect(ev).to.have.property("qComponentVersion");
46 | done();
47 | });
48 | });
49 |
50 | describe("qComponentVersion", function() {
51 | it("should be a string", function(done) {
52 | ev$.subscribe(ev => {
53 | expect(ev.qComponentVersion).to.be.a("string");
54 | done();
55 | });
56 | });
57 | });
58 | });
59 |
60 | describe("openDoc", function() {
61 | const app$ = eng$.pipe(
62 | switchMap(handle => handle.ask(OpenDoc, "iris.qvf")),
63 | publishReplay(1),
64 | refCount()
65 | );
66 |
67 | it("should return a Handle", function(done) {
68 | app$.subscribe(appH => {
69 | expect(appH).to.be.instanceof(Handle);
70 | done();
71 | });
72 | });
73 |
74 | describe("Returned Handle", function() {
75 | it("should have qClass property of 'Doc'", function(done) {
76 | app$.subscribe(h => {
77 | expect(h.qClass).to.equal("Doc");
78 | done();
79 | });
80 | });
81 | });
82 | });
83 |
84 | after(function(done) {
85 | container$.subscribe(container =>
86 | container.kill((err, result) => {
87 | container.remove();
88 | done();
89 | })
90 | );
91 | });
92 | });
93 | }
94 |
95 | module.exports = testGlobal;
96 |
--------------------------------------------------------------------------------
/docs/recipes/get-pages-in-sequence.md:
--------------------------------------------------------------------------------
1 | # Get HyperCube Pages in Sequence
2 | [Code Sandbox](https://codesandbox.io/embed/0yzo8ykl2n)
3 | ```javascript
4 | import { connectSession } from "rxq";
5 | import { OpenDoc } from "rxq/Global";
6 | import { CreateSessionObject } from "rxq/Doc";
7 | import { GetLayout, GetHyperCubeData } from "rxq/GenericObject";
8 | import { reduce, shareReplay, startWith, switchMap } from "rxjs/operators";
9 | import { concat } from "rxjs";
10 |
11 | const appname = "aae16724-dfd9-478b-b401-0d8038793adf"
12 |
13 | // Define the configuration for your session
14 | const config = {
15 | host: "sense.axisgroup.com",
16 | isSecure: true,
17 | appname
18 | };
19 |
20 | // Connect the session and share the Global handle
21 | const session = connectSession(config);
22 | const global$ = session.global$;
23 |
24 | // Open an app and share the app handle
25 | const app$ = global$.pipe(
26 | switchMap(h => h.ask(OpenDoc, appname)),
27 | shareReplay(1)
28 | );
29 |
30 | // Create a Generic Object with a hypercube
31 | const obj$ = app$.pipe(
32 | switchMap(h => h.ask(CreateSessionObject, {
33 | "qInfo": {
34 | "qType": "my-object"
35 | },
36 | "qHyperCubeDef": {
37 | "qDimensions": [
38 | {
39 | "qDef": {
40 | "qFieldDefs": ["petal_length"]
41 | }
42 | }
43 | ],
44 | "qMeasures": [
45 | {
46 | "qDef": {
47 | "qDef": "=avg(petal_width)"
48 | }
49 | }
50 | ]
51 | }
52 | })),
53 | shareReplay(1)
54 | );
55 |
56 | // On invalidation, get layout to validate, then request multiple pages in sequence
57 | const data$ = obj$.pipe(
58 | switchMap(h => h.invalidated$.pipe(startWith(h))),
59 | switchMap(h => h.ask(GetLayout), (h, layout) => [h, layout]),
60 | switchMap(([h, layout]) => {
61 | const totalRows = layout.qHyperCube.qSize.qcy;
62 | const rowsPerPage = 10;
63 | const pageCt = Math.ceil(totalRows / rowsPerPage);
64 |
65 | const pageRequests = new Array(pageCt)
66 | .fill(undefined)
67 | .map((m, i) => h.ask(GetHyperCubeData, "/qHyperCubeDef", [
68 | {
69 | qTop: i * rowsPerPage,
70 | qLeft: 0,
71 | qWidth: 2,
72 | qHeight: rowsPerPage
73 | }
74 | ]));
75 |
76 | return concat(...pageRequests).pipe(
77 | reduce((acc, curr) => acc.concat(curr))
78 | );
79 | })
80 | );
81 |
82 | // Print the pages to the DOM in a table
83 | data$.subscribe(pages => {
84 | const data = pages.reduce((acc, page) => acc.concat(page.qMatrix),[]);
85 |
86 | document.querySelector("tbody").innerHTML = data.map(row => {
87 | return `
88 | | ${row[0].qText} |
89 | ${row[1].qText} |
90 |
`
91 | }).join("");
92 | });
93 |
94 | ```
--------------------------------------------------------------------------------
/docs/recipes/get-pages-in-parallel.md:
--------------------------------------------------------------------------------
1 | # Get HyperCube Data Pages in Parallel
2 | [Code Sandbox](https://codesandbox.io/embed/1r55yyy9nq)
3 | ```javascript
4 | import { connectSession } from "rxq";
5 | import { OpenDoc } from "rxq/Global";
6 | import { CreateSessionObject } from "rxq/Doc";
7 | import { GetLayout, GetHyperCubeData } from "rxq/GenericObject";
8 | import { reduce, shareReplay, startWith, switchMap } from "rxjs/operators";
9 | import { merge } from "rxjs";
10 |
11 | const appname = "aae16724-dfd9-478b-b401-0d8038793adf"
12 |
13 | // Define the configuration for your session
14 | const config = {
15 | host: "sense.axisgroup.com",
16 | isSecure: true,
17 | appname
18 | };
19 |
20 | // Connect the session and share the Global handle
21 | const session = connectSession(config);
22 | const global$ = session.global$;
23 |
24 | // Open an app and share the app handle
25 | const app$ = global$.pipe(
26 | switchMap(h => h.ask(OpenDoc, appname)),
27 | shareReplay(1)
28 | );
29 |
30 | // Create a Generic Object with a hypercube
31 | const obj$ = app$.pipe(
32 | switchMap(h => h.ask(CreateSessionObject, {
33 | "qInfo": {
34 | "qType": "my-object"
35 | },
36 | "qHyperCubeDef": {
37 | "qDimensions": [
38 | {
39 | "qDef": {
40 | "qFieldDefs": ["petal_length"]
41 | }
42 | }
43 | ],
44 | "qMeasures": [
45 | {
46 | "qDef": {
47 | "qDef": "=avg(petal_width)"
48 | }
49 | }
50 | ]
51 | }
52 | })),
53 | shareReplay(1)
54 | );
55 |
56 | // On invalidation, get layout to validate, then request multiple pages in parallel
57 | const data$ = obj$.pipe(
58 | switchMap(h => h.invalidated$.pipe(startWith(h))),
59 | switchMap(h => h.ask(GetLayout), (h, layout) => [h, layout]),
60 | switchMap(([h, layout]) => {
61 | const totalRows = layout.qHyperCube.qSize.qcy;
62 | const rowsPerPage = 10;
63 | const pageCt = Math.ceil(totalRows / rowsPerPage);
64 |
65 | const pageRequests = new Array(pageCt)
66 | .fill(undefined)
67 | .map((m, i) => h.ask(GetHyperCubeData, "/qHyperCubeDef", [
68 | {
69 | qTop: i * rowsPerPage,
70 | qLeft: 0,
71 | qWidth: 2,
72 | qHeight: rowsPerPage
73 | }
74 | ]));
75 |
76 | return merge(...pageRequests).pipe(
77 | reduce((acc, curr) => acc.concat(curr))
78 | );
79 | })
80 | );
81 |
82 | // Print the pages to the DOM in a table
83 | data$.subscribe(pages => {
84 | const data = pages.reduce((acc, page) => acc.concat(page.qMatrix),[]);
85 |
86 | document.querySelector("tbody").innerHTML = data.map(row => {
87 | return `
88 | | ${row[0].qText} |
89 | ${row[1].qText} |
90 |
`
91 | }).join("");
92 | });
93 |
94 | ```
--------------------------------------------------------------------------------
/docs/basics/delta.md:
--------------------------------------------------------------------------------
1 | # Delta Mode
2 |
3 | ## What is Delta Mode
4 | Qlik's Engine API has the ability to provide responses in what is known as "delta mode". In delta mode, rather than sending the entire API response back for a call, the Engine will send a list of changes to apply from a previous API call instead. For example, imagine you have received the layout of an app from the Engine in a JSON structure that includes properties like `lastReloadTime`:
5 | ```json
6 | {
7 | qId: "app-id",
8 | title: "My App",
9 | // ...other props
10 | lastReloadTime: "date"
11 | }
12 | ```
13 |
14 | Now imagine the app is reloaded. This app layout is no longer valid, so you make an API request for another layout. Normally, you would receive the entire object back again, which would included an updated reload time:
15 | ```json
16 | {
17 | qId: "app-id",
18 | title: "My App",
19 | //...other props
20 | lastReloadTime: "new date"
21 | }
22 | ```
23 |
24 | In this scenario, only one property actually changes from the first layout call to the second: `lastReloadTime`. Therefore, it's redundant to send all of the other info again. Enter delta mode.
25 |
26 | In delta mode, the Engine only sends instructions back on how to update the previously seen value. In this case, it would send a message indicating that the "lastReloadTime" property has been updated, along with its new value. These instructions are similar to the JSON-Patch specification.
27 |
28 | The benefit of using delta mode is that less information will be sent over the network as an app is used, leading to faster API calls. On the other hand, delta mode requires more work on the client to process the changes and keep track of previous values. Realistically, this is a trade-off you will probably not notice in your typical application built with the Engine.
29 |
30 | ## Usage in RxQ
31 | By default, delta mode is not used in RxQ. The library provides two mechanisms for opting into delta mode:
32 | 1. **Session-level**: Delta mode can be enabled across the session for every API call. This is done by setting the session config `delta` property to true:
33 | ```javascript
34 | const session = connectSession({
35 | host: "localhost",
36 | port: 9076,
37 | delta: true
38 | })
39 | ```
40 |
41 | 2. **Method-level**: Rather than opting the entire session into delta mode, you can also selectively pick which methods that you want to leverage delta mode by providing an object with the Qlik class as a key and the method names that should use delta mode. For example, to enable delta mode for GenericObject `GetLayout` and `GetProperties` calls, you could configure your session like so:
42 | ```javascript
43 | const session = connectSession({
44 | host: "localhost",
45 | port: 9076,
46 | delta: {
47 | GenericObject: ["GetLayout", "GetProperties"]
48 | }
49 | })
50 | ```
--------------------------------------------------------------------------------
/docs/basics/handle-invalidations.md:
--------------------------------------------------------------------------------
1 | # Handle Invalidations
2 | In QAE, Handles can have properties or calculated values that change while the session is open. This usually happens as a result of API calls - maybe a filter is applied that changes the calculated data you have from a GenericObject, or maybe you update some metadata in your App properties.
3 |
4 | QAE categorizes Handles into two states: valid and invalid. A valid Handle is one in which you have the latest properties or data for it. An invalid Handle is one in which you don't have the latest properties for it – the Handle has changed on the server since you last used it, but you have not pulled the latest information to your app. There is a disconnect between server state and client state; hence, your Handle is invalid.
5 |
6 | Whenever an operation occurs that causes a Handle to go from valid to invalid, the Engine notifies us via the WebSocket connection so that we can respond if needed. A really common use case for this is getting data from a GenericObject. Let's say we have the following stream that gives us data from QAE:
7 |
8 | ```javascript
9 | import { CreateSessionObject } from "rxq/Doc";
10 | import { GetLayout } from "rxq/GenericObject";
11 | import { shareReplay, switchMap } from "rxjs/operators";
12 |
13 | const app$; // assume we have a Doc Handle that we've opened
14 |
15 | const obj$ = app$.pipe(
16 | switchMap(h => h.ask(CreateSessionObject, {
17 | qInfo: {
18 | qType: "session"
19 | },
20 | myValue: {
21 | qValueExpression: "=sum(Sales)"
22 | }
23 | })),
24 | shareReplay(1)
25 | );
26 |
27 | const layout$ = obj$.pipe(
28 | switchMap(h => h.ask(GetLayout))
29 | );
30 |
31 | layout$.subscribe(console.log);
32 | ```
33 |
34 | The code above will calculate the layout of our GenericObject once and print it to the console. What if the data model state changes? Suppose we apply a selection that causes our `sum(Sales)` expression to change in value. The layout we received previously will be out of sync with the server. The GenericObject will be invalid.
35 |
36 | To resolve this situation, we need to call `getLayout` again. This will get us the up to date data and validate the GenericObject.
37 |
38 | In RxQ, each Handle comes with an `invalidation$` stream on it. This stream will fire with the Handle any time that the Engine API tells us that the Handle has invalidated. Using this stream, we can rewrite our layout logic above to produce a stream of layouts that will be calculated on every invalidation. We will also add an initial layout call to the stream:
39 | ```javascript
40 | const layouts$ = obj$.pipe(
41 | switchMap(h => h.invalidated$.pipe(
42 | startWith(h)
43 | )),
44 | switchMap(h => h.ask(GetLayout))
45 | );
46 | ```
47 |
48 | Now our `layouts$` stream will stay in sync with the server.
--------------------------------------------------------------------------------
/test/e2e/connectSession.test.js:
--------------------------------------------------------------------------------
1 | const chai = require("chai");
2 | chai.use(require("chai-generator"));
3 | const expect = chai.expect;
4 |
5 | var createContainer = require("../util/create-container");
6 | var {
7 | publishReplay,
8 | refCount,
9 | switchMap,
10 | map,
11 | take
12 | } = require("rxjs/operators");
13 | var { connectSession } = require("../../dist");
14 | var Handle = require("../../dist/_cjs/handle");
15 |
16 | var { port, image } = require("./config.json");
17 |
18 | // launch a new container
19 | var container$ = createContainer(image, port);
20 |
21 | var session$ = container$.pipe(
22 | map(() => {
23 | return connectSession({
24 | host: "localhost",
25 | port: port,
26 | isSecure: false
27 | });
28 | }),
29 | publishReplay(1),
30 | refCount()
31 | );
32 |
33 | const eng$ = session$.pipe(switchMap(session => session.global$));
34 | const notifications$ = session$.pipe(
35 | switchMap(session => session.notifications$)
36 | );
37 |
38 | function testConnect() {
39 | describe("Connect to an engine", function() {
40 | before(function(done) {
41 | this.timeout(10000);
42 | container$.subscribe(() => done());
43 | });
44 |
45 | it("should return a Handle", function(done) {
46 | var eng$ = container$.pipe(
47 | switchMap(() => {
48 | return connectSession({
49 | host: "localhost",
50 | port: port,
51 | isSecure: false
52 | }).global$;
53 | }),
54 | publishReplay(1),
55 | refCount()
56 | );
57 |
58 | eng$.subscribe(h => {
59 | expect(h).to.be.instanceof(Handle);
60 | done();
61 | });
62 | });
63 |
64 | it("should return a Handle when given a URL to connect with", function(done) {
65 | container$
66 | .pipe(
67 | map(() => {
68 | return connectSession({
69 | url: `ws://localhost:${port}/app`
70 | });
71 | }),
72 | switchMap(session => session.global$),
73 | take(1)
74 | )
75 | .subscribe(h => {
76 | expect(h).to.be.instanceof(Handle);
77 | done();
78 | });
79 | });
80 |
81 | describe("Returned Handle", function() {
82 | it("should have qClass property of 'Global'", function(done) {
83 | eng$.subscribe(h => {
84 | expect(h.qClass).to.equal("Global");
85 | done();
86 | });
87 | });
88 | });
89 |
90 | after(function(done) {
91 | container$.subscribe(container =>
92 | container.kill((err, result) => {
93 | container.remove();
94 | done();
95 | })
96 | );
97 | });
98 | });
99 | }
100 |
101 | module.exports = testConnect;
102 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rxq",
3 | "version": "2.0.4",
4 | "sideEffects": false,
5 | "description": "A reactive wrapper for the Qlik Analytics Platform APIs",
6 | "main": "index.js",
7 | "qix-version": "12.329.0",
8 | "scripts": {
9 | "build-qix-methods": "node scripts/build-qix-methods.js",
10 | "compile-cjs": "node scripts/compile_cjs.js",
11 | "compile-esm5": "node scripts/compile_esm5.js",
12 | "compile-esm2015": "node scripts/compile_esm2015.js",
13 | "compile-all": "npm run compile-cjs && npm run compile-esm5 && npm run compile-esm2015",
14 | "make-packages": "node scripts/make-packages.js",
15 | "build": "node scripts/build-browser",
16 | "build-min": "node scripts/build-browser --min",
17 | "build-dist": "npm run compile-all && npm run make-packages && npm run build && npm run build-min",
18 | "build-release": "npm run build-dist && cp -R dist/build/ rxq-build && zip rxq-build.zip rxq-build/* && rm -rf rxq-build/",
19 | "test-unit": "mocha test/unit/*.spec.js",
20 | "test-e2e": "mocha test/e2e/*.spec.js",
21 | "test": "npm run test-unit && npm run test-e2e",
22 | "test-compile": "mocha --compilers js:babel-core/register test/unit/*-spec.js",
23 | "docs-build": "gitbook build",
24 | "docs-serve": "gitbook serve"
25 | },
26 | "repository": {
27 | "type": "git",
28 | "url": "git+https://github.com/axisgroup/RxQ.git"
29 | },
30 | "keywords": [
31 | "qap",
32 | "qix",
33 | "engine",
34 | "engineAPI",
35 | "qrs"
36 | ],
37 | "author": "Axis Group",
38 | "license": "ISC",
39 | "homepage": "https://github.com/axisgroup/RxQ#readme",
40 | "devDependencies": {
41 | "babel-core": "^6.26.3",
42 | "babel-loader": "^7.1.4",
43 | "babel-plugin-add-module-exports": "^0.2.1",
44 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
45 | "babel-plugin-transform-object-rest-spread": "^6.26.0",
46 | "babel-plugin-transform-runtime": "^6.15.0",
47 | "babel-polyfill": "^6.16.0",
48 | "babel-preset-env": "^1.7.0",
49 | "babel-preset-es2015": "^6.16.0",
50 | "babel-regenerator-runtime": "^6.5.0",
51 | "babel-register": "^6.26.0",
52 | "chai": "^4.1.2",
53 | "chai-generator": "^2.1.0",
54 | "chai-spies": "^1.0.0",
55 | "dockerode": "^2.5.3",
56 | "fs-extra": "^4.0.2",
57 | "gitbook-cli": "^2.3.2",
58 | "mocha": "^4.0.1",
59 | "mock-socket": "^7.0.0",
60 | "raw-loader": "^0.5.1",
61 | "rimraf": "^2.6.2",
62 | "rxjs": "^6.2.0",
63 | "string-replace-webpack-plugin": "^0.1.3",
64 | "webpack": "^4.10.2"
65 | },
66 | "dependencies": {
67 | "babel-runtime": "^6.18.0",
68 | "https-browserify": "0.0.1",
69 | "immer": "^2.1.1",
70 | "stream-http": "~2.0.1",
71 | "ws": "^1.1.1"
72 | },
73 | "peerDependencies": {
74 | "rxjs": "^6.2.0"
75 | },
76 | "bugs": {
77 | "url": "https://github.com/axisgroup/RxQ/issues"
78 | },
79 | "browser": {
80 | "ws": false
81 | },
82 | "directories": {
83 | "example": "examples"
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/docs/introduction/why-rx.md:
--------------------------------------------------------------------------------
1 | # Why Rx?
2 | [Reactive programming](https://en.wikipedia.org/wiki/Reactive_programming) is a declarative style of programming that enables developers to define variables as entities that change over time, with their behavior and interdependencies clearly defined. This approach is best represented in the following pseudocode examples:
3 |
4 | *Non-reactive*
5 | ```
6 | a = 1;
7 | b = a + 1;
8 | a = 2;
9 |
10 | print a; // -> 2
11 | print b; // -> 2
12 | ```
13 |
14 | *Reactive*
15 | ```
16 | a = 1;
17 | b = a + 1;
18 | a = 2;
19 |
20 | print a; // -> 2
21 | print b; // -> 3
22 | ```
23 |
24 | In the reactive example, the variable `b` is declared as depending on `a`, so when `a` changes, `b` necessarily changes.
25 |
26 | Because of this feature, reactive programming is useful in highly interactive interfaces, especially when complex relationships exist between various variables. It also lends itself to asynchronous operations, where time delays affect when and how variables in a program change. When used properly, Rx enables scalable and maintainable code for complex, dynamic applications.
27 |
28 | Let's take a simple example. [Say you want to create a component that can be dragged and dropped around the screen.](https://codesandbox.io/embed/9ol0rvokpo) The Rx code for this is concise, easy to read, and easy to modify:
29 | ```javascript
30 | import { fromEvent } from "rxjs/observable/fromEvent";
31 | import { switchMap, takeUntil } from "rxjs/operators";
32 |
33 | const box = document.querySelector("#box");
34 |
35 | const mousedown$ = fromEvent(box, "mousedown");
36 | const mouseup$ = fromEvent(document, "mouseup");
37 | const mousemove$ = fromEvent(document, "mousemove");
38 |
39 | const move$ = mousedown$.pipe(
40 | switchMap(() => mousemove$.pipe(
41 | takeUntil(mouseup$)
42 | ))
43 | );
44 |
45 | move$.subscribe(evt => {
46 | const top = parseInt(box.style.top);
47 | const left = parseInt(box.style.left);
48 | box.style.top = `${top + evt.movementY}px`;
49 | box.style.left = `${left + evt.movementX}px`;
50 | });
51 | ```
52 |
53 | For more on Rx, we highly recommend this guide to get started: [The introduction to Reactive Programming you've been missing](https://gist.github.com/staltz/868e7e9bc2a7b8c1f754), by [André Staltz](https://gist.github.com/staltz).
54 |
55 | Or if you're looking for a quicker summary, try this [shorter read](https://branch-blog.qlik.com/what-is-reactive-programming-a1e82cf28575).
56 |
57 | ## Rx & Qlik
58 | Because Rx works so well with dynamic applications, it pairs well with Qlik and it's engine. At its core, the Qlik Associative Engine (QAE) can be thought of as a reactive interface. It models data into a **state** that is modified by interactions like filtering data, causing the state of the model to update and any existing calculations to be recalculated based on this new state. In other words, the data model state in the engine is a dynamic entity that can change over time. This fits perfectly with Rx's strengths!
59 |
60 | RxQ bridges the gap between RxJS and QAE, enabling developers to leverage reactive programming when working with Qlik.
--------------------------------------------------------------------------------
/docs/recipes/make-lb-selections.md:
--------------------------------------------------------------------------------
1 | # Make Selections in a Listbox
2 | [Code Sandbox](https://codesandbox.io/embed/3kj6nw46q)
3 | ```javascript
4 | import { connectSession } from "rxq";
5 | import { OpenDoc } from "rxq/Global";
6 | import { CreateSessionObject } from "rxq/Doc";
7 | import { GetLayout, SelectListObjectValues } from "rxq/GenericObject";
8 | import { filter, map, publish, repeat, shareReplay, startWith, switchMap, take, withLatestFrom } from "rxjs/operators";
9 | import { fromEvent } from "rxjs";
10 |
11 | const appname = "aae16724-dfd9-478b-b401-0d8038793adf"
12 |
13 | // Define the configuration for your session
14 | const config = {
15 | host: "sense.axisgroup.com",
16 | isSecure: true,
17 | appname
18 | };
19 |
20 | // Connect the session and share the Global handle
21 | const session = connectSession(config);
22 | const global$ = session.global$;
23 |
24 | // Open an app and share the app handle
25 | const app$ = global$.pipe(
26 | switchMap(h => h.ask(OpenDoc, appname)),
27 | shareReplay(1)
28 | );
29 |
30 | // Create a Generic Object with a metric
31 | const obj$ = app$.pipe(
32 | switchMap(h => h.ask(CreateSessionObject, {
33 | "qInfo": {
34 | "qType": "my-object"
35 | },
36 | "myValue": {
37 | "qValueExpression": "=avg(petal_length)"
38 | }
39 | })),
40 | shareReplay(1)
41 | );
42 |
43 | // Get the latest selections whenever the model changes
44 | const metricLayouts$ = obj$.pipe(
45 | switchMap(h => h.invalidated$.pipe(startWith(h))),
46 | switchMap(h => h.ask(GetLayout))
47 | );
48 |
49 | // Print the selections to the DOM
50 | metricLayouts$.subscribe(layout => {
51 | document.querySelector("#metric").innerHTML = `
The average petal length is ${layout.myValue}`;
52 | });
53 |
54 | // Create a Generic Object with a list object for the field "species"
55 | const lb$ = app$.pipe(
56 | switchMap(h => h.ask(CreateSessionObject, {
57 | "qInfo": {
58 | "qType": "my-listbox"
59 | },
60 | "qListObjectDef": {
61 | "qDef": {
62 | "qFieldDefs": ["species"]
63 | },
64 | "qInitialDataFetch": [
65 | {
66 | "qTop": 0,
67 | "qLeft": 0,
68 | "qWidth": 1,
69 | "qHeight": 100
70 | }
71 | ]
72 | }
73 | })),
74 | shareReplay(1)
75 | );
76 |
77 | // Get a stream of list object layouts
78 | const lbLayouts$ = lb$.pipe(
79 | switchMap(h => h.invalidated$.pipe(startWith(h))),
80 | switchMap(h => h.ask(GetLayout))
81 | );
82 |
83 | // Render the list object to the page in an unordered list
84 | lbLayouts$.subscribe(layout => {
85 | const data = layout.qListObject.qDataPages[0].qMatrix;
86 | document.querySelector("ul").innerHTML = data.map(item => `
87 | ${item[0].qText}
88 | `).join("");
89 | });
90 |
91 | // Select values when a user clicks on them
92 | const select$ = fromEvent(document.querySelector("body"), "click").pipe(
93 | filter(evt => evt.target.hasAttribute("data-qno")),
94 | map(evt => parseInt(evt.target.getAttribute("data-qno"))),
95 | withLatestFrom(lb$),
96 | switchMap(([qno, h]) => h.ask(SelectListObjectValues, "/qListObjectDef", [qno], true)),
97 | publish()
98 | );
99 |
100 | select$.connect();
101 | ```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # RxQ
2 | **RxQ** is a JavaScript library that provides utilities to link [**RxJS**](https://github.com/ReactiveX/rxjs), a JavaScript implementation of Rx, with the Qlik Associative Engine (QAE). RxQ can be used to build complex, interactive solutions on top of QAE using the reactive programming paradigm. RxQ can be used in projects via npm like so:
3 | ```
4 | $ npm install rxq
5 | ```
6 |
7 | *Note that **RxJS ^6.2.0** is a peer dependency of **RxQ**; It will not be automatically installed when you install RxQ. If you do not already have **RxJS ^6.2.0** installed to your project, be sure to include it by running:*
8 | ```
9 | $ npm install rxjs
10 | ```
11 |
12 | ## Usage and Documentation
13 | Documentation for RxQ is hosted on [http://opensrc.axisgroup.com/rxq/docs](http://opensrc.axisgroup.com/rxq/docs/). We highly recommend reviewing the list of Recipes for examples of usage.
14 |
15 | ## Quick Start
16 | Want to play with it immediately? [Try forking this sandbox.](https://codesandbox.io/embed/k3mvn2k815)
17 |
18 | ## Qlik Support
19 | As of v2.0.2, the following APIs are supported:
20 | - Engine 12.260.0
21 |
22 | Custom builds for other versions of the Qlik Associative Engine can be generated using the included build scripts. See the build section.
23 |
24 |
25 | ## Building RxQ
26 | RxQ has several auto-generated components that build the source code and compile it into the distributed package for NPM. It leverages Qlik Engine v12.181.0 to generate method names. The steps are:
27 | 1) Getting the correct QIX Engine schemas and generating operators for all API methods for the desired engine version
28 | 2) Converting all source code into distribution modules and move them to the distribution folder
29 | 3) Creating the package.json files for the distribution folder
30 |
31 | Each of these steps can be triggered using npm scripts in the repository:
32 |
33 | ### Step 1: Getting Engine schemas and generating operators
34 | `npm run build-qix-methods` uses Qlik Core to spin up an Engine and pull the API schema that will be used to generate enums for all of the API methods.
35 |
36 | ### Step 2: Converting all source code into distribution modules
37 | `npm run compile-cjs` compiles the CommonJS modules.
38 |
39 | `npm run compile-esm5` compiles the ESM5 modules.
40 |
41 | `npm run compile-esm` compiles the ESM modules.
42 |
43 | `npm run build` compiles the browser bundle.
44 |
45 | `npm run build-min` compiles the minified browser bundle.
46 |
47 | ### Step 3: Creating the package.json files for distribution
48 | `npm run make-packages` creates and stores the package.json files.
49 |
50 | ### Rebuilding the Distribution Folder
51 | It is common to edit source code of RxQ and then execute steps 2 and 3 to rebuild the distribution folder. Those steps can be done in a single command with:
52 | `npm run build-dist`.
53 |
54 | The final package for distribution is stored in a sub-directory called `dist`. The NPM package should be published from this directory, NOT from the parent level repository which contains the source code.
55 |
56 | ## Testing RxQ
57 | `npm run test-unit` will run the unit tests.
58 |
59 | `npm run test-e2e` will run the end to end tests. These tests require Docker and the Qlik Core image associated with the engine version in package.json. For example, for version 12.207.0 of the Engine, the tests need the `qlikcore/engine:12.207.0` image. This image can be pulled from Docker like so: `docker pull qlikcore/engine:12.207.0`.
60 |
--------------------------------------------------------------------------------
/docs/basics/reusing-handles.md:
--------------------------------------------------------------------------------
1 | # Reusing Handles
2 | It is common to make multiple API calls on a single Handle. Hence, we usually save off Handles in their own variable that can be reused. [However, Observables in RxJS are cold by default – they create a new producer for each subscriber](https://medium.com/@benlesh/hot-vs-cold-observables-f8094ed53339). In RxQ land, that means that each subscriber to an API call will cause it to run.
3 |
4 | Let's illustrate with an example. Assume you want to open an app and use the Doc Class to get both the app properties and the app layout. You write the following:
5 | ```javascript
6 | import { connectSession } from "rxq";
7 | import { EngineVersion, OpenDoc } from "rxq/Global";
8 | import { GetAppProperties, GetAppLayout } from "rxq/Doc";
9 | import { switchMap } from "rxjs/operators";
10 |
11 | const session = connectSession({
12 | host: "localhost",
13 | port: 4848,
14 | isSecure: false
15 | });
16 |
17 | const doc$ = session.global$.pipe(
18 | switchMap(handle => handle.ask(OpenDoc))
19 | );
20 |
21 | const appProps$ = doc$.pipe(
22 | switchMap(handle => handle.ask(GetAppProperties))
23 | );
24 |
25 | const appLayout$ = doc$.pipe(
26 | switchMap(handle = handle.ask(GetAppLayout))
27 | );
28 |
29 | appProps$.subscribe(version => {
30 | console.log(version);
31 | });
32 |
33 | appLayout$.subscribe(doclist => {
34 | console.log(doclist);
35 | });
36 | ```
37 |
38 | In the code above, we execute two API calls leveraging the `doc$` Observable. This Observable is getting two subscribers via the `appProps$` and `appLayout$` subscriptions. **Therefore, this will execute the `doc$` Observable twice, running the OpenDoc command twice!** This is rarely what we want when working with QAE, especially when creating new Generic Objects. Instead, we want to share the Handle across subscribers. This can be done by multicasting the Handle Observable.
39 |
40 | [There are many ways to multicast Observables in RxJS.](https://blog.angularindepth.com/rxjs-understanding-the-publish-and-share-operators-16ea2f446635) For RxQ usage, we often find the operator `shareReplay(1)` does the trick for us. This operator:
41 | * will create the Observable producer when going from 0 to 1 observers
42 | * will share the latest seen value with any late subscribers
43 |
44 | In most RxQ examples, you will see reused handles written like so:
45 | ```javascript
46 | doc$.pipe(
47 | shareReplay(1)
48 | );
49 | ```
50 |
51 | Our example from before becomes:
52 | ```javascript
53 | import { connectSession } from "rxq";
54 | import { EngineVersion, OpenDoc } from "rxq/Global";
55 | import { GetAppProperties, GetAppLayout } from "rxq/Doc";
56 | import { switchMap, shareReplay } from "rxjs/operators";
57 |
58 | const session = connectSession({
59 | host: "localhost",
60 | port: 4848,
61 | isSecure: false
62 | });
63 |
64 | const doc$ = session.global$.pipe(
65 | switchMap(handle => handle.ask(OpenDoc)),
66 | shareReplay(1)
67 | );
68 |
69 | const appProps$ = doc$.pipe(
70 | switchMap(handle => handle.ask(GetAppProperties))
71 | );
72 |
73 | const appLayout$ = doc$.pipe(
74 | switchMap(handle = handle.ask(GetAppLayout))
75 | );
76 |
77 | appProps$.subscribe(version => {
78 | console.log(version);
79 | });
80 |
81 | appLayout$.subscribe(doclist => {
82 | console.log(doclist);
83 | });
84 | ```
85 |
86 | Now, our `appProps$` and `appLayout$` Observables will share the Global Handle produced by `doc$`. We should multicast any responses from the API that will be used by multiple observers.
--------------------------------------------------------------------------------
/docs/basics/making-api-calls.md:
--------------------------------------------------------------------------------
1 | # Making API Calls
2 | As discussed in [Core Concepts](../introduction/core-concepts.md), RxQ provides Handle instances that have an `ask` method for executing an API call and then return an Observable for the response.
3 |
4 | Any API method name can be entered via a string. However, the Qlik Engine API is vast so RxQ provides exports of method name enums for each Qlik class.
5 |
6 | Let's use the `EngineVersion` method from Qlik's Global class as an example. We can import the `EngineVersion` enum from RxQ like so:
7 | ```javascript
8 | import { EngineVersion } from "rxq/Global";
9 | ```
10 |
11 | To use it, we call a Global Handle's ask method with it. Then, we can subscribe to get the engine version back:
12 | ```javascript
13 | const version$ = globalHandle.ask(EngineVersion);
14 |
15 | version$.subscribe((version) => {
16 | console.log(version);
17 | });
18 | ```
19 |
20 | There's just one problem here: how do we get the Global Handle in order to make the call? Let's review.
21 |
22 | ## Getting Handles
23 | Handles are provided to us by QAE. Therefore, we can only receive them asychronously through some sort of API call. The Engine has several API calls that will return Handles, such as:
24 | * [OpenDoc](http://help.qlik.com/en-US/sense-developer/November2017/Subsystems/EngineAPI/Content/Classes/GlobalClass/Global-class-OpenDoc-method.htm), which will return a Doc Handle from a Global Handle
25 | * [GetField](http://help.qlik.com/en-US/sense-developer/November2017/Subsystems/EngineAPI/Content/Classes/AppClass/App-class-GetField-method.htm), which will return a Field Handle from a Doc Handle
26 |
27 | RxQ automatically parses the results of Engine API calls and produces Handles for you as needed. However, this still has to happen asynchronously, so we have to write asynchronous logic to connect a Handle with an API call. This is where higher order Observables come into play.
28 |
29 | ## Leveraging Higher Order Observables for API Calls
30 | Higher Order Observables are essentially Observables of Observables. They allow us to create asynchronous data streams based on other asynchronous data streams. This concept is pertitent to us when making API calls, since we are trying to produce an async API call from an asynchronously provided Handle.
31 |
32 | RxJS makes handling these higher order observables easy using operators like [mergeMap](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-mergeMap), [concatMap](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-concatMap), and [switchMap](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-switchMap) to flatten them into normal Observables.
33 |
34 | When using `connectSession`, the resulting Observable provides the Global Handle for the established session. By combining this Observable with the `switchMap` operator, we can get our engine version like so:
35 |
36 | ```javascript
37 | import { connectSession } from "rxq";
38 | import { EngineVersion } from "rxq/Global";
39 | import { switchMap } from "rxjs/operators";
40 |
41 | const session = connectSession({
42 | host: "localhost",
43 | port: 4848,
44 | isSecure: false
45 | });
46 |
47 | const global$ = session.global$;
48 |
49 | const version$ = global$.pipe(
50 | switchMap(handle => handle.ask(EngineVersion))
51 | );
52 |
53 | version$.subscribe(version => {
54 | console.log(version);
55 | });
56 | ```
57 |
58 | We commonly use `switchMap` when utilizing RxQ because we often only care about making an API call on the latest Handle provided. For more on higher order observables, [we recommend this course](https://egghead.io/courses/use-higher-order-observables-in-rxjs-effectively).
--------------------------------------------------------------------------------
/docs/recipes/search-lb.md:
--------------------------------------------------------------------------------
1 | # Search a Listbox
2 | [Code Sandbox](https://codesandbox.io/embed/843q0ny400)
3 | ```javascript
4 | import { connectSession } from "rxq";
5 | import { OpenDoc } from "rxq/Global";
6 | import { CreateSessionObject } from "rxq/Doc";
7 | import { GetLayout, SearchListObjectFor, SelectListObjectValues } from "rxq/GenericObject";
8 | import { filter, map, publish, repeat, shareReplay, startWith, switchMap, withLatestFrom } from "rxjs/operators";
9 | import { fromEvent } from "rxjs";
10 |
11 | const appname = "aae16724-dfd9-478b-b401-0d8038793adf"
12 |
13 | // Define the configuration for your session
14 | const config = {
15 | host: "sense.axisgroup.com",
16 | isSecure: true,
17 | appname
18 | };
19 |
20 | // Connect the session and share the Global handle
21 | const session = connectSession(config);
22 | const global$ = session.global$;
23 |
24 | // Open an app and share the app handle
25 | const app$ = global$.pipe(
26 | switchMap(h => h.ask(OpenDoc, appname)),
27 | shareReplay(1)
28 | );
29 |
30 | // Create a Generic Object with a metric
31 | const obj$ = app$.pipe(
32 | switchMap(h => h.ask(CreateSessionObject, {
33 | "qInfo": {
34 | "qType": "my-object"
35 | },
36 | "myValue": {
37 | "qValueExpression": "=avg(petal_length)"
38 | }
39 | })),
40 | shareReplay(1)
41 | );
42 |
43 | // Get the latest selections whenever the model changes
44 | const metricLayouts$ = obj$.pipe(
45 | switchMap(h => h.invalidated$.pipe(startWith(h))),
46 | switchMap(h => h.ask(GetLayout))
47 | );
48 |
49 | // Print the selections to the DOM
50 | metricLayouts$.subscribe(layout => {
51 | document.querySelector("#metric").innerHTML = `
The average petal length is ${layout.myValue}`;
52 | });
53 |
54 | // Create a Generic Object with a list object for the field "species"
55 | const lb$ = app$.pipe(
56 | switchMap(h => h.ask(CreateSessionObject, {
57 | "qInfo": {
58 | "qType": "my-listbox"
59 | },
60 | "qListObjectDef": {
61 | "qDef": {
62 | "qFieldDefs": ["species"]
63 | },
64 | "qInitialDataFetch": [
65 | {
66 | "qTop": 0,
67 | "qLeft": 0,
68 | "qWidth": 1,
69 | "qHeight": 100
70 | }
71 | ]
72 | }
73 | })),
74 | shareReplay(1)
75 | );
76 |
77 | // Get a stream of list object layouts
78 | const lbLayouts$ = lb$.pipe(
79 | switchMap(h => h.invalidated$.pipe(startWith(h))),
80 | switchMap(h => h.ask(GetLayout))
81 | );
82 |
83 | // Render the list object to the page in an unordered list
84 | lbLayouts$.subscribe(layout => {
85 | const data = layout.qListObject.qDataPages[0].qMatrix;
86 | document.querySelector("ul").innerHTML = data.map(item => `
87 | ${item[0].qText}
88 | `).join("");
89 | });
90 |
91 | // Select values when a user clicks on them
92 | const select$ = fromEvent(document.querySelector("body"), "click").pipe(
93 | filter(evt => evt.target.hasAttribute("data-qno")),
94 | map(evt => parseInt(evt.target.getAttribute("data-qno"))),
95 | withLatestFrom(lb$),
96 | switchMap(([qno, h]) => h.ask(SelectListObjectValues, "/qListObjectDef", [qno], true)),
97 | publish()
98 | );
99 |
100 | select$.connect();
101 |
102 | // Search the listbox when text is entered to the input
103 | const search$ = fromEvent(document.querySelector("input"), "keyup").pipe(
104 | map(evt => evt.target.value),
105 | withLatestFrom(lb$, (val, h) => [val, h]),
106 | switchMap(([val, h]) => h.ask(SearchListObjectFor, "/qListObjectDef", val)),
107 | publish()
108 | );
109 |
110 | search$.connect();
111 | ```
--------------------------------------------------------------------------------
/docs/recipes/offline.md:
--------------------------------------------------------------------------------
1 | # Offline
2 | [Code Sandbox](https://codesandbox.io/embed/o5kwmv77w9)
3 | ```javascript
4 | import { connectSession } from "rxq";
5 | import { GetActiveDoc, OpenDoc } from "rxq/Global";
6 | import { CreateSessionObject } from "rxq/Doc";
7 | import { GetLayout, SelectListObjectValues } from "rxq/GenericObject";
8 | import { fromEvent, merge } from "rxjs";
9 | import { filter, map, mapTo, publish, shareReplay, startWith, switchMap, take, tap, withLatestFrom } from "rxjs/operators";
10 |
11 | const appname = "aae16724-dfd9-478b-b401-0d8038793adf";
12 |
13 | // Event for when app goes on and offline
14 | const online$ = merge(
15 | fromEvent(window, "online").pipe(mapTo(true)),
16 | fromEvent(window, "offline").pipe(mapTo(false))
17 | ).pipe(
18 | startWith(true)
19 | );
20 |
21 | // When going on or offline, show/hide offline banner
22 | online$.subscribe(status => {
23 | if (status) {
24 | document.querySelector("#offline").classList.add("hidden");
25 | }
26 | else {
27 | document.querySelector("#offline").classList.remove("hidden");
28 | }
29 | });
30 |
31 | // Define a session config
32 | const config = {
33 | host: "sense.axisgroup.com",
34 | isSecure: true,
35 | appname
36 | };
37 |
38 | // When going online, connect a new session
39 | const global$ = online$.pipe(
40 | filter(f => f),
41 | switchMap(() => connectSession(config).global$),
42 | shareReplay(1)
43 | );
44 |
45 | // Open an app
46 | const app$ = global$.pipe(
47 | switchMap(h => h.ask(OpenDoc, appname)),
48 | shareReplay(1)
49 | );
50 |
51 | // Calculate a value on invalidation
52 | const layouts$ = app$.pipe(
53 | switchMap(h => h.ask(CreateSessionObject, {
54 | "qInfo": {
55 | "qType": "custom"
56 | },
57 | "value": {
58 | "qValueExpression": "=avg(petal_length)"
59 | }
60 | })),
61 | switchMap(h => h.invalidated$.pipe(startWith(h))),
62 | switchMap(h => h.ask(GetLayout))
63 | );
64 |
65 | // Print the value to the DOM
66 | layouts$.subscribe(layout => {
67 | document.querySelector("#metric").innerHTML = layout.value;
68 | });
69 |
70 | // Create a Generic Object with a list object for the field species
71 | const listbox$ = app$.pipe(
72 | switchMap(h => h.ask(CreateSessionObject, {
73 | "qInfo": {
74 | "qType": "my-listbox"
75 | },
76 | "qListObjectDef": {
77 | "qDef": {
78 | "qFieldDefs": ["species"]
79 | },
80 | "qInitialDataFetch": [
81 | {
82 | "qTop": 0,
83 | "qLeft": 0,
84 | "qWidth": 1,
85 | "qHeight": 100
86 | }
87 | ]
88 | }
89 | })),
90 | shareReplay(1)
91 | );
92 |
93 | // Get a stream of listbox layouts
94 | const listboxLayout$ = listbox$.pipe(
95 | switchMap(h => h.invalidated$.pipe(startWith(h))),
96 | switchMap(h => h.ask(GetLayout))
97 | );
98 |
99 | // Render the list object to the page in an unordered list
100 | listboxLayout$.subscribe(layout => {
101 | const data = layout.qListObject.qDataPages[0].qMatrix;
102 | document.querySelector("ul").innerHTML = data.map(item => `
103 | ${item[0].qText}
104 | `).join("");
105 | });
106 |
107 | // Select values when a user clicks on them
108 | const select$ = fromEvent(document.querySelector("ul"), "click").pipe(
109 | filter(evt => evt.target.hasAttribute("data-qno")),
110 | map(evt => parseInt(evt.target.getAttribute("data-qno"))),
111 | withLatestFrom(listbox$),
112 | switchMap(([qno, h]) => h.ask(SelectListObjectValues, "/qListObjectDef", [qno], true)),
113 | publish()
114 | );
115 |
116 | select$.connect();
117 | ```
--------------------------------------------------------------------------------
/docs/introduction/core-concepts.md:
--------------------------------------------------------------------------------
1 | # Core Concepts
2 |
3 | RxQ provides functions that create Observables around Qlik Associative Engine (QAE) API calls. An API call to QAE happens asynchronously over a network, with some sort of value being returned later in time. These could be API calls like getting the engine version being used, opening a document, or calculating a value from a data model.
4 |
5 | The functions provided by RxQ are capable of making these API calls and returning the responses via Observables. A connection function is provided as well which will return an Observable representing the establishment of a session with QAE.
6 |
7 | ## QAE Background
8 | To better understand how this works, a basic understanding of how Qlik's Engine API works is helpful. The Engine API uses the JSON-RPC 2.0 protocol via a WebSocket for bidirectional communication. Messages can be sent to the Engine to initiate API calls. Responses can be received from the Engine. The Engine can also push messages without a preceding request.
9 |
10 | The Engine has various classes that can be interacted with via API calls. For example, the Global class represents a session with the Engine and has API methods for getting the engine version and opening a document. A Doc class exists for applications that are opened. This class has methods for application operations like clearing selections and getting app properties.
11 |
12 | When making an API call to the Engine, the call must tell the Engine what method to use and on what class instance it should be executed on. This class instance is referred to as a Handle. For exmaple, when opening a document in QAE, the Engine will return an identifier for a Handle for the opened document. The developer can then make API calls on this document by referencing this Handle identifier.
13 |
14 | For a more guided and in-depth review of these concepts, try this [Engine Tutorial](http://playground.qlik.com/learn/engine-tutorial/101.%20What%20is%20QIX%20and%20Why%20Should%20You%20Care.html).
15 |
16 | ## Using RxQ to Make API Calls
17 | In RxQ, Handles are provided with an `ask` method that will accept a method name and parameters and execute an API call to the Engine. An Observable is returned that will provide the response and complete. Method names are provided as strings; for convenience, the list of possible methods in the Engine API schema are provided as enums. [Let's use the "EngineVersion" method of Qlik's "Global" class as an example.](http://help.qlik.com/en-US/sense-developer/November2017/Subsystems/EngineAPI/Content/Classes/GlobalClass/Global-class-EngineVersion-method.htm)
18 |
19 | To make this call in RxQ, we can import the `EngineVersion` enum from RxQ and use it with a Global Handle's `ask` method. The `EngineVersion` method takes no parameters, so we don't need any other inputs. This call will return an Observable that can make the API call and return the response. We can subscribe to this Observable to execute it and get the response.
20 | ```javascript
21 | import { EngineVersion } from "rxq/Global";
22 |
23 | const version$ = myGlobalHandle.ask(EngineVersion);
24 |
25 | version$.subscribe((version) => {
26 | console.log(`The version of the Engine is ${version}`);
27 | });
28 | ```
29 |
30 | If a method takes parameters, we just add them as arguments to our function call. For example, to open a document called "Sales.qvf", we would write:
31 | ```javascript
32 | const app$ = myGlobalHandle.ask(OpenDoc, "Sales.qvf");
33 | ```
34 |
35 | **This is essentially the core of RxQ: run a function that creates an Observable for a Qlik Engine API call response.**
36 |
37 | You may be wondering how to get Handles from the Engine to feed into these functions. This process is detailed in the [Making API Calls](../basics/making-api-calls.md) section.
--------------------------------------------------------------------------------
/test/e2e/genericBookmark.test.js:
--------------------------------------------------------------------------------
1 | const chai = require("chai");
2 | chai.use(require("chai-generator"));
3 | const expect = chai.expect;
4 |
5 | var createContainer = require("../util/create-container");
6 | var {
7 | publish,
8 | publishReplay,
9 | refCount,
10 | shareReplay,
11 | switchMap,
12 | take,
13 | tap,
14 | withLatestFrom
15 | } = require("rxjs/operators");
16 |
17 | var { OpenDoc } = require("../../dist/global");
18 | var { connectSession } = require("../../dist");
19 | var Handle = require("../../dist/_cjs/handle");
20 |
21 | var { CreateBookmark } = require("../../dist/doc");
22 | var { GetProperties, SetProperties } = require("../../dist/genericBookmark");
23 |
24 | var { port, image } = require("./config.json");
25 |
26 | // launch a new container
27 | var container$ = createContainer(image, port);
28 |
29 | var eng$ = container$.pipe(
30 | switchMap(() => {
31 | return connectSession({
32 | host: "localhost",
33 | port: port,
34 | isSecure: false
35 | }).global$;
36 | }),
37 | publishReplay(1),
38 | refCount()
39 | );
40 |
41 | const app$ = eng$.pipe(
42 | switchMap(handle => handle.ask(OpenDoc, "iris.qvf")),
43 | publishReplay(1),
44 | refCount()
45 | );
46 |
47 | const bkmk$ = app$.pipe(
48 | switchMap(handle =>
49 | handle.ask(CreateBookmark, {
50 | qInfo: { qType: "test-bkmk" },
51 | foo: "bar"
52 | })
53 | ),
54 | shareReplay(1)
55 | );
56 |
57 | function testGenericBookmark() {
58 | describe("GenericBookmark Class", function() {
59 | before(function(done) {
60 | this.timeout(10000);
61 | container$.subscribe(() => done());
62 | });
63 |
64 | describe("GetProperties", function() {
65 | const props$ = bkmk$.pipe(
66 | switchMap(h => h.ask(GetProperties)),
67 | shareReplay(1)
68 | );
69 |
70 | it("should return an object", done => {
71 | props$.subscribe(props => {
72 | expect(props).to.be.a("object");
73 | done();
74 | });
75 | });
76 |
77 | it("should return a property 'foo' with value 'bar'", done => {
78 | props$.subscribe(props => {
79 | expect(props.foo).to.equal("bar");
80 | done();
81 | });
82 | });
83 | });
84 |
85 | describe("SetProperties", function() {
86 | const setProps$ = bkmk$.pipe(
87 | switchMap(h =>
88 | h.ask(SetProperties, {
89 | qInfo: {
90 | qType: "test-bkmk"
91 | },
92 | foo: "baz"
93 | })
94 | ),
95 | publish()
96 | );
97 |
98 | it("should cause an invalidation", done => {
99 | bkmk$
100 | .pipe(
101 | switchMap(h => h.invalidated$),
102 | take(1)
103 | )
104 | .subscribe(i => {
105 | done();
106 | });
107 |
108 | setProps$.connect();
109 | });
110 |
111 | it("should change the 'foo' property to 'baz'", done => {
112 | bkmk$
113 | .pipe(
114 | switchMap(h => h.invalidated$),
115 | switchMap(h => h.ask(GetProperties))
116 | )
117 | .subscribe(props => {
118 | expect(props.foo).to.equal("baz");
119 | done();
120 | });
121 |
122 | setProps$.connect();
123 | });
124 | });
125 |
126 | after(function(done) {
127 | container$.subscribe(container =>
128 | container.kill((err, result) => {
129 | container.remove();
130 | done();
131 | })
132 | );
133 | });
134 | });
135 | }
136 |
137 | module.exports = testGenericBookmark;
138 |
--------------------------------------------------------------------------------
/docs/recipes/apply-patches.md:
--------------------------------------------------------------------------------
1 | # Apply Patches to an Object
2 | [Code Sandbox](https://codesandbox.io/embed/2x833lz3on)
3 | ```javascript
4 | import { connectSession } from "rxq";
5 | import { OpenDoc } from "rxq/Global";
6 | import { CreateSessionObject } from "rxq/Doc";
7 | import { ApplyPatches, GetLayout, SelectListObjectValues } from "rxq/GenericObject";
8 | import { filter, map, publish, repeat, shareReplay, startWith, switchMap, withLatestFrom } from "rxjs/operators";
9 | import { fromEvent } from "rxjs";
10 |
11 | const appname = "aae16724-dfd9-478b-b401-0d8038793adf"
12 |
13 | // Define the configuration for your session
14 | const config = {
15 | host: "sense.axisgroup.com",
16 | isSecure: true,
17 | appname
18 | };
19 |
20 | // Connect the session and get the global handle
21 | const session = connectSession(config);
22 | const global$ = session.global$
23 |
24 | // Open an app and share the app handle
25 | const app$ = global$.pipe(
26 | switchMap(h => h.ask(OpenDoc, appname)),
27 | shareReplay(1)
28 | );
29 |
30 | // Create a Generic Object with a metric
31 | const obj$ = app$.pipe(
32 | switchMap(h => h.ask(CreateSessionObject, {
33 | "qInfo": {
34 | "qType": "my-object"
35 | },
36 | "myValue": {
37 | "qValueExpression": "=avg(petal_length)"
38 | }
39 | })),
40 | shareReplay(1)
41 | );
42 |
43 | // Get the latest selections whenever the model changes
44 | const metricLayouts$ = obj$.pipe(
45 | switchMap(h => h.invalidated$.pipe(startWith(h))),
46 | switchMap(h => h.ask(GetLayout))
47 | );
48 |
49 | // Print the selections to the DOM
50 | metricLayouts$.subscribe(layout => {
51 | document.querySelector("#metric").innerHTML = `
The average petal length is ${layout.myValue}`;
52 | });
53 |
54 | // Create a Generic Object with a list object for the field "species"
55 | const lb$ = app$.pipe(
56 | switchMap(h => h.ask(CreateSessionObject, {
57 | "qInfo": {
58 | "qType": "my-listbox"
59 | },
60 | "qListObjectDef": {
61 | "qDef": {
62 | "qFieldDefs": ["species"]
63 | },
64 | "qInitialDataFetch": [
65 | {
66 | "qTop": 0,
67 | "qLeft": 0,
68 | "qWidth": 1,
69 | "qHeight": 100
70 | }
71 | ]
72 | }
73 | })),
74 | shareReplay(1)
75 | );
76 |
77 | // Get a stream of list object layouts
78 | const lbLayouts$ = lb$.pipe(
79 | switchMap(h => h.invalidated$.pipe(startWith(h))),
80 | switchMap(h => h.ask(GetLayout))
81 | );
82 |
83 | // Render the list object to the page in an unordered list
84 | lbLayouts$.subscribe(layout => {
85 | const data = layout.qListObject.qDataPages[0].qMatrix;
86 | document.querySelector("ul").innerHTML = data.map(item => `
87 | ${item[0].qText}
88 | `).join("");
89 | });
90 |
91 | // Select values when a user clicks on them
92 | const select$ = fromEvent(document.querySelector("body"), "click").pipe(
93 | filter(evt => evt.target.hasAttribute("data-qno")),
94 | map(evt => parseInt(evt.target.getAttribute("data-qno"))),
95 | withLatestFrom(lb$),
96 | switchMap(([qno, h]) => h.ask(SelectListObjectValues, "/qListObjectDef", [qno], true)),
97 | publish()
98 | );
99 |
100 | select$.connect();
101 |
102 | // Change the dimension with a dropdown
103 | const patch$ = fromEvent(document.querySelector("select"), "change").pipe(
104 | map(evt => evt.target.value),
105 | withLatestFrom(lb$),
106 | switchMap(([dim, h]) => h.ask(ApplyPatches, [
107 | {
108 | qPath: "/qListObjectDef/qDef/qFieldDefs/0",
109 | qOp: "replace",
110 | qValue: JSON.stringify(dim)
111 | }
112 | ])),
113 | publish()
114 | );
115 |
116 | patch$.connect();
117 | ```
--------------------------------------------------------------------------------
/scripts/build-qix-methods.js:
--------------------------------------------------------------------------------
1 | // with qix version, go through the schema and publish into operators
2 | var fs = require("fs-extra");
3 | var path = require("path");
4 | var pack = JSON.parse(fs.readFileSync("package.json", "utf8"));
5 | var Docker = require("dockerode");
6 | var { Observable } = require("rxjs");
7 | var { publishReplay, refCount, switchMap } = require("rxjs/operators");
8 | const http = require("http");
9 |
10 | // qix version
11 | var version = pack["qix-version"];
12 |
13 | const image = `qlikcore/engine:${version}`;
14 | const port = "9079";
15 |
16 | const container$ = createContainer(image, port);
17 |
18 | const schema$ = container$.pipe(
19 | switchMap(container =>
20 | Observable.create(observer => {
21 | http
22 | .get(`http://localhost:${port}/jsonrpc-api`, resp => {
23 | let data = "";
24 |
25 | resp.on("data", chunk => {
26 | data += chunk;
27 | });
28 |
29 | resp.on("end", () => {
30 | container.kill((err, result) => {
31 | container.remove();
32 | observer.complete();
33 | });
34 |
35 | observer.next(JSON.parse(data));
36 | });
37 | })
38 | .on("error", err => {
39 | observer.error(err);
40 | });
41 | })
42 | )
43 | );
44 |
45 | schema$.subscribe(schema => {
46 | const schemaOutput = Object.keys(schema.services).reduce((acc, qClass) => {
47 | const classMethods = schema.services[qClass].methods;
48 | return {
49 | ...acc,
50 | [qClass]: Object.keys(classMethods).reduce((methodList, method) => {
51 | const methodResponses = classMethods[method].responses;
52 | return {
53 | ...methodList,
54 | [method]:
55 | typeof methodResponses === "undefined"
56 | ? []
57 | : methodResponses.map(response => response.name)
58 | };
59 | }, {})
60 | };
61 | }, {});
62 |
63 | var qClasses = Object.keys(schema.services);
64 |
65 | var classImports = [];
66 | var classExports = [];
67 |
68 | qClasses.forEach(qClass => {
69 | var methods = schema.services[qClass].methods;
70 |
71 | var classDir = `../src/${qClass}`;
72 | var absClassDir = path.join(__dirname, classDir);
73 | fs.emptydirSync(absClassDir);
74 |
75 | const eNumScript = Object.keys(methods)
76 | .reduce((acc, methodname) => {
77 | return [
78 | `const ${methodname} = "${methodname}"`,
79 | ...acc,
80 | `export { ${methodname} };`
81 | ];
82 | }, [])
83 | .join("\n");
84 |
85 | fs.writeFile(path.join(absClassDir, `index.js`), eNumScript);
86 |
87 | classImports.push(`import * as ${qClass} from "./${qClass}";`);
88 | classExports.push(`export { ${qClass} }`);
89 | });
90 | });
91 |
92 | function createContainer(image, port) {
93 | // launch a new container
94 | var container$ = Observable.create(observer => {
95 | var docker = new Docker();
96 |
97 | docker.createContainer(
98 | {
99 | Image: image,
100 | Cmd: ["-S", "AcceptEULA=yes"],
101 | ExposedPorts: {
102 | "9076/tcp": {}
103 | },
104 | HostConfig: {
105 | RestartPolicy: {
106 | Name: "always"
107 | },
108 | PortBindings: {
109 | "9076/tcp": [
110 | {
111 | HostPort: port
112 | }
113 | ]
114 | }
115 | }
116 | },
117 | (err, container) => {
118 | if (err) return observer.error(err);
119 |
120 | container.start((err, data) => {
121 | if (err) return observer.error(err);
122 | setTimeout(() => {
123 | observer.next(container);
124 | observer.complete();
125 | }, 2000);
126 | });
127 | }
128 | );
129 | }).pipe(
130 | publishReplay(1),
131 | refCount()
132 | );
133 |
134 | return container$;
135 | }
136 |
--------------------------------------------------------------------------------
/scripts/build-browser.js:
--------------------------------------------------------------------------------
1 | var webpack = require("webpack");
2 | const path = require("path");
3 | var StringReplacePlugin = require("string-replace-webpack-plugin");
4 | var program = require("commander");
5 |
6 | program
7 | .version("0.1.0")
8 | .option("--min", "minified")
9 | .parse(process.argv);
10 |
11 | const plugins = [new StringReplacePlugin()];
12 |
13 | const filename = program.min ? "rxq.bundle.min.js" : "rxq.bundle.js";
14 |
15 | webpack(
16 | {
17 | mode: "production",
18 | entry: path.join(__dirname, "../index.js"),
19 | output: {
20 | path: path.resolve(__dirname, "../dist/bundle"),
21 | filename: filename,
22 | library: "RxQ",
23 | libraryTarget: "umd"
24 | },
25 | devtool: "source-map",
26 | optimization: {
27 | minimize: program.min || false
28 | },
29 | module: {
30 | rules: [
31 | // configure replacements for file patterns
32 | {
33 | enforce: "pre",
34 | test: /src\/|index.js$/,
35 | loader: StringReplacePlugin.replace({
36 | replacements: [
37 | {
38 | pattern: /import {([^;]*?)} from ([^;]*?)('|")rxjs([^;]*?)('|");/g,
39 | //pattern: /from ([^;]*?)('|")rxjs([^;]*?)('|");/g,
40 | replacement: function(
41 | match,
42 | p1,
43 | p2,
44 | p3,
45 | p4,
46 | p5,
47 | offset,
48 | string
49 | ) {
50 | return match
51 | .replace("import", "const")
52 | .replace(/from ('|")rxjs('|")/g, "= rxjs")
53 | .replace(
54 | /from ('|")rxjs\/operators('|")/g,
55 | "= rxjs.operators"
56 | )
57 | .replace(/ as /g, ": ");
58 |
59 | // Check if loading an operator or not
60 | var operatorsFlag = p4.split("/").indexOf("operators") > 0;
61 |
62 | // The source to load from
63 | var srcBase = ["Rx"]
64 | .concat(p4.split("/").filter(f => f != ""))
65 | .join(".");
66 |
67 | // Get vars to map
68 | var defs = p1
69 | .replace(/({|})/g, "")
70 | .split(",")
71 | .map(d => {
72 | var splits = d
73 | .split(" as ")
74 | .map(s => s.replace(/( |\n)/g, ""));
75 |
76 | return {
77 | name: splits[1] || splits[0],
78 | source: splits[0]
79 | };
80 | });
81 |
82 | // Turn the var definitions into statements
83 | var globalImports = defs
84 | .map(d => {
85 | var dec = `var ${d.name} = ${srcBase}`;
86 | if (operatorsFlag) dec += `.${d.source}`;
87 | dec += ";";
88 | return dec;
89 | })
90 | .join("\n")
91 | .replace(/observable/g, "Observable");
92 |
93 | return globalImports;
94 | }
95 | }
96 | ]
97 | })
98 | },
99 | // Babel
100 | {
101 | test: /\.js$/,
102 | exclude: /node_modules/,
103 | loader: "babel-loader",
104 | options: {
105 | presets: ["es2015"],
106 | plugins: [
107 | // "transform-runtime",
108 | "transform-object-rest-spread",
109 | "babel-plugin-add-module-exports"
110 | ]
111 | }
112 | }
113 | ]
114 | },
115 | plugins: plugins
116 | },
117 | (err, stats) => {
118 | if (err || stats.hasErrors()) {
119 | console.log(err);
120 | // Handle errors here
121 | }
122 | console.log(stats);
123 | // Done processing
124 | }
125 | );
126 |
--------------------------------------------------------------------------------
/docs/basics/shortcut-operators.md:
--------------------------------------------------------------------------------
1 | # Shortcut Operators for Common Patterns
2 |
3 | ## Common Patterns
4 | In the [Making API Calls](making-api-calls.html) section, we discuss using methods like `mergeMap`, `switchMap`, and `concatMap` along with a Handle's `ask` method to turn an Observable of a Handle into an Observable of an API call. For example, to get the engine version from a global handle, you would do the following:
5 | ```javascript
6 | // Assuming an Observable of a Global Handle
7 | const global$;
8 |
9 | const engineVersion$ = global$.pipe(
10 | switchMap(handle => handle.ask(EngineVersion))
11 | );
12 | ```
13 |
14 | When working with Handle Observables, you often want to reuse them to make API calls. In this scenario, it is common to [share the handle observable via multicasting](reusing-handles.html):
15 | ```javascript
16 | const app$ = global$.pipe(
17 | switchMap(handle => handle.ask(OpenDoc, "my-app.qvf")),
18 | shareReplay(1)
19 | );
20 | ```
21 |
22 | Similarly, you can get a stream of [handle invalidations](handle-invalidations.html) using the following pattern:
23 | ```javascript
24 | // Assume an Observable of a Doc Handle
25 | const app$;
26 |
27 | const appInvalidations$ = app$.pipe(
28 | switchMap(handle => handle.invalidated$)
29 | );
30 | ```
31 |
32 | ## Simplifying these Patterns
33 | Manually using operators like `switchMap`, `mergeMap`, and `concatMap` can be useful when writing more complex async logic with the Qlik Engine. However, in our experience the vast majority of API calls with RxQ follow the patterns above. It can get tedious rewriting these patterns over and over, so we have encapsulated the logic into 3 helper operators:
34 |
35 | ### qAsk
36 | `qAsk` is an alias for `switchMap(handle => handle.ask())`. For example, the following patterns are equivalent:
37 |
38 | *EngineVersion using original pattern*
39 | ```javascript
40 | const engineVersion$ = global$.pipe(
41 | switchMap(handle => handle.ask(EngineVersion))
42 | );
43 | ```
44 |
45 | *EngineVersion using qAsk*
46 | ```javascript
47 | import { qAsk } from "rxq";
48 |
49 | const engineVersion$ = global$.pipe(
50 | qAsk(EngineVersion)
51 | );
52 | ```
53 |
54 | ### qAskReplay
55 | `qAskReplay` is an alias for `switchMap(handle => handle.ask()), shareReplay(1)`. For example, the following patterns are equivalent:
56 |
57 | *App Handle using original pattern*
58 | ```javascript
59 | const doc$ = global$.pipe(
60 | switchMap(handle => handle.ask(OpenDoc, "my-app.qvf")),
61 | shareReplay(1)
62 | );
63 | ```
64 |
65 | *App Handle using qAskReplay*
66 | ```javascript
67 | import { qAskReplay } from "rxq";
68 |
69 | const doc$$ = global$.pipe(
70 | qAskReplay(OpenDoc, "my-app.qvf")
71 | );
72 | ```
73 |
74 | ### invalidations
75 | `invalidations` is an alias for `switchMap(handle => handle.invalidated$)`. For example, the following patterns are equivalent:
76 |
77 | *App Handle invalidations using original pattern*
78 | ```javascript
79 | const docInvalidations$ = doc$.pipe(
80 | switchMap(handle => handle.invalidated$)
81 | );
82 | ```
83 |
84 | *App Handle invalidations using `invalidations`*
85 | ```javascript
86 | import { invalidations } from "rxq";
87 |
88 | const docInvalidations$ = doc$.pipe(
89 | invalidations()
90 | );
91 | ```
92 |
93 | In many cases, it's useful to assume an initial invalidation for a Qlik Handle when working with it. Automatically starting with an invalidation can be accomplished by calling `qInvalidations(true)`:
94 |
95 | *App Handle invalidations with initial handle emit*
96 | ```javascript
97 | import { invalidations } from "rxq";
98 |
99 | const docInvalidations$ = doc$.pipe(
100 | invalidations(true)
101 | );
102 | ```
103 |
104 | Putting these together, you can greatly simplify some common scenarios. For example, consider getting a stream of layouts for a generic object:
105 |
106 | *GenericObject layouts via original pattern*
107 | ```javascript
108 | const layouts$ = object$.pipe(
109 | switchMap(handle => handle.invalidated$.pipe(
110 | startWith(handle)
111 | )
112 | ),
113 | switchMap(handle => handle.ask(GetLayout))
114 | );
115 | ```
116 |
117 | *GenericObject layouts using shortcut operators*
118 | ```javascript
119 | const layouts$ = object$.pipe(
120 | invalidations(true),
121 | qAsk(GetLayout)
122 | );
123 | ```
--------------------------------------------------------------------------------
/test/unit/Session.spec.js:
--------------------------------------------------------------------------------
1 | const chai = require("chai");
2 | chai.use(require("chai-generator"));
3 | const expect = chai.expect;
4 |
5 | var { Observable, Subject } = require("rxjs");
6 | var { pluck, take } = require("rxjs/operators");
7 | const isObservable = require("../util/isObservable");
8 | const mockEngine = require("../util/mock-qix-engine.js");
9 |
10 | // RxQ
11 | var { connectSession } = require("../../dist");
12 | var Handle = require("../../dist/_cjs/handle");
13 | var Session = require("../../dist/_cjs/session");
14 |
15 | describe("Session", function() {
16 | // Mock Engine for Testing
17 | var { server, ws } = mockEngine();
18 | var config = {
19 | ws
20 | };
21 |
22 | const session = connectSession(config);
23 | var eng$ = session.global$;
24 |
25 | var sesh$ = eng$.pipe(pluck("session"));
26 |
27 | it("should be a function", function() {
28 | expect(Session).to.be.a("function");
29 | });
30 |
31 | it("should have an ask method", function(done) {
32 | sesh$.subscribe(sesh => {
33 | expect(sesh.ask).to.be.a("function");
34 | done();
35 | });
36 | });
37 |
38 | it("should have a global method", function(done) {
39 | sesh$.subscribe(sesh => {
40 | expect(sesh.global).to.be.a("function");
41 | done();
42 | });
43 | });
44 |
45 | it("should have a WebSocket Observable", function(done) {
46 | sesh$.subscribe(sesh => {
47 | expect(isObservable(sesh.ws$)).to.equal(true);
48 | // expect(sesh.ws$).to.be.instanceof(Observable);
49 | done();
50 | });
51 | });
52 |
53 | it("should have a response Observable", function(done) {
54 | sesh$.subscribe(sesh => {
55 | expect(isObservable(sesh.finalResponse$)).to.equal(true);
56 | done();
57 | });
58 | });
59 |
60 | it("should have a changes Observable", function(done) {
61 | sesh$.subscribe(sesh => {
62 | expect(isObservable(sesh.changes$)).to.equal(true);
63 | done();
64 | });
65 | });
66 |
67 | it("should have a sequence integer generator", function(done) {
68 | sesh$.subscribe(sesh => {
69 | expect(sesh).to.have.property("seqGen");
70 | done();
71 | });
72 | });
73 |
74 | describe("ask method", function() {
75 | it("should return an Observable", function(done) {
76 | sesh$.subscribe(sesh => {
77 | var req = sesh.ask({});
78 | expect(isObservable(req)).to.equal(true);
79 | done();
80 | });
81 | });
82 |
83 | it("should dispatch a method to the request stream", function(done) {
84 | sesh$.subscribe(sesh => {
85 | var methodName = "myMethod";
86 |
87 | sesh.requests$.pipe(take(1)).subscribe(r => {
88 | var method = r.method;
89 | expect(method).to.equal(methodName);
90 | done();
91 | });
92 |
93 | var req = sesh.ask({ method: "myMethod" });
94 | });
95 | });
96 |
97 | it("should dispatch method parameters to the request stream", function(done) {
98 | sesh$.subscribe(sesh => {
99 | var methodParams = ["hello", 42, true];
100 |
101 | sesh.requests$.pipe(take(1)).subscribe(r => {
102 | var params = r.params;
103 | expect(params).to.equal(methodParams);
104 | done();
105 | });
106 |
107 | var req = sesh.ask({ params: methodParams });
108 | });
109 | });
110 |
111 | it("should give requests a numeric id", function(done) {
112 | sesh$.subscribe(sesh => {
113 | sesh.requests$.pipe(take(1)).subscribe(r => {
114 | var id = r.id;
115 | expect(id).to.be.a("number");
116 | done();
117 | });
118 |
119 | var req = sesh.ask({});
120 | });
121 | });
122 | });
123 |
124 | describe("global method", function() {
125 | it("should return an Observable", function(done) {
126 | sesh$.subscribe(sesh => {
127 | var global$ = sesh.global();
128 | expect(isObservable(global$)).to.equal(true);
129 | done();
130 | });
131 | });
132 | });
133 |
134 | describe("Sequence Generator", function() {
135 | it("should yield a value", function(done) {
136 | sesh$.subscribe(sesh => {
137 | var gen = sesh.seqGen;
138 | expect(gen).to.yield();
139 | done();
140 | });
141 | });
142 |
143 | it("should increment values by 1", function(done) {
144 | sesh$.subscribe(sesh => {
145 | var gen = sesh.seqGen;
146 | var firstValue = gen.next().value;
147 | var secondValue = gen.next().value;
148 | expect(secondValue).to.equal(firstValue + 1);
149 | done();
150 | });
151 | });
152 | });
153 |
154 | after(function() {
155 | server.stop();
156 | });
157 | });
158 |
--------------------------------------------------------------------------------
/test/unit/Handle.spec.js:
--------------------------------------------------------------------------------
1 | const chai = require("chai");
2 | chai.use(require("chai-generator"));
3 | const expect = chai.expect;
4 |
5 | var { Observable, Subject } = require("rxjs");
6 | var { pluck, take, shareReplay } = require("rxjs/operators");
7 | const mockEngine = require("../util/mock-qix-engine.js");
8 | const isObservable = require("../util/isObservable");
9 |
10 | // RxQ
11 | var { connectSession } = require("../../dist");
12 | var Handle = require("../../dist/_cjs/handle");
13 | var Session = require("../../dist/_cjs/session");
14 |
15 | describe("Handle", function() {
16 | // Mock Engine for Testing
17 | var { server, ws } = mockEngine();
18 | var config = {
19 | ws
20 | };
21 |
22 | const session = connectSession(config);
23 | const notifications$ = session.notifications$;
24 | var eng$ = session.global$;
25 |
26 | it("should have a handle number property", function(done) {
27 | eng$.subscribe(h => {
28 | expect(h.handle).to.be.a("number");
29 | done();
30 | });
31 | });
32 |
33 | it("should have an invalidated Observable stream", function(done) {
34 | eng$.subscribe(h => {
35 | expect(isObservable(h.invalidated$)).to.equal(true);
36 | done();
37 | });
38 | });
39 |
40 | it("should have a qClass string property", function(done) {
41 | eng$.subscribe(h => {
42 | expect(h.qClass).to.be.a("string");
43 | done();
44 | });
45 | });
46 |
47 | it("should have a Session property", function(done) {
48 | eng$.subscribe(h => {
49 | expect(h.session).to.be.instanceof(Session);
50 | done();
51 | });
52 | });
53 |
54 | it("should have an ask method", function(done) {
55 | eng$.subscribe(h => {
56 | expect(h.ask).to.be.a("function");
57 | done();
58 | });
59 | });
60 |
61 | describe("ask method", function() {
62 | it("should return an Observable", function(done) {
63 | eng$.subscribe(h => {
64 | var req = h.ask("t");
65 | expect(isObservable(req)).to.equal(true);
66 | done();
67 | });
68 | });
69 |
70 | it("should dispatch a method to the request stream", function(done) {
71 | eng$.subscribe(handle => {
72 | var sesh = handle.session;
73 |
74 | var methodName = "myMethod";
75 |
76 | sesh.requests$.pipe(take(1)).subscribe(r => {
77 | var method = r.method;
78 | expect(method).to.equal(methodName);
79 | done();
80 | });
81 |
82 | var req = handle.ask("myMethod").subscribe();
83 | });
84 | });
85 |
86 | it("should dispatch method parameters to the request stream", function(done) {
87 | eng$.subscribe(handle => {
88 | var sesh = handle.session;
89 |
90 | var methodParams = ["hello", 42, true];
91 |
92 | sesh.requests$.pipe(take(1)).subscribe(r => {
93 | var params = r.params;
94 | expect(params).to.deep.equal(methodParams);
95 | done();
96 | });
97 |
98 | var req = handle
99 | .ask("methodParamsTest - " + sesh.sessionId, ...methodParams)
100 | .subscribe();
101 | });
102 | });
103 |
104 | it("should filter out undefined parameters", function(done) {
105 | eng$.subscribe(handle => {
106 | var sesh = handle.session;
107 |
108 | var methodParams = ["hello", 42, undefined];
109 | var expectedParams = ["hello", 42];
110 |
111 | sesh.requests$.pipe(take(1)).subscribe(r => {
112 | var params = r.params;
113 | expect(params).to.deep.equal(expectedParams);
114 | done();
115 | });
116 |
117 | var req = handle
118 | .ask("methodParamsTest - " + sesh.sessionId, ...methodParams)
119 | .subscribe();
120 | });
121 | });
122 |
123 | it("should give requests a numeric id", function(done) {
124 | eng$.subscribe(handle => {
125 | var sesh = handle.session;
126 |
127 | sesh.requests$.pipe(take(1)).subscribe(r => {
128 | var id = r.id;
129 | expect(id).to.be.a("number");
130 | done();
131 | });
132 |
133 | var req = handle.ask("numericId - " + sesh.sessionId).subscribe();
134 | });
135 | });
136 |
137 | it("should generate requests for its handle number", function(done) {
138 | eng$.subscribe(handle => {
139 | var sesh = handle.session;
140 | var no = handle.handle;
141 |
142 | sesh.requests$.pipe(take(1)).subscribe(r => {
143 | var h = r.handle;
144 | expect(h).to.equal(no);
145 | done();
146 | });
147 |
148 | var req = handle.ask("handleNumber - " + sesh.sessionId).subscribe();
149 | });
150 | });
151 | });
152 |
153 | after(function() {
154 | server.stop();
155 | });
156 | });
157 |
--------------------------------------------------------------------------------
/test/e2e/suspend.test.js:
--------------------------------------------------------------------------------
1 | const chai = require("chai");
2 | chai.use(require("chai-generator"));
3 | const expect = chai.expect;
4 |
5 | var createContainer = require("../util/create-container");
6 | var {
7 | map,
8 | publish,
9 | publishReplay,
10 | refCount,
11 | shareReplay,
12 | switchMap,
13 | take,
14 | takeUntil,
15 | withLatestFrom,
16 | tap
17 | } = require("rxjs/operators");
18 | var { Subject } = require("rxjs");
19 |
20 | var { OpenDoc } = require("../../dist/global");
21 | var { connectSession, suspendUntilCompleted } = require("../../dist");
22 | var Handle = require("../../dist/_cjs/handle");
23 |
24 | var { GetAppProperties, SetAppProperties } = require("../../dist/doc");
25 |
26 | var { port, image } = require("./config.json");
27 |
28 | // launch a new container
29 | var container$ = createContainer(image, port);
30 |
31 | const session$ = container$.pipe(
32 | map(() => {
33 | return connectSession({
34 | host: "localhost",
35 | port: port,
36 | isSecure: false
37 | });
38 | }),
39 | shareReplay(1)
40 | );
41 |
42 | var eng$ = session$.pipe(
43 | switchMap(session => session.global$),
44 | shareReplay(1)
45 | );
46 |
47 | const app$ = eng$.pipe(
48 | switchMap(handle => handle.ask(OpenDoc, "iris.qvf")),
49 | publishReplay(1),
50 | refCount()
51 | );
52 |
53 | function testSuspend() {
54 | describe("Suspend", function() {
55 | before(function(done) {
56 | this.timeout(10000);
57 | app$.subscribe(() => done());
58 | });
59 |
60 | it("should withhold invalidations while suspended", function(done) {
61 | this.timeout(5000);
62 |
63 | // Trigger invalidation event by changing app events
64 | const setAppProps$ = app$.pipe(
65 | withLatestFrom(session$, (appH, session) => {
66 | session.suspend();
67 | return appH;
68 | }),
69 | switchMap(handle => handle.ask(GetAppProperties)),
70 | take(1),
71 | withLatestFrom(app$),
72 | switchMap(([props, handle]) => {
73 | const newProps = Object.assign({ test: "invalid" }, props);
74 | return handle.ask(SetAppProperties, newProps);
75 | }),
76 | publish()
77 | );
78 |
79 | const invalid$ = app$.pipe(switchMap(h => h.invalidated$));
80 |
81 | var streamKill$ = new Subject();
82 |
83 | invalid$.pipe(takeUntil(streamKill$)).subscribe(h => {
84 | done(new Error("Invalidation fired"));
85 | });
86 |
87 | setTimeout(() => {
88 | streamKill$.next(undefined);
89 | done();
90 | }, 2000);
91 |
92 | setAppProps$.connect();
93 | });
94 |
95 | it("should share buffered invalidations when unsuspended", function(done) {
96 | const invalid$ = app$.pipe(switchMap(h => h.invalidated$));
97 |
98 | invalid$.subscribe(() => {
99 | done();
100 | });
101 |
102 | session$.subscribe(session => {
103 | session.unsuspend();
104 | });
105 | });
106 |
107 | describe("suspendUntilCompleted operator", function() {
108 | it("should buffer invalidations until the Observable completes", function(done) {
109 | this.timeout(10000);
110 |
111 | const session$ = container$.pipe(
112 | map(() => {
113 | return connectSession({
114 | host: "localhost",
115 | port: port,
116 | isSecure: false,
117 | appname: "iris.qvf"
118 | });
119 | }),
120 | shareReplay(1)
121 | );
122 |
123 | var eng$ = session$.pipe(
124 | switchMap(session => session.global$),
125 | publishReplay(1),
126 | refCount()
127 | );
128 |
129 | const app$ = eng$.pipe(
130 | switchMap(handle => handle.ask(OpenDoc, "iris.qvf")),
131 | publishReplay(1),
132 | refCount()
133 | );
134 |
135 | // Trigger invalidation event by changing app events
136 | const setAppProps$ = app$.pipe(
137 | withLatestFrom(session$),
138 | switchMap(([appHandle, session]) =>
139 | appHandle.ask(GetAppProperties).pipe(
140 | switchMap(props => {
141 | const newProps = Object.assign({ test: "invalid" }, props);
142 | return appHandle.ask(SetAppProperties, newProps);
143 | }),
144 | suspendUntilCompleted(session)
145 | )
146 | ),
147 | publish()
148 | );
149 |
150 | const invalid$ = app$.pipe(switchMap(h => h.invalidated$));
151 |
152 | invalid$.subscribe(h => {
153 | done();
154 | });
155 |
156 | setAppProps$.connect();
157 | });
158 | });
159 |
160 | after(function(done) {
161 | container$.subscribe(container =>
162 | container.kill((err, result) => {
163 | container.remove();
164 | done();
165 | })
166 | );
167 | });
168 | });
169 | }
170 |
171 | module.exports = testSuspend;
172 |
--------------------------------------------------------------------------------
/test/e2e/closeSession.test.js:
--------------------------------------------------------------------------------
1 | const chai = require("chai");
2 | chai.use(require("chai-generator"));
3 | const expect = chai.expect;
4 |
5 | var createContainer = require("../util/create-container");
6 | var {
7 | publishReplay,
8 | refCount,
9 | switchMap,
10 | map,
11 | filter,
12 | shareReplay
13 | } = require("rxjs/operators");
14 | var { connectSession } = require("../../dist");
15 | var Handle = require("../../dist/_cjs/handle");
16 | var { OpenDoc } = require("../../dist/_cjs/Global");
17 |
18 | var { Observable } = require("rxjs");
19 |
20 | var { port, image } = require("./config.json");
21 |
22 | // launch a new container
23 | var container$ = createContainer(image, port);
24 |
25 | var session$ = container$.pipe(
26 | map(() => {
27 | return connectSession({
28 | host: "localhost",
29 | port: port,
30 | isSecure: false
31 | });
32 | }),
33 | publishReplay(1),
34 | refCount()
35 | );
36 |
37 | const eng$ = session$.pipe(
38 | switchMap(session => session.global$),
39 | shareReplay(1)
40 | );
41 | const notifications$ = session$.pipe(
42 | switchMap(session => session.notifications$)
43 | );
44 | const ws$ = eng$.pipe(switchMap(eng => eng.session.ws$));
45 | const socketClosed$ = ws$.pipe(
46 | switchMap(ws =>
47 | Observable.create(observer => {
48 | ws.addEventListener("close", evt => {
49 | observer.next(evt);
50 | });
51 | })
52 | )
53 | );
54 |
55 | const app$ = eng$.pipe(
56 | switchMap(h => h.ask(OpenDoc, "iris.qvf")),
57 | publishReplay(1)
58 | );
59 |
60 | function testClose() {
61 | describe("Close", function() {
62 | before(function(done) {
63 | this.timeout(10000);
64 | container$.subscribe(() => done());
65 | // connect the app
66 | app$.connect();
67 | });
68 |
69 | it("should close the WebSocket when the close function is called", function(done) {
70 | socketClosed$.subscribe(closedEvt => {
71 | done();
72 | });
73 |
74 | // Get the engine handle and then close
75 | app$.pipe(switchMap(() => session$)).subscribe(session => {
76 | session.close();
77 | });
78 | });
79 |
80 | it("should complete invalidation Observables when the close function is called", function(done) {
81 | app$.pipe(switchMap(handle => handle.invalidated$)).subscribe({
82 | complete: () => done()
83 | });
84 | });
85 |
86 | it("should trigger the 'socket:close' notification", done => {
87 | const session$ = container$.pipe(
88 | map(() => {
89 | return connectSession({
90 | host: "localhost",
91 | port: port,
92 | isSecure: false
93 | });
94 | }),
95 | publishReplay(1),
96 | refCount()
97 | );
98 |
99 | const eng$ = session$.pipe(
100 | switchMap(session => session.global$),
101 | shareReplay(1)
102 | );
103 | const notifications$ = session$.pipe(
104 | switchMap(session => session.notifications$)
105 | );
106 |
107 | notifications$
108 | .pipe(filter(f => f.type === "socket:close"))
109 | .subscribe(() => done());
110 |
111 | eng$
112 | .pipe(switchMap(() => session$))
113 | .subscribe(session => session.close());
114 | });
115 |
116 | it("should complete the responses stream", done => {
117 | const session$ = container$.pipe(
118 | map(() => {
119 | return connectSession({
120 | host: "localhost",
121 | port: port,
122 | isSecure: false
123 | });
124 | }),
125 | publishReplay(1),
126 | refCount()
127 | );
128 |
129 | const eng$ = session$.pipe(
130 | switchMap(session => session.global$),
131 | shareReplay(1)
132 | );
133 | const notifications$ = session$.pipe(
134 | switchMap(session => session.notifications$)
135 | );
136 |
137 | eng$.pipe(switchMap(handle => handle.session.finalResponse$)).subscribe({
138 | complete: () => done()
139 | });
140 |
141 | eng$
142 | .pipe(switchMap(() => session$))
143 | .subscribe(session => session.close());
144 | });
145 |
146 | it("should complete the requests stream", done => {
147 | const session$ = container$.pipe(
148 | map(() => {
149 | return connectSession({
150 | host: "localhost",
151 | port: port,
152 | isSecure: false
153 | });
154 | }),
155 | publishReplay(1),
156 | refCount()
157 | );
158 |
159 | const eng$ = session$.pipe(
160 | switchMap(session => session.global$),
161 | shareReplay(1)
162 | );
163 | const notifications$ = session$.pipe(
164 | switchMap(session => session.notifications$)
165 | );
166 |
167 | eng$.pipe(switchMap(handle => handle.session.requests$)).subscribe({
168 | complete: () => done()
169 | });
170 |
171 | eng$
172 | .pipe(switchMap(() => session$))
173 | .subscribe(session => session.close());
174 | });
175 |
176 | after(function(done) {
177 | container$.subscribe(container =>
178 | container.kill((err, result) => {
179 | container.remove();
180 | done();
181 | })
182 | );
183 | });
184 | });
185 | }
186 |
187 | module.exports = testClose;
188 |
--------------------------------------------------------------------------------
/test/e2e/utilityOperators.test.js:
--------------------------------------------------------------------------------
1 | const chai = require("chai");
2 | const spies = require("chai-spies");
3 | chai.use(require("chai-generator"));
4 | chai.use(spies);
5 | const expect = chai.expect;
6 |
7 | var createContainer = require("../util/create-container");
8 | var {
9 | shareReplay,
10 | map,
11 | switchMap,
12 | tap,
13 | publish,
14 | take
15 | } = require("rxjs/operators");
16 | const { of } = require("rxjs");
17 |
18 | var { OpenDoc } = require("../../dist/global");
19 | var { CreateSessionObject, GetAppProperties } = require("../../dist/doc");
20 | var { SetProperties } = require("../../dist/GenericObject");
21 | var { connectSession, qAsk, qAskReplay, invalidations } = require("../../dist");
22 |
23 | var { port, image } = require("./config.json");
24 |
25 | // launch a new container
26 | var container$ = createContainer(image, port);
27 |
28 | var session$ = container$.pipe(
29 | map(() => {
30 | return connectSession({
31 | host: "localhost",
32 | port: port,
33 | isSecure: false
34 | });
35 | }),
36 | shareReplay(1)
37 | );
38 |
39 | var eng$ = session$.pipe(switchMap(session => session.global$));
40 |
41 | const app$ = eng$.pipe(
42 | switchMap(handle => handle.ask(OpenDoc, "iris.qvf")),
43 | shareReplay(1)
44 | );
45 |
46 | function testUtilityOperators() {
47 | describe("Utility Operators", function() {
48 | before(function(done) {
49 | this.timeout(10000);
50 | app$.subscribe(() => done());
51 | });
52 |
53 | describe("qAsk", () => {
54 | it("should make an Engine API call and deliver the response", done => {
55 | app$.pipe(qAsk(GetAppProperties)).subscribe(props => {
56 | expect(props.qTitle).to.equal("Iris");
57 | done();
58 | });
59 | });
60 |
61 | it("should throw an error when called on an Observable that doesn't emit a Handle", done => {
62 | of(null)
63 | .pipe(qAsk(GetAppProperties))
64 | .subscribe({
65 | error: err => {
66 | done();
67 | }
68 | });
69 | });
70 | });
71 |
72 | describe("qAskReplay", () => {
73 | it("should make an Engine API call and share the response across subscribers", done => {
74 | var count = 0;
75 |
76 | const appProps$ = app$.pipe(
77 | tap(() => count++),
78 | qAskReplay(GetAppProperties)
79 | );
80 |
81 | appProps$.subscribe(props => {
82 | appProps$.subscribe(props => {
83 | expect(props.qTitle).to.equal("Iris");
84 | expect(count).to.equal(1);
85 | done();
86 | });
87 | });
88 | });
89 |
90 | it("should throw an error when called on an Observable that doesn't emit a Handle", done => {
91 | of(null)
92 | .pipe(qAskReplay(GetAppProperties))
93 | .subscribe({
94 | error: err => {
95 | done();
96 | }
97 | });
98 | });
99 | });
100 |
101 | describe("invalidations", () => {
102 | it("should not start with an invalidation by default", done => {
103 | const obj$ = app$.pipe(
104 | qAskReplay(CreateSessionObject, {
105 | qInfo: { qType: "test" },
106 | foo: "bar"
107 | })
108 | );
109 |
110 | const testFn = chai.spy();
111 | const invalidations$ = obj$.pipe(
112 | invalidations(),
113 | tap(testFn),
114 | publish()
115 | );
116 |
117 | invalidations$.connect();
118 |
119 | // update app props
120 | const setProps$ = obj$.pipe(
121 | qAsk(SetProperties, {
122 | qInfo: { qType: "test" },
123 | foo: "baz"
124 | }),
125 | take(1)
126 | );
127 |
128 | setProps$.subscribe({
129 | complete: () => {
130 | expect(testFn).to.have.been.called.once;
131 | done();
132 | }
133 | });
134 | });
135 |
136 | it("should start with an invalidation when passed true", done => {
137 | const obj$ = app$.pipe(
138 | qAskReplay(CreateSessionObject, {
139 | qInfo: { qType: "test" },
140 | foo: "bar"
141 | })
142 | );
143 |
144 | const testFn = chai.spy();
145 | const invalidations$ = obj$.pipe(
146 | invalidations(true),
147 | tap(testFn),
148 | publish()
149 | );
150 |
151 | invalidations$.connect();
152 |
153 | // update app props
154 | const setProps$ = obj$.pipe(
155 | qAsk(SetProperties, {
156 | qInfo: { qType: "test" },
157 | foo: "baz"
158 | }),
159 | take(1)
160 | );
161 |
162 | setProps$.subscribe({
163 | complete: () => {
164 | expect(testFn).to.have.been.called.twice;
165 | done();
166 | }
167 | });
168 | });
169 |
170 | it("should throw an error when called on an Observable that doesn't emit a Handle", done => {
171 | of(null)
172 | .pipe(invalidations())
173 | .subscribe({
174 | error: err => {
175 | done();
176 | }
177 | });
178 | });
179 | });
180 |
181 | after(function(done) {
182 | container$.subscribe(container =>
183 | container.kill((err, result) => {
184 | container.remove();
185 | done();
186 | })
187 | );
188 | });
189 | });
190 | }
191 |
192 | module.exports = testUtilityOperators;
193 |
--------------------------------------------------------------------------------
/test/e2e/genericObject.test.js:
--------------------------------------------------------------------------------
1 | const chai = require("chai");
2 | chai.use(require("chai-generator"));
3 | const expect = chai.expect;
4 |
5 | var createContainer = require("../util/create-container");
6 | var {
7 | publish,
8 | publishReplay,
9 | refCount,
10 | shareReplay,
11 | switchMap,
12 | take,
13 | tap,
14 | withLatestFrom
15 | } = require("rxjs/operators");
16 |
17 | var { OpenDoc } = require("../../dist/global");
18 | var { connectSession } = require("../../dist");
19 | var Handle = require("../../dist/_cjs/handle");
20 |
21 | var { CreateSessionObject, GetObject } = require("../../dist/doc");
22 | var {
23 | ApplyPatches,
24 | GetLayout,
25 | GetProperties,
26 | SetProperties
27 | } = require("../../dist/genericObject");
28 |
29 | var { port, image } = require("./config.json");
30 |
31 | // launch a new container
32 | var container$ = createContainer(image, port);
33 |
34 | var eng$ = container$.pipe(
35 | switchMap(() => {
36 | return connectSession({
37 | host: "localhost",
38 | port: port,
39 | isSecure: false
40 | }).global$;
41 | }),
42 | publishReplay(1),
43 | refCount()
44 | );
45 |
46 | const app$ = eng$.pipe(
47 | switchMap(handle => handle.ask(OpenDoc, "iris.qvf")),
48 | publishReplay(1),
49 | refCount()
50 | );
51 |
52 | const obj$ = app$.pipe(
53 | switchMap(handle =>
54 | handle.ask(CreateSessionObject, {
55 | qInfo: {
56 | qType: "test-e2e"
57 | },
58 | metric: {
59 | qValueExpression: "=1+1"
60 | },
61 | foo: "bar"
62 | })
63 | ),
64 | shareReplay(1)
65 | );
66 |
67 | function testGenericObject() {
68 | describe("GenericObject Class", function() {
69 | before(function(done) {
70 | this.timeout(10000);
71 | container$.subscribe(() => done());
72 | });
73 |
74 | describe("GetProperties", function() {
75 | const objProps$ = obj$.pipe(
76 | switchMap(h => h.ask(GetProperties)),
77 | shareReplay(1)
78 | );
79 |
80 | it("should return an object", function(done) {
81 | objProps$.subscribe(props => {
82 | expect(props).to.be.a("object");
83 | done();
84 | });
85 | });
86 |
87 | it("should have a property called 'qInfo' that is an object", function(done) {
88 | objProps$.subscribe(props => {
89 | expect(props.qInfo).to.be.a("object");
90 | done();
91 | });
92 | });
93 |
94 | it("should have a property called 'foo' that equals 'bar'", function(done) {
95 | objProps$.subscribe(props => {
96 | expect(props.foo).to.equal("bar");
97 | done();
98 | });
99 | });
100 | });
101 |
102 | describe("SetProperties", function() {
103 | const objProps$ = obj$.pipe(
104 | switchMap(h => h.ask(GetProperties)),
105 | shareReplay(1)
106 | );
107 |
108 | const updatedObjProps$ = obj$.pipe(
109 | switchMap(h =>
110 | h.ask(SetProperties, {
111 | qInfo: {
112 | qType: "test-e2e"
113 | },
114 | metric: {
115 | qValueExpression: "=1+1"
116 | },
117 | foo: "baz"
118 | })
119 | ),
120 | switchMap(() => objProps$)
121 | );
122 |
123 | it("should set the property 'foo' to equal 'baz'", function(done) {
124 | updatedObjProps$.subscribe(props => {
125 | expect(props.foo).to.equal("baz");
126 | done();
127 | });
128 | });
129 | });
130 |
131 | describe("GetLayout", function() {
132 | const layout$ = obj$.pipe(
133 | switchMap(h => h.ask(GetLayout)),
134 | shareReplay(1)
135 | );
136 |
137 | it("should return an object", function(done) {
138 | layout$.subscribe(layout => {
139 | expect(layout).to.be.a("object");
140 | done();
141 | });
142 | });
143 |
144 | it("should have a property 'metric' that equals 2", function(done) {
145 | layout$.subscribe(layout => {
146 | expect(layout.metric).to.equal(2);
147 | done();
148 | });
149 | });
150 | });
151 |
152 | describe("ApplyPatches", function() {
153 | const patches$ = obj$.pipe(
154 | switchMap(h =>
155 | h.ask(ApplyPatches, [
156 | {
157 | qOp: "add",
158 | qPath: "/patch",
159 | qValue: "1"
160 | }
161 | ])
162 | ),
163 | publish()
164 | );
165 |
166 | it("should cause an invalidation", function(done) {
167 | obj$
168 | .pipe(
169 | switchMap(h => h.invalidated$),
170 | take(1)
171 | )
172 | .subscribe(i => {
173 | done();
174 | });
175 |
176 | patches$.connect();
177 | });
178 |
179 | it("should create a new property called 'patch' with a value of 1", function(done) {
180 | obj$
181 | .pipe(
182 | switchMap(h => h.invalidated$),
183 | switchMap(h => h.ask(GetLayout)),
184 | take(1)
185 | )
186 | .subscribe(layout => {
187 | expect(layout.patch).to.equal(1);
188 | done();
189 | });
190 |
191 | patches$.connect();
192 | });
193 | });
194 |
195 | after(function(done) {
196 | container$.subscribe(container =>
197 | container.kill((err, result) => {
198 | container.remove();
199 | done();
200 | })
201 | );
202 | });
203 | });
204 | }
205 |
206 | module.exports = testGenericObject;
207 |
--------------------------------------------------------------------------------
/test/e2e/doc.test.js:
--------------------------------------------------------------------------------
1 | const chai = require("chai");
2 | chai.use(require("chai-generator"));
3 | const expect = chai.expect;
4 |
5 | var createContainer = require("../util/create-container");
6 | var {
7 | publishReplay,
8 | refCount,
9 | shareReplay,
10 | switchMap,
11 | take,
12 | withLatestFrom
13 | } = require("rxjs/operators");
14 |
15 | var { OpenDoc } = require("../../dist/global");
16 | var { connectSession } = require("../../dist");
17 | var Handle = require("../../dist/_cjs/handle");
18 |
19 | var {
20 | CreateObject,
21 | CreateSessionObject,
22 | GetAppProperties,
23 | GetObject,
24 | SetAppProperties
25 | } = require("../../dist/doc");
26 |
27 | var { port, image } = require("./config.json");
28 |
29 | // launch a new container
30 | var container$ = createContainer(image, port);
31 |
32 | var eng$ = container$.pipe(
33 | switchMap(() => {
34 | return connectSession({
35 | host: "localhost",
36 | port: port,
37 | isSecure: false
38 | }).global$;
39 | }),
40 | publishReplay(1),
41 | refCount()
42 | );
43 |
44 | const app$ = eng$.pipe(
45 | switchMap(handle => handle.ask(OpenDoc, "iris.qvf")),
46 | publishReplay(1),
47 | refCount()
48 | );
49 |
50 | function testDoc() {
51 | describe("Doc Class", function() {
52 | before(function(done) {
53 | this.timeout(10000);
54 | container$.subscribe(() => done());
55 | });
56 |
57 | describe("GetAppProperties", function() {
58 | const appProps$ = app$.pipe(
59 | switchMap(handle => handle.ask(GetAppProperties)),
60 | publishReplay(1),
61 | refCount()
62 | );
63 |
64 | it("should return an object", function(done) {
65 | appProps$.subscribe(props => {
66 | expect(props).to.be.a("object");
67 | done();
68 | });
69 | });
70 |
71 | it("should have a property 'qTitle' that equals 'Iris'", function(done) {
72 | appProps$.subscribe(props => {
73 | expect(props.qTitle).to.equal("Iris");
74 | done();
75 | });
76 | });
77 | });
78 |
79 | describe("setAppProperties", function() {
80 | const appProps$ = app$.pipe(
81 | switchMap(handle => handle.ask(GetAppProperties)),
82 | publishReplay(1),
83 | refCount()
84 | );
85 |
86 | const updatedAppProps$ = appProps$.pipe(
87 | take(1),
88 | withLatestFrom(app$),
89 | switchMap(([props, handle]) => {
90 | const newProps = Object.assign({ foo: "bar" }, props);
91 | return handle.ask(SetAppProperties, newProps);
92 | }),
93 | switchMap(() =>
94 | app$.pipe(
95 | switchMap(handle => handle.ask(GetAppProperties)),
96 | publishReplay(1),
97 | refCount()
98 | )
99 | )
100 | );
101 |
102 | it("should add a property 'foo' that equals 'bar'", function(done) {
103 | updatedAppProps$.subscribe(props => {
104 | expect(props.foo).to.equal("bar");
105 | done();
106 | });
107 | });
108 | });
109 |
110 | describe("getObject", function() {
111 | const obj$ = app$.pipe(
112 | switchMap(h => h.ask(GetObject, "fpZbty")),
113 | shareReplay(1)
114 | );
115 |
116 | it("should return a Handle of type 'GenericObject'", function(done) {
117 | obj$.subscribe(h => {
118 | expect(h).to.be.instanceof(Handle);
119 | expect(h.qClass).to.equal("GenericObject");
120 | done();
121 | });
122 | });
123 | });
124 |
125 | describe("createSessionObject", function() {
126 | const obj$ = app$.pipe(
127 | switchMap(h =>
128 | h.ask(CreateSessionObject, {
129 | qInfo: {
130 | qType: "e2e-test"
131 | }
132 | })
133 | ),
134 | shareReplay(1)
135 | );
136 |
137 | it("should return a Handle of type 'GenericObject'", function(done) {
138 | obj$.subscribe(h => {
139 | expect(h).to.be.instanceof(Handle);
140 | expect(h.qClass).to.equal("GenericObject");
141 | done();
142 | });
143 | });
144 | });
145 |
146 | describe("createObject", function() {
147 | const obj$ = app$.pipe(
148 | switchMap(h =>
149 | h.ask(CreateObject, {
150 | qInfo: {
151 | qType: "e2e-test"
152 | }
153 | })
154 | ),
155 | shareReplay(1)
156 | );
157 |
158 | it("should return a Handle of type 'GenericObject'", function(done) {
159 | obj$.subscribe(h => {
160 | expect(h).to.be.instanceof(Handle);
161 | expect(h.qClass).to.equal("GenericObject");
162 | done();
163 | });
164 | });
165 | });
166 |
167 | describe("invalidation", function() {
168 | it("should receive an invalidation event when properties change", function(done) {
169 | // Listen for invalidation event
170 | app$.subscribe(h => {
171 | h.invalidated$.pipe(take(1)).subscribe(i => {
172 | done();
173 | });
174 | });
175 |
176 | // Trigger invalidation event by changing app events
177 | const appProps$ = app$.pipe(
178 | switchMap(handle => handle.ask(GetAppProperties)),
179 | publishReplay(1),
180 | refCount()
181 | );
182 |
183 | appProps$
184 | .pipe(
185 | take(1),
186 | withLatestFrom(app$),
187 | switchMap(([props, handle]) => {
188 | const newProps = Object.assign({ test: "invalid" }, props);
189 | return handle.ask(SetAppProperties, newProps);
190 | })
191 | )
192 | .subscribe();
193 | });
194 | });
195 |
196 | after(function(done) {
197 | container$.subscribe(container =>
198 | container.kill((err, result) => {
199 | container.remove();
200 | done();
201 | })
202 | );
203 | });
204 | });
205 | }
206 |
207 | module.exports = testDoc;
208 |
--------------------------------------------------------------------------------
/test/e2e/notification.test.js:
--------------------------------------------------------------------------------
1 | const chai = require("chai");
2 | chai.use(require("chai-generator"));
3 | const expect = chai.expect;
4 |
5 | var createContainer = require("../util/create-container");
6 | var {
7 | publish,
8 | publishReplay,
9 | refCount,
10 | shareReplay,
11 | switchMap,
12 | map,
13 | tap,
14 | take,
15 | takeUntil,
16 | withLatestFrom,
17 | filter,
18 | reduce,
19 | skip
20 | } = require("rxjs/operators");
21 | var { Subject } = require("rxjs");
22 |
23 | var { EngineVersion, OpenDoc } = require("../../dist/global");
24 | var { connectSession } = require("../../dist");
25 |
26 | var { GetAppProperties, SetAppProperties } = require("../../dist/doc");
27 |
28 | var { port, image } = require("./config.json");
29 |
30 | // launch a new container
31 | var container$ = createContainer(image, port);
32 |
33 | var session$ = container$.pipe(
34 | map(() =>
35 | connectSession({
36 | host: "localhost",
37 | port: port,
38 | isSecure: false
39 | })
40 | ),
41 | publishReplay(1),
42 | refCount()
43 | );
44 |
45 | const eng$ = session$.pipe(switchMap(session => session.global$));
46 |
47 | const app$ = eng$.pipe(
48 | switchMap(handle => handle.ask(OpenDoc, "iris.qvf")),
49 | publishReplay(1),
50 | refCount()
51 | );
52 |
53 | const notifications$ = session$.pipe(
54 | switchMap(session => session.notifications$)
55 | );
56 |
57 | function testNotification() {
58 | describe("Notifications", function() {
59 | before(function(done) {
60 | this.timeout(10000);
61 | app$.subscribe(() => done());
62 | });
63 |
64 | it("should emit any traffic sent", function(done) {
65 | this.timeout(10000);
66 | const session$ = container$.pipe(
67 | map(() => {
68 | return connectSession({
69 | host: "localhost",
70 | port: port,
71 | isSecure: false
72 | });
73 | }),
74 | publishReplay(1),
75 | refCount()
76 | );
77 |
78 | const eng$ = session$.pipe(switchMap(session => session.global$));
79 |
80 | // after app opened, start listening on notifications
81 | session$
82 | .pipe(
83 | switchMap(session => session.notifications$),
84 | filter(f => f.type === "traffic:sent"),
85 | skip(1), // skip the opening message
86 | take(1)
87 | )
88 | .subscribe(req => {
89 | expect(req.data.method).to.equal("EngineVersion");
90 | done();
91 | });
92 |
93 | // After app has been opened, send a call
94 | eng$
95 | .pipe(
96 | switchMap(h => h.ask(EngineVersion)),
97 | publish()
98 | )
99 | .connect();
100 | });
101 |
102 | it("should emit any traffic received", function(done) {
103 | this.timeout(10000);
104 | const session$ = container$.pipe(
105 | map(() => {
106 | return connectSession({
107 | host: "localhost",
108 | port: port,
109 | isSecure: false
110 | });
111 | }),
112 | publishReplay(1),
113 | refCount()
114 | );
115 |
116 | const eng$ = session$.pipe(switchMap(session => session.global$));
117 | const notifications$ = session$.pipe(
118 | switchMap(session => session.notifications$)
119 | );
120 |
121 | const msgId$ = notifications$.pipe(
122 | filter(
123 | f => (f.type === "traffic:sent") & (f.data.method === "EngineVersion")
124 | ),
125 | map(req => req.data.id)
126 | );
127 |
128 | notifications$
129 | .pipe(
130 | filter(f => f.type === "traffic:received"),
131 | withLatestFrom(msgId$),
132 | take(1)
133 | )
134 | .subscribe(([resp, reqId]) => {
135 | expect(resp.data.id).to.equal(reqId);
136 | done();
137 | });
138 |
139 | // After app has been opened, send a call
140 | eng$
141 | .pipe(
142 | switchMap(h => h.ask(EngineVersion)),
143 | publish()
144 | )
145 | .connect();
146 | });
147 |
148 | it("should emit the suspended status", function(done) {
149 | notifications$
150 | .pipe(
151 | filter(f => f.type === "traffic:suspend-status"),
152 | map(f => f.data),
153 | take(3),
154 | reduce((acc, curr) => [...acc, curr], [])
155 | )
156 | .subscribe(statusHistory => {
157 | expect(statusHistory[0]).to.equal(false);
158 | expect(statusHistory[1]).to.equal(true);
159 | expect(statusHistory[2]).to.equal(false);
160 | done();
161 | });
162 |
163 | app$.subscribe(h => {
164 | h.session.suspended$.next(true);
165 | h.session.suspended$.next(false);
166 | });
167 | });
168 |
169 | it("should emit the changes that the session passes down", function(done) {
170 | const setAppProps$ = app$.pipe(
171 | switchMap(handle => handle.ask(GetAppProperties)),
172 | take(1),
173 | withLatestFrom(app$),
174 | switchMap(([props, handle]) => {
175 | const newProps = Object.assign({ test: "invalid" }, props);
176 | return handle.ask(SetAppProperties, newProps);
177 | }),
178 | publish()
179 | );
180 |
181 | const appHandle$ = app$.pipe(map(h => h.handle));
182 |
183 | notifications$
184 | .pipe(
185 | filter(f => f.type === "traffic:change"),
186 | withLatestFrom(appHandle$)
187 | )
188 | .subscribe(([notification, handle]) => {
189 | const didHandleChange = notification.data.indexOf(handle) > -1;
190 | expect(didHandleChange).to.equal(true);
191 | done();
192 | });
193 |
194 | setAppProps$.connect();
195 | });
196 |
197 | it("should emit a close event when the session socket closes", function(done) {
198 | const session$ = container$.pipe(
199 | map(() => {
200 | return connectSession({
201 | host: "localhost",
202 | port: port,
203 | isSecure: false
204 | });
205 | }),
206 | publishReplay(1),
207 | refCount()
208 | );
209 |
210 | const eng$ = session$.pipe(switchMap(session => session.global$));
211 | const notifications$ = session$.pipe(
212 | switchMap(session => session.notifications$)
213 | );
214 |
215 | const ws$ = eng$.pipe(switchMap(h => h.session.ws$));
216 |
217 | notifications$
218 | .pipe(
219 | filter(f => f.type === "socket:close"),
220 | map(m => m.data),
221 | take(1)
222 | )
223 | .subscribe(evt => {
224 | expect(evt.type).to.equal("close");
225 | done();
226 | });
227 |
228 | ws$.subscribe(ws => ws.close());
229 | });
230 |
231 | after(function(done) {
232 | container$.subscribe(container =>
233 | container.kill((err, result) => {
234 | container.remove();
235 | done();
236 | })
237 | );
238 | });
239 | });
240 | }
241 |
242 | module.exports = testNotification;
243 |
--------------------------------------------------------------------------------
/test/e2e/delta.test.js:
--------------------------------------------------------------------------------
1 | const chai = require("chai");
2 | chai.use(require("chai-generator"));
3 | const expect = chai.expect;
4 |
5 | var createContainer = require("../util/create-container");
6 | var {
7 | shareReplay,
8 | switchMap,
9 | filter,
10 | map,
11 | pairwise,
12 | take,
13 | withLatestFrom
14 | } = require("rxjs/operators");
15 | var { combineLatest } = require("rxjs");
16 |
17 | var { EngineVersion, OpenDoc, IsDesktopMode } = require("../../dist/global");
18 | var { GetAppProperties, SetAppProperties } = require("../../dist/Doc");
19 | var { connectSession, qAsk, qAskReplay, invalidations } = require("../../dist");
20 |
21 | var { port, image } = require("./config.json");
22 |
23 | // launch a new container
24 | var container$ = createContainer(image, port);
25 |
26 | const session$ = container$.pipe(
27 | map(() => {
28 | return connectSession({
29 | host: "localhost",
30 | port: port,
31 | isSecure: false,
32 | delta: true
33 | });
34 | }),
35 | shareReplay(1)
36 | );
37 |
38 | const eng$ = session$.pipe(switchMap(session => session.global$));
39 |
40 | const sessionSelectiveDelta$ = container$.pipe(
41 | map(() => {
42 | return connectSession({
43 | host: "localhost",
44 | port: port,
45 | isSecure: false,
46 | delta: {
47 | Global: ["IsDesktopMode"]
48 | }
49 | });
50 | }),
51 | shareReplay(1)
52 | );
53 |
54 | const engSelectiveDelta$ = sessionSelectiveDelta$.pipe(
55 | switchMap(session => session.global$)
56 | );
57 |
58 | function testDelta() {
59 | describe("Delta Mode", function() {
60 | before(function(done) {
61 | this.timeout(10000);
62 | container$.subscribe(() => done());
63 | });
64 |
65 | describe("Global Delta", function() {
66 | const ev$ = eng$.pipe(switchMap(handle => handle.ask(EngineVersion)));
67 |
68 | const notifications$ = session$.pipe(
69 | switchMap(session => session.notifications$)
70 | );
71 |
72 | describe("Engine Version", function() {
73 | it("should return an object with property qComponentVersion", done => {
74 | ev$.subscribe(ev => {
75 | expect(ev).to.have.property("qComponentVersion");
76 | done();
77 | });
78 | });
79 |
80 | it("should return the object when called a second time after receiving an empty patch from the engine", done => {
81 | const receivedPatch$ = notifications$.pipe(
82 | filter(notification => notification.type === "traffic:received"),
83 | map(notification => notification.data.result.qVersion),
84 | take(1)
85 | );
86 |
87 | combineLatest(ev$, receivedPatch$).subscribe(([ev, patch]) => {
88 | expect(ev).to.have.property("qComponentVersion");
89 | expect(patch.length).to.equal(0);
90 | done();
91 | });
92 | });
93 | });
94 |
95 | const doc$ = eng$.pipe(
96 | switchMap(handle => handle.ask(OpenDoc, "iris.qvf"))
97 | );
98 |
99 | describe("Getting a Doc Handle", function() {
100 | it("should return a handle with qClass 'Doc'", done => {
101 | doc$.subscribe(handle => {
102 | expect(handle.qClass).to.equal("Doc");
103 | done();
104 | });
105 | });
106 |
107 | it("should return the handle with qClass 'Doc' a second time", done => {
108 | doc$.subscribe(handle => {
109 | expect(handle.qClass).to.equal("Doc");
110 | done();
111 | });
112 | });
113 | });
114 |
115 | describe("Updating Props", function() {
116 | it("should return the app properties", done => {
117 | const appProps$ = doc$.pipe(
118 | switchMap(handle => handle.ask(GetAppProperties))
119 | );
120 |
121 | appProps$.subscribe(props => {
122 | expect(props.foo).to.be.undefined;
123 | done();
124 | });
125 | });
126 |
127 | it("should return updated app properties after a change is made", done => {
128 | const getAppProps$ = doc$.pipe(
129 | switchMap(handle => handle.ask(GetAppProperties))
130 | );
131 |
132 | getAppProps$
133 | .pipe(
134 | switchMap(props =>
135 | doc$.pipe(
136 | switchMap(handle =>
137 | handle.ask(SetAppProperties, { ...props, foo: "bar" })
138 | )
139 | )
140 | ),
141 | switchMap(() => getAppProps$)
142 | )
143 | .subscribe(props => {
144 | expect(props.foo).to.equal("bar");
145 | done();
146 | });
147 | });
148 | });
149 | describe("Patch Immutability", function() {
150 | it("should not mutate target paths and keep non-patched paths", done => {
151 | const testObj = doc$.pipe(
152 | qAskReplay("CreateSessionObject", {
153 | qInfo: { qType: "test" },
154 | foo: {
155 | a: 1
156 | },
157 | bar: {
158 | b: 2
159 | }
160 | })
161 | );
162 |
163 | const layouts = testObj.pipe(
164 | invalidations(true),
165 | qAsk("GetLayout"),
166 | pairwise(),
167 | take(2)
168 | );
169 |
170 | layouts.subscribe(([prevLayout, currLayout]) => {
171 | expect(prevLayout.foo).to.equal(currLayout.foo);
172 | expect(prevLayout.bar).to.not.equal(currLayout.bar);
173 | done();
174 | });
175 |
176 | testObj
177 | .pipe(
178 | qAsk("SetProperties", {
179 | qInfo: { qType: "test" },
180 | foo: {
181 | a: 1
182 | },
183 | bar: {
184 | b: 3
185 | }
186 | }),
187 | take(1)
188 | )
189 | .subscribe();
190 | });
191 | });
192 | });
193 |
194 | describe("Selective Delta", function() {
195 | const selectiveNotifications$ = sessionSelectiveDelta$.pipe(
196 | switchMap(session => session.notifications$),
197 | filter(f => f.type === "traffic:received")
198 | );
199 |
200 | it("should not use delta for EngineVersion", done => {
201 | const ev$ = engSelectiveDelta$.pipe(
202 | switchMap(handle => handle.ask(EngineVersion))
203 | );
204 |
205 | ev$
206 | .pipe(
207 | withLatestFrom(selectiveNotifications$),
208 | take(1)
209 | )
210 | .subscribe(([response, notification]) => {
211 | expect(notification.data.delta).to.be.undefined;
212 | done();
213 | });
214 | });
215 |
216 | it("should use delta for IsDesktopMode", done => {
217 | const isDesktop$ = engSelectiveDelta$.pipe(
218 | switchMap(handle => handle.ask(IsDesktopMode))
219 | );
220 |
221 | isDesktop$
222 | .pipe(
223 | withLatestFrom(selectiveNotifications$),
224 | take(1)
225 | )
226 | .subscribe(([response, notification]) => {
227 | expect(notification.data.delta).to.equal(true);
228 | done();
229 | });
230 | });
231 | });
232 |
233 | after(function(done) {
234 | container$.subscribe(container =>
235 | container.kill((err, result) => {
236 | container.remove();
237 | done();
238 | })
239 | );
240 | });
241 | });
242 | }
243 |
244 | module.exports = testDelta;
245 |
--------------------------------------------------------------------------------
/src/session.js:
--------------------------------------------------------------------------------
1 | import {
2 | Observable,
3 | Subject,
4 | BehaviorSubject,
5 | of as $of,
6 | throwError as $throw,
7 | merge,
8 | concat
9 | } from "rxjs";
10 |
11 | import {
12 | groupBy,
13 | partition,
14 | publishLast,
15 | refCount,
16 | map,
17 | withLatestFrom,
18 | publish,
19 | filter,
20 | mergeMap,
21 | concatMap,
22 | take,
23 | mapTo,
24 | distinctUntilChanged,
25 | pluck,
26 | switchMap,
27 | takeUntil,
28 | ignoreElements,
29 | tap,
30 | scan
31 | } from "rxjs/operators";
32 |
33 | import Handle from "./handle";
34 | import connectWS from "./util/connectWS";
35 | import { applyPatches } from "immer";
36 |
37 | export default class Session {
38 | constructor(config) {
39 | const session = this;
40 |
41 | // closed signal
42 | this.closed$ = new Subject();
43 |
44 | // delta mode
45 | const delta = config.delta || false;
46 |
47 | // Suspended changes state
48 | const suspended$ = new BehaviorSubject(false);
49 |
50 | // Connect WS
51 | const ws$ = Observable.create(observer => {
52 | // If they supplied a WebSocket, use it. Otherwise, build one
53 | if (typeof config.ws !== "undefined") {
54 | observer.next(config.ws);
55 | observer.complete();
56 | } else {
57 | var ws = connectWS(config);
58 |
59 | ws.addEventListener("open", evt => {
60 | observer.next(ws);
61 | observer.complete();
62 | });
63 | }
64 |
65 | return;
66 | }).pipe(
67 | publishLast(),
68 | refCount()
69 | );
70 |
71 | // On close signal, execute side effect to close the websocket
72 | this.closed$
73 | .pipe(
74 | withLatestFrom(ws$),
75 | take(1)
76 | )
77 | .subscribe(([close, ws]) => ws.close());
78 |
79 | // WebSocket close events
80 | const wsClose$ = ws$.pipe(
81 | switchMap(ws =>
82 | Observable.create(observer => {
83 | ws.addEventListener("close", evt => {
84 | observer.next(evt);
85 | observer.complete();
86 | });
87 | })
88 | ),
89 | // Side effects when websocket gets closed
90 | tap(() => {
91 | // complete the requests stream
92 | requests$.complete();
93 | // complete the suspended stream
94 | suspended$.complete();
95 | })
96 | );
97 |
98 | // Requests
99 | const requests$ = new Subject();
100 |
101 | // Hook in request pipeline
102 | requests$
103 | .pipe(
104 | withLatestFrom(ws$),
105 | takeUntil(wsClose$)
106 | )
107 | .subscribe(([req, ws]) => {
108 | const request = {
109 | id: req.id,
110 | handle: req.handle,
111 | method: req.method,
112 | params: req.params
113 | };
114 |
115 | // Set delta if necessary
116 | if (typeof config.delta === "object") {
117 | const overrides = config.delta[req.qClass] || [];
118 | if (overrides.indexOf(req.method) > -1) {
119 | request.delta = true;
120 | }
121 | } else if (config.delta === true) {
122 | request.delta = true;
123 | }
124 |
125 | ws.send(JSON.stringify(request));
126 | });
127 |
128 | // Responses
129 | const responses$ = ws$.pipe(
130 | concatMap(ws =>
131 | Observable.create(observer => {
132 | ws.addEventListener("message", evt => {
133 | const response = JSON.parse(evt.data);
134 | observer.next(response);
135 | });
136 |
137 | ws.addEventListener("error", err => {
138 | observer.error(err);
139 | });
140 |
141 | ws.addEventListener("close", function() {
142 | observer.complete();
143 | });
144 | })
145 | ),
146 | publish(),
147 | refCount()
148 | );
149 |
150 | // Link responses with requests
151 | const responsesWithRequest$ = requests$.pipe(
152 | // this prevents errors from going through the delta processing chain. is that appropriate?
153 | filter(response => !response.hasOwnProperty("error")),
154 | mergeMap(req =>
155 | responses$.pipe(
156 | filter(response => req.id === response.id),
157 | take(1),
158 | map(response => ({
159 | request: req,
160 | response: response
161 | }))
162 | )
163 | )
164 | );
165 |
166 | // Responses with errors
167 | const errorResponses$ = responses$.pipe(
168 | filter(resp => resp.hasOwnProperty("error"))
169 | );
170 |
171 | // Split direct responses from delta responses
172 | const [directResponse$, deltaResponses$] = responsesWithRequest$.pipe(
173 | filter(reqResp => !reqResp.response.hasOwnProperty("error")),
174 | partition(reqResp => !reqResp.response.delta)
175 | );
176 |
177 | // Apply JSON Patching to delta responses
178 | const deltaResponsesCalculated$ = deltaResponses$.pipe(
179 | groupBy(
180 | reqResp => `${reqResp.request.handle} - ${reqResp.request.method}`
181 | ),
182 | mergeMap(grouped$ =>
183 | grouped$.pipe(
184 | scan(
185 | (acc, reqResp) => {
186 | const { response } = reqResp;
187 | const resultKeys = Object.keys(response.result);
188 | const patches = resultKeys.reduce((acc, key) => {
189 | const keyPatches = response.result[key];
190 | const transformedPatches = keyPatches.map(patch => {
191 | // Keep in mind that engine patch root path is out of compliance with JSON-Pointer spec used by JSON-Patch spec for "/"
192 | // https://tools.ietf.org/html/rfc6902#page-3
193 | // https://tools.ietf.org/html/rfc6901#section-5
194 | const arrayPath = patch.path.split("/").filter(s => s !== "");
195 | return {
196 | ...patch,
197 | path: [key, ...arrayPath]
198 | };
199 | });
200 |
201 | return [...acc, ...transformedPatches];
202 | }, []);
203 | return {
204 | ...reqResp,
205 | response: {
206 | ...reqResp.response,
207 | result: applyPatches(acc.response.result, patches)
208 | }
209 | };
210 | },
211 | {
212 | response: {
213 | result: {}
214 | }
215 | }
216 | )
217 | )
218 | )
219 | );
220 |
221 | // Merge the direct and delta responses back together and parse them
222 | const mappedResponses$ = merge(
223 | directResponse$.pipe(map(reqResp => reqResp.response)),
224 | deltaResponsesCalculated$.pipe(map(reqResp => reqResp.response))
225 | ).pipe(
226 | map(response => {
227 | const result = response.result;
228 | const resultKeys = Object.keys(result);
229 | if (
230 | result.hasOwnProperty("qReturn") &&
231 | result.qReturn.hasOwnProperty("qHandle")
232 | ) {
233 | return {
234 | id: response.id,
235 | result: new Handle(
236 | this,
237 | result.qReturn.qHandle,
238 | result.qReturn.qType
239 | )
240 | };
241 | } else if (resultKeys.length === 1) {
242 | return {
243 | id: response.id,
244 | result: result[resultKeys[0]]
245 | };
246 | } else {
247 | return response;
248 | }
249 | })
250 | );
251 |
252 | // Publish response stream
253 | const finalResponse$ = merge(mappedResponses$, errorResponses$).pipe(
254 | publish()
255 | );
256 |
257 | // Connect the response stream
258 | const finalResponseSub = finalResponse$.connect();
259 |
260 | // Changes
261 | const changesIn$ = responses$.pipe(
262 | filter(f => f.hasOwnProperty("change")),
263 | pluck("change")
264 | );
265 |
266 | // Buffer changes during suspends
267 | const changes$ = changesIn$.pipe(bufferInvalids(suspended$));
268 |
269 | // Session Notifications
270 | const notifications$ = merge(
271 | requests$.pipe(
272 | map(req => ({
273 | type: "traffic:sent",
274 | data: req
275 | }))
276 | ),
277 | responses$.pipe(
278 | map(resp => ({
279 | type: "traffic:received",
280 | data: resp
281 | }))
282 | ),
283 | changes$.pipe(
284 | map(changes => ({
285 | type: "traffic:change",
286 | data: changes
287 | }))
288 | ),
289 | suspended$.pipe(
290 | map(suspend => ({
291 | type: "traffic:suspend-status",
292 | data: suspend
293 | }))
294 | ),
295 | wsClose$.pipe(
296 | map(evt => ({
297 | type: "socket:close",
298 | data: evt
299 | }))
300 | )
301 | );
302 |
303 | // Sequence generator
304 | this.seqGen = (function*() {
305 | var index = 1;
306 | while (true) yield index++;
307 | })();
308 |
309 | Object.assign(this, {
310 | ws$,
311 | requests$,
312 | finalResponse$,
313 | changes$,
314 | suspended$,
315 | notifications$,
316 | delta
317 | });
318 | }
319 |
320 | ask(action) {
321 | const requestId = this.seqGen.next().value;
322 |
323 | const request = {
324 | id: requestId,
325 | jsonrpc: "2.0",
326 | handle: action.handle,
327 | method: action.method,
328 | params: action.params,
329 | qClass: action.qClass
330 | };
331 |
332 | this.requests$.next(request);
333 |
334 | return this.finalResponse$.pipe(
335 | filter(r => r.id === requestId),
336 | mergeMap(m => {
337 | if (m.hasOwnProperty("error")) {
338 | return $throw(m.error);
339 | } else {
340 | return $of(m);
341 | }
342 | }),
343 | map(m => m.result),
344 | take(1)
345 | );
346 | }
347 |
348 | global() {
349 | const globalHandle = new Handle(this, -1, "Global");
350 |
351 | // ask for a sample call to test that we are authenticated properly, then either pass global or pass the error
352 | return this.ws$.pipe(
353 | switchMap(() =>
354 | this.ask({
355 | handle: -1,
356 | method: "GetUniqueID",
357 | params: [],
358 | qClass: "Global"
359 | })
360 | ),
361 | mapTo(globalHandle),
362 | publishLast(),
363 | refCount()
364 | );
365 | }
366 |
367 | close() {
368 | this.closed$.next(null);
369 | }
370 | }
371 |
372 | function bufferInvalids(status$) {
373 | return function(src$) {
374 | const values = [];
375 | const directStream$ = new Observable.create(observer => {
376 | return src$.pipe(withLatestFrom(status$)).subscribe(
377 | ([val, status]) => {
378 | if (status) {
379 | values.push(...val);
380 | } else {
381 | observer.next(val);
382 | }
383 | },
384 | err => observer.error(err),
385 | () => observer.complete()
386 | );
387 | });
388 |
389 | const bufferStream$ = status$.pipe(
390 | distinctUntilChanged(),
391 | filter(f => !f),
392 | map(() => values),
393 | filter(f => f.length > 0),
394 | takeUntil(concat(src$.pipe(ignoreElements()), $of(undefined)))
395 | );
396 |
397 | return merge(directStream$, bufferStream$);
398 | };
399 | }
400 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright (c) 2016-2018 Speros Kokenes
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/src/schema/schema.js:
--------------------------------------------------------------------------------
1 | export default {
2 | "Field": {
3 | "GetCardinal": [
4 | "qReturn"
5 | ],
6 | "GetAndMode": [
7 | "qReturn"
8 | ],
9 | "SelectValues": [
10 | "qReturn"
11 | ],
12 | "Select": [
13 | "qReturn"
14 | ],
15 | "ToggleSelect": [
16 | "qReturn"
17 | ],
18 | "ClearAllButThis": [
19 | "qReturn"
20 | ],
21 | "SelectPossible": [
22 | "qReturn"
23 | ],
24 | "SelectExcluded": [
25 | "qReturn"
26 | ],
27 | "SelectAll": [
28 | "qReturn"
29 | ],
30 | "Lock": [
31 | "qReturn"
32 | ],
33 | "Unlock": [
34 | "qReturn"
35 | ],
36 | "GetNxProperties": [
37 | "qProperties"
38 | ],
39 | "SetNxProperties": [],
40 | "SetAndMode": [],
41 | "SelectAlternative": [
42 | "qReturn"
43 | ],
44 | "LowLevelSelect": [
45 | "qReturn"
46 | ],
47 | "Clear": [
48 | "qReturn"
49 | ]
50 | },
51 | "Variable": {
52 | "GetContent": [
53 | "qContent"
54 | ],
55 | "GetRawContent": [
56 | "qReturn"
57 | ],
58 | "SetContent": [
59 | "qReturn"
60 | ],
61 | "ForceContent": [],
62 | "GetNxProperties": [
63 | "qProperties"
64 | ],
65 | "SetNxProperties": []
66 | },
67 | "GenericObject": {
68 | "GetLayout": [
69 | "qLayout"
70 | ],
71 | "GetListObjectData": [
72 | "qDataPages"
73 | ],
74 | "GetHyperCubeData": [
75 | "qDataPages"
76 | ],
77 | "GetHyperCubeReducedData": [
78 | "qDataPages"
79 | ],
80 | "GetHyperCubePivotData": [
81 | "qDataPages"
82 | ],
83 | "GetHyperCubeStackData": [
84 | "qDataPages"
85 | ],
86 | "GetHyperCubeContinuousData": [
87 | "qDataPages",
88 | "qAxisData"
89 | ],
90 | "GetHyperCubeTreeData": [
91 | "qNodes"
92 | ],
93 | "GetHyperCubeBinnedData": [
94 | "qDataPages"
95 | ],
96 | "ApplyPatches": [],
97 | "ClearSoftPatches": [],
98 | "SetProperties": [],
99 | "GetProperties": [
100 | "qProp"
101 | ],
102 | "GetEffectiveProperties": [
103 | "qProp"
104 | ],
105 | "SetFullPropertyTree": [],
106 | "GetFullPropertyTree": [
107 | "qPropEntry"
108 | ],
109 | "GetInfo": [
110 | "qInfo"
111 | ],
112 | "ClearSelections": [],
113 | "ExportData": [
114 | "qUrl",
115 | "qWarnings"
116 | ],
117 | "SelectListObjectValues": [
118 | "qSuccess"
119 | ],
120 | "SelectListObjectPossible": [
121 | "qSuccess"
122 | ],
123 | "SelectListObjectExcluded": [
124 | "qSuccess"
125 | ],
126 | "SelectListObjectAlternative": [
127 | "qSuccess"
128 | ],
129 | "SelectListObjectAll": [
130 | "qSuccess"
131 | ],
132 | "SelectListObjectContinuousRange": [
133 | "qSuccess"
134 | ],
135 | "SearchListObjectFor": [
136 | "qSuccess"
137 | ],
138 | "AbortListObjectSearch": [],
139 | "AcceptListObjectSearch": [],
140 | "ExpandLeft": [],
141 | "ExpandTop": [],
142 | "CollapseLeft": [],
143 | "CollapseTop": [],
144 | "DrillUp": [],
145 | "Lock": [],
146 | "Unlock": [],
147 | "SelectHyperCubeValues": [
148 | "qSuccess"
149 | ],
150 | "SelectHyperCubeCells": [
151 | "qSuccess"
152 | ],
153 | "SelectPivotCells": [
154 | "qSuccess"
155 | ],
156 | "RangeSelectHyperCubeValues": [
157 | "qSuccess"
158 | ],
159 | "MultiRangeSelectHyperCubeValues": [
160 | "qSuccess"
161 | ],
162 | "MultiRangeSelectTreeDataValues": [
163 | "qSuccess"
164 | ],
165 | "SelectHyperCubeContinuousRange": [
166 | "qSuccess"
167 | ],
168 | "GetChild": [
169 | "qReturn"
170 | ],
171 | "GetChildInfos": [
172 | "qInfos"
173 | ],
174 | "CreateChild": [
175 | "qInfo",
176 | "qReturn"
177 | ],
178 | "DestroyChild": [
179 | "qSuccess"
180 | ],
181 | "DestroyAllChildren": [],
182 | "SetChildArrayOrder": [],
183 | "GetLinkedObjects": [
184 | "qItems"
185 | ],
186 | "CopyFrom": [],
187 | "BeginSelections": [],
188 | "EndSelections": [],
189 | "ResetMadeSelections": [],
190 | "EmbedSnapshotObject": [],
191 | "GetSnapshotObject": [
192 | "qReturn"
193 | ],
194 | "Publish": [],
195 | "UnPublish": [],
196 | "Approve": [],
197 | "UnApprove": []
198 | },
199 | "GenericDimension": {
200 | "GetLayout": [
201 | "qLayout"
202 | ],
203 | "ApplyPatches": [],
204 | "SetProperties": [],
205 | "GetProperties": [
206 | "qProp"
207 | ],
208 | "GetInfo": [
209 | "qInfo"
210 | ],
211 | "GetDimension": [
212 | "qDim"
213 | ],
214 | "GetLinkedObjects": [
215 | "qItems"
216 | ],
217 | "Publish": [],
218 | "UnPublish": [],
219 | "Approve": [],
220 | "UnApprove": []
221 | },
222 | "GenericBookmark": {
223 | "GetFieldValues": [
224 | "qFieldValues"
225 | ],
226 | "GetLayout": [
227 | "qLayout"
228 | ],
229 | "ApplyPatches": [],
230 | "SetProperties": [],
231 | "GetProperties": [
232 | "qProp"
233 | ],
234 | "GetInfo": [
235 | "qInfo"
236 | ],
237 | "Apply": [
238 | "qSuccess"
239 | ],
240 | "Publish": [],
241 | "UnPublish": [],
242 | "Approve": [],
243 | "UnApprove": []
244 | },
245 | "GenericVariable": {
246 | "GetLayout": [
247 | "qLayout"
248 | ],
249 | "ApplyPatches": [],
250 | "SetProperties": [],
251 | "GetProperties": [
252 | "qProp"
253 | ],
254 | "GetInfo": [
255 | "qInfo"
256 | ],
257 | "SetStringValue": [],
258 | "SetNumValue": [],
259 | "SetDualValue": []
260 | },
261 | "GenericMeasure": {
262 | "GetLayout": [
263 | "qLayout"
264 | ],
265 | "ApplyPatches": [],
266 | "SetProperties": [],
267 | "GetProperties": [
268 | "qProp"
269 | ],
270 | "GetInfo": [
271 | "qInfo"
272 | ],
273 | "GetMeasure": [
274 | "qMeasure"
275 | ],
276 | "GetLinkedObjects": [
277 | "qItems"
278 | ],
279 | "Publish": [],
280 | "UnPublish": [],
281 | "Approve": [],
282 | "UnApprove": []
283 | },
284 | "Doc": {
285 | "GetField": [
286 | "qReturn"
287 | ],
288 | "GetFieldDescription": [
289 | "qReturn"
290 | ],
291 | "GetVariable": [
292 | "qReturn"
293 | ],
294 | "GetLooselyCoupledVector": [
295 | "qv"
296 | ],
297 | "SetLooselyCoupledVector": [
298 | "qReturn"
299 | ],
300 | "Evaluate": [
301 | "qReturn"
302 | ],
303 | "EvaluateEx": [
304 | "qValue"
305 | ],
306 | "ClearAll": [],
307 | "LockAll": [],
308 | "UnlockAll": [],
309 | "Back": [],
310 | "Forward": [],
311 | "CreateVariable": [
312 | "qReturn"
313 | ],
314 | "RemoveVariable": [
315 | "qReturn"
316 | ],
317 | "GetLocaleInfo": [
318 | "qReturn"
319 | ],
320 | "GetTablesAndKeys": [
321 | "qtr",
322 | "qk"
323 | ],
324 | "GetViewDlgSaveInfo": [
325 | "qReturn"
326 | ],
327 | "SetViewDlgSaveInfo": [],
328 | "GetEmptyScript": [
329 | "qReturn"
330 | ],
331 | "DoReload": [
332 | "qReturn"
333 | ],
334 | "GetScriptBreakpoints": [
335 | "qBreakpoints"
336 | ],
337 | "SetScriptBreakpoints": [],
338 | "GetScript": [
339 | "qScript"
340 | ],
341 | "GetTextMacros": [
342 | "qMacros"
343 | ],
344 | "SetFetchLimit": [],
345 | "DoSave": [],
346 | "GetTableData": [
347 | "qData"
348 | ],
349 | "GetAppLayout": [
350 | "qLayout"
351 | ],
352 | "SetAppProperties": [],
353 | "GetAppProperties": [
354 | "qProp"
355 | ],
356 | "GetLineage": [
357 | "qLineage"
358 | ],
359 | "CreateSessionObject": [
360 | "qReturn"
361 | ],
362 | "DestroySessionObject": [
363 | "qSuccess"
364 | ],
365 | "CreateObject": [
366 | "qInfo",
367 | "qReturn"
368 | ],
369 | "DestroyObject": [
370 | "qSuccess"
371 | ],
372 | "GetObject": [
373 | "qReturn"
374 | ],
375 | "GetObjects": [
376 | "qList"
377 | ],
378 | "GetBookmarks": [
379 | "qList"
380 | ],
381 | "CloneObject": [
382 | "qCloneId"
383 | ],
384 | "CreateDraft": [
385 | "qDraftId"
386 | ],
387 | "CommitDraft": [],
388 | "DestroyDraft": [
389 | "qSuccess"
390 | ],
391 | "Undo": [
392 | "qSuccess"
393 | ],
394 | "Redo": [
395 | "qSuccess"
396 | ],
397 | "ClearUndoBuffer": [],
398 | "CreateDimension": [
399 | "qInfo",
400 | "qReturn"
401 | ],
402 | "DestroyDimension": [
403 | "qSuccess"
404 | ],
405 | "GetDimension": [
406 | "qReturn"
407 | ],
408 | "CloneDimension": [
409 | "qCloneId"
410 | ],
411 | "CreateMeasure": [
412 | "qInfo",
413 | "qReturn"
414 | ],
415 | "DestroyMeasure": [
416 | "qSuccess"
417 | ],
418 | "GetMeasure": [
419 | "qReturn"
420 | ],
421 | "CloneMeasure": [
422 | "qCloneId"
423 | ],
424 | "CreateSessionVariable": [
425 | "qReturn"
426 | ],
427 | "DestroySessionVariable": [
428 | "qSuccess"
429 | ],
430 | "CreateVariableEx": [
431 | "qInfo",
432 | "qReturn"
433 | ],
434 | "DestroyVariableById": [
435 | "qSuccess"
436 | ],
437 | "DestroyVariableByName": [
438 | "qSuccess"
439 | ],
440 | "GetVariableById": [
441 | "qReturn"
442 | ],
443 | "GetVariableByName": [
444 | "qReturn"
445 | ],
446 | "CheckExpression": [
447 | "qErrorMsg",
448 | "qBadFieldNames",
449 | "qDangerousFieldNames"
450 | ],
451 | "CheckNumberOrExpression": [
452 | "qErrorMsg",
453 | "qBadFieldNames"
454 | ],
455 | "AddAlternateState": [],
456 | "RemoveAlternateState": [],
457 | "CreateBookmark": [
458 | "qInfo",
459 | "qReturn"
460 | ],
461 | "DestroyBookmark": [
462 | "qSuccess"
463 | ],
464 | "GetBookmark": [
465 | "qReturn"
466 | ],
467 | "ApplyBookmark": [
468 | "qSuccess"
469 | ],
470 | "CloneBookmark": [
471 | "qCloneId"
472 | ],
473 | "AddFieldFromExpression": [
474 | "qSuccess"
475 | ],
476 | "GetFieldOnTheFlyByName": [
477 | "qName"
478 | ],
479 | "GetAllInfos": [
480 | "qInfos"
481 | ],
482 | "Resume": [],
483 | "AbortModal": [],
484 | "Publish": [],
485 | "GetMatchingFields": [
486 | "qFieldNames"
487 | ],
488 | "FindMatchingFields": [
489 | "qFieldNames"
490 | ],
491 | "Scramble": [],
492 | "SaveObjects": [],
493 | "GetAssociationScores": [
494 | "qScore"
495 | ],
496 | "GetMediaList": [
497 | "qList",
498 | "qReturn"
499 | ],
500 | "GetContentLibraries": [
501 | "qList"
502 | ],
503 | "GetLibraryContent": [
504 | "qList"
505 | ],
506 | "DoReloadEx": [
507 | "qResult"
508 | ],
509 | "BackCount": [
510 | "qReturn"
511 | ],
512 | "ForwardCount": [
513 | "qReturn"
514 | ],
515 | "SetScript": [],
516 | "CheckScriptSyntax": [
517 | "qErrors"
518 | ],
519 | "GetFavoriteVariables": [
520 | "qNames"
521 | ],
522 | "SetFavoriteVariables": [],
523 | "GetIncludeFileContent": [
524 | "qContent"
525 | ],
526 | "CreateConnection": [
527 | "qConnectionId"
528 | ],
529 | "ModifyConnection": [],
530 | "DeleteConnection": [],
531 | "GetConnection": [
532 | "qConnection"
533 | ],
534 | "GetConnections": [
535 | "qConnections"
536 | ],
537 | "GetDatabaseInfo": [
538 | "qInfo"
539 | ],
540 | "GetDatabases": [
541 | "qDatabases"
542 | ],
543 | "GetDatabaseOwners": [
544 | "qOwners"
545 | ],
546 | "GetDatabaseTables": [
547 | "qTables"
548 | ],
549 | "GetDatabaseTableFields": [
550 | "qFields"
551 | ],
552 | "GetDatabaseTablePreview": [
553 | "qPreview",
554 | "qRowCount"
555 | ],
556 | "GetFolderItemsForConnection": [
557 | "qFolderItems"
558 | ],
559 | "GuessFileType": [
560 | "qDataFormat"
561 | ],
562 | "GetFileTables": [
563 | "qTables"
564 | ],
565 | "GetFileTableFields": [
566 | "qFields",
567 | "qFormatSpec"
568 | ],
569 | "GetFileTablePreview": [
570 | "qPreview",
571 | "qFormatSpec"
572 | ],
573 | "GetFileTablesEx": [
574 | "qTables"
575 | ],
576 | "SendGenericCommandToCustomConnector": [
577 | "qResult"
578 | ],
579 | "SearchSuggest": [
580 | "qResult"
581 | ],
582 | "SearchAssociations": [
583 | "qResults"
584 | ],
585 | "SelectAssociations": [],
586 | "SearchResults": [
587 | "qResult"
588 | ],
589 | "SearchObjects": [
590 | "qResult"
591 | ],
592 | "GetScriptEx": [
593 | "qScript"
594 | ]
595 | },
596 | "Global": {
597 | "AbortRequest": [],
598 | "AbortAll": [],
599 | "GetProgress": [
600 | "qProgressData"
601 | ],
602 | "QvVersion": [
603 | "qReturn"
604 | ],
605 | "OSVersion": [
606 | "qReturn"
607 | ],
608 | "OSName": [
609 | "qReturn"
610 | ],
611 | "QTProduct": [
612 | "qReturn"
613 | ],
614 | "GetDocList": [
615 | "qDocList"
616 | ],
617 | "GetInteract": [
618 | "qDef",
619 | "qReturn"
620 | ],
621 | "InteractDone": [],
622 | "GetAuthenticatedUser": [
623 | "qReturn"
624 | ],
625 | "CreateDocEx": [
626 | "qDocId",
627 | "qReturn"
628 | ],
629 | "GetActiveDoc": [
630 | "qReturn"
631 | ],
632 | "AllowCreateApp": [
633 | "qReturn"
634 | ],
635 | "CreateApp": [
636 | "qSuccess",
637 | "qAppId"
638 | ],
639 | "DeleteApp": [
640 | "qSuccess"
641 | ],
642 | "IsDesktopMode": [
643 | "qReturn"
644 | ],
645 | "CancelRequest": [],
646 | "ShutdownProcess": [],
647 | "ReloadExtensionList": [],
648 | "ReplaceAppFromID": [
649 | "qSuccess"
650 | ],
651 | "CopyApp": [
652 | "qSuccess"
653 | ],
654 | "ExportApp": [
655 | "qSuccess"
656 | ],
657 | "PublishApp": [],
658 | "IsPersonalMode": [
659 | "qReturn"
660 | ],
661 | "GetUniqueID": [
662 | "qUniqueID"
663 | ],
664 | "OpenDoc": [
665 | "qReturn"
666 | ],
667 | "CreateSessionApp": [
668 | "qSessionAppId",
669 | "qReturn"
670 | ],
671 | "CreateSessionAppFromApp": [
672 | "qSessionAppId",
673 | "qReturn"
674 | ],
675 | "ProductVersion": [
676 | "qReturn"
677 | ],
678 | "GetAppEntry": [
679 | "qEntry"
680 | ],
681 | "ConfigureReload": [],
682 | "CancelReload": [],
683 | "GetBNF": [
684 | "qBnfDefs"
685 | ],
686 | "GetFunctions": [
687 | "qFunctions"
688 | ],
689 | "GetOdbcDsns": [
690 | "qOdbcDsns"
691 | ],
692 | "GetOleDbProviders": [
693 | "qOleDbProviders"
694 | ],
695 | "GetDatabasesFromConnectionString": [
696 | "qDatabases"
697 | ],
698 | "IsValidConnectionString": [
699 | "qReturn"
700 | ],
701 | "GetDefaultAppFolder": [
702 | "qPath"
703 | ],
704 | "GetLogicalDriveStrings": [
705 | "qDrives"
706 | ],
707 | "GetFolderItemsForPath": [
708 | "qFolderItems"
709 | ],
710 | "GetSupportedCodePages": [
711 | "qCodePages"
712 | ],
713 | "GetCustomConnectors": [
714 | "qConnectors"
715 | ],
716 | "GetStreamList": [
717 | "qStreamList"
718 | ],
719 | "EngineVersion": [
720 | "qVersion"
721 | ],
722 | "GetBaseBNF": [
723 | "qBnfDefs",
724 | "qBnfHash"
725 | ],
726 | "GetBaseBNFHash": [
727 | "qBnfHash"
728 | ],
729 | "GetBaseBNFString": [
730 | "qBnfStr",
731 | "qBnfHash"
732 | ]
733 | }
734 | }
--------------------------------------------------------------------------------