├── .gitignore ├── .jscs.json ├── .travis.yml ├── Gruntfile.js ├── LICENSE ├── README.md ├── index.js ├── package.json └── test └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # Real Sense certificates 40 | test/certs 41 | 42 | # Old stuff 43 | old -------------------------------------------------------------------------------- /.jscs.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "google", 3 | "maxErrors" : 5000, 4 | "validateIndentation": 4, 5 | "maximumLineLength": 512, 6 | "requireCurlyBraces": false, 7 | "jsDoc": { 8 | "checkAnnotations": { 9 | "preset": "closurecompiler", 10 | "extra": { 11 | "module": true, 12 | "typicalname": true, 13 | "copyright": true, 14 | "classdesc": true 15 | } 16 | } 17 | }, 18 | "requireCamelCaseOrUpperCaseIdentifiers": false 19 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "7.4.0" 4 | - "6.9.4" 5 | - "6" 6 | - "6.1" 7 | - "5.11" 8 | after_success: 9 | - 'cat ./coverage/lcov.info | node ./node_modules/coveralls/bin/coveralls.js' -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | // Project configuration. 4 | grunt.initConfig({ 5 | pkg: grunt.file.readJSON('package.json'), 6 | 7 | jsdoc2md: { 8 | oneOutputFile: { 9 | src: ['index.js', 'lib/**/*.js'], 10 | dest: 'README.md' 11 | }, 12 | multipleOutputfiles: { 13 | files: [ 14 | {src: 'index.js', dest: 'README.md'}, 15 | {src: 'lib/array.js', dest: 'lib/array.md'}, 16 | {src: 'lib/core.js', dest: 'lib/core.md'}, 17 | {src: 'lib/object.js', dest: 'lib/object.md'}, 18 | {src: ['lib/qlik.js', 'lib/qlik/*.js', 'lib/qlik/apis/qrs/qrs.js', 'lib/qlik/apis/qps/qps.js'], dest: 'lib/qlik.md'}, 19 | {src: 'lib/qlik/apis/qrs/qrs.sdk.js', dest: 'lib/qlik/apis/qrs/qrs.md'}, 20 | {src: 'lib/qlik/apis/qrs/qrs.sdk.about.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.about.md'}, 21 | {src: 'lib/qlik/apis/qrs/qrs.sdk.app.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.app.md'}, 22 | {src: 'lib/qlik/apis/qrs/qrs.sdk.appavailability.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.appavailability.md'}, 23 | {src: 'lib/qlik/apis/qrs/qrs.sdk.appcomponent.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.appcomponent.md'}, 24 | {src: 'lib/qlik/apis/qrs/qrs.sdk.appcontent.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.appcontent.md'}, 25 | {src: 'lib/qlik/apis/qrs/qrs.sdk.appcontentquota.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.appcontentquota.md'}, 26 | {src: 'lib/qlik/apis/qrs/qrs.sdk.applicationlog.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.applicationlog.md'}, 27 | {src: 'lib/qlik/apis/qrs/qrs.sdk.appseedinfo.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.appseedinfo.md'}, 28 | {src: 'lib/qlik/apis/qrs/qrs.sdk.appstatus.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.appstatus.md'}, 29 | {src: 'lib/qlik/apis/qrs/qrs.sdk.binarydelete.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.binarydelete.md'}, 30 | {src: 'lib/qlik/apis/qrs/qrs.sdk.binarydownload.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.binarydownload.md'}, 31 | {src: 'lib/qlik/apis/qrs/qrs.sdk.binarysyncruleevaluation.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.binarysyncruleevaluation.md'}, 32 | {src: 'lib/qlik/apis/qrs/qrs.sdk.cache.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.cache.md'}, 33 | {src: 'lib/qlik/apis/qrs/qrs.sdk.certificatedistribution.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.certificatedistribution.md'}, 34 | {src: 'lib/qlik/apis/qrs/qrs.sdk.compositeevent.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.compositeevent.md'}, 35 | {src: 'lib/qlik/apis/qrs/qrs.sdk.compositeeventoperational.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.compositeeventoperational.md'}, 36 | {src: 'lib/qlik/apis/qrs/qrs.sdk.compositeeventruleoperational.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.compositeeventruleoperational.md'}, 37 | {src: 'lib/qlik/apis/qrs/qrs.sdk.contentlibrary.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.contentlibrary.md'}, 38 | {src: 'lib/qlik/apis/qrs/qrs.sdk.custompropertydefinition.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.custompropertydefinition.md'}, 39 | {src: 'lib/qlik/apis/qrs/qrs.sdk.dataconnection.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.dataconnection.md'}, 40 | {src: 'lib/qlik/apis/qrs/qrs.sdk.download.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.download.md'}, 41 | {src: 'lib/qlik/apis/qrs/qrs.sdk.engineservice.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.engineservice.md'}, 42 | {src: 'lib/qlik/apis/qrs/qrs.sdk.event.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.event.md'}, 43 | {src: 'lib/qlik/apis/qrs/qrs.sdk.eventoperational.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.eventoperational.md'}, 44 | {src: 'lib/qlik/apis/qrs/qrs.sdk.executionresult.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.executionresult.md'}, 45 | {src: 'lib/qlik/apis/qrs/qrs.sdk.executionsession.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.executionsession.md'}, 46 | {src: 'lib/qlik/apis/qrs/qrs.sdk.extension.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.extension.md'}, 47 | {src: 'lib/qlik/apis/qrs/qrs.sdk.externalchangeinfo.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.externalchangeinfo.md'}, 48 | {src: 'lib/qlik/apis/qrs/qrs.sdk.externalprogramtask.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.externalprogramtask.md'}, 49 | {src: 'lib/qlik/apis/qrs/qrs.sdk.externalprogramtaskoperational.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.externalprogramtaskoperational.md'}, 50 | {src: 'lib/qlik/apis/qrs/qrs.sdk.filereference.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.filereference.md'}, 51 | {src: 'lib/qlik/apis/qrs/qrs.sdk.license.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.license.md'}, 52 | {src: 'lib/qlik/apis/qrs/qrs.sdk.licenseaccessusage.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.licenseaccessusage.md'}, 53 | {src: 'lib/qlik/apis/qrs/qrs.sdk.loadbalancing.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.loadbalancing.md'}, 54 | {src: 'lib/qlik/apis/qrs/qrs.sdk.log.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.log.md'}, 55 | {src: 'lib/qlik/apis/qrs/qrs.sdk.managementconsolelog.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.managementconsolelog.md'}, 56 | {src: 'lib/qlik/apis/qrs/qrs.sdk.mimetype.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.mimetype.md'}, 57 | {src: 'lib/qlik/apis/qrs/qrs.sdk.notification.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.notification.md'}, 58 | {src: 'lib/qlik/apis/qrs/qrs.sdk.printingservice.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.printingservice.md'}, 59 | {src: 'lib/qlik/apis/qrs/qrs.sdk.proxyservice.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.proxyservice.md'}, 60 | {src: 'lib/qlik/apis/qrs/qrs.sdk.reloadtask.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.reloadtask.md'}, 61 | {src: 'lib/qlik/apis/qrs/qrs.sdk.reloadtaskoperational.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.reloadtaskoperational.md'}, 62 | {src: 'lib/qlik/apis/qrs/qrs.sdk.repositoryservice.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.repositoryservice.md'}, 63 | {src: 'lib/qlik/apis/qrs/qrs.sdk.schedulerservice.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.schedulerservice.md'}, 64 | {src: 'lib/qlik/apis/qrs/qrs.sdk.schemaevent.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.schemaevent.md'}, 65 | {src: 'lib/qlik/apis/qrs/qrs.sdk.schemaeventoperational.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.schemaeventoperational.md'}, 66 | {src: 'lib/qlik/apis/qrs/qrs.sdk.selection.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.selection.md'}, 67 | {src: 'lib/qlik/apis/qrs/qrs.sdk.servernodeconfiguration.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.servernodeconfiguration.md'}, 68 | {src: 'lib/qlik/apis/qrs/qrs.sdk.servernoderegistration.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.servernoderegistration.md'}, 69 | {src: 'lib/qlik/apis/qrs/qrs.sdk.servicestatus.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.servicestatus.md'}, 70 | {src: 'lib/qlik/apis/qrs/qrs.sdk.staticcontent.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.staticcontent.md'}, 71 | {src: 'lib/qlik/apis/qrs/qrs.sdk.staticcontentreference.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.staticcontentreference.md'}, 72 | {src: 'lib/qlik/apis/qrs/qrs.sdk.staticcontentreferencebase.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.staticcontentreferencebase.md'}, 73 | {src: 'lib/qlik/apis/qrs/qrs.sdk.stream.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.stream.md'}, 74 | {src: 'lib/qlik/apis/qrs/qrs.sdk.sync.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.sync.md'}, 75 | {src: 'lib/qlik/apis/qrs/qrs.sdk.syncsession.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.syncsession.md'}, 76 | {src: 'lib/qlik/apis/qrs/qrs.sdk.systemrule.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.systemrule.md'}, 77 | {src: 'lib/qlik/apis/qrs/qrs.sdk.tag.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.tag.md'}, 78 | {src: 'lib/qlik/apis/qrs/qrs.sdk.task.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.task.md'}, 79 | {src: 'lib/qlik/apis/qrs/qrs.sdk.taskoperational.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.taskoperational.md'}, 80 | {src: 'lib/qlik/apis/qrs/qrs.sdk.tempcontent.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.tempcontent.md'}, 81 | {src: 'lib/qlik/apis/qrs/qrs.sdk.user.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.user.md'}, 82 | {src: 'lib/qlik/apis/qrs/qrs.sdk.userdirectory.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.userdirectory.md'}, 83 | {src: 'lib/qlik/apis/qrs/qrs.sdk.userdirectoryconnector.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.userdirectoryconnector.md'}, 84 | {src: 'lib/qlik/apis/qrs/qrs.sdk.usersynctask.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.usersynctask.md'}, 85 | {src: 'lib/qlik/apis/qrs/qrs.sdk.usersynctaskoperational.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.usersynctaskoperational.md'}, 86 | {src: 'lib/qlik/apis/qrs/qrs.sdk.virtualproxyconfig.js', dest: 'lib/qlik/apis/qrs/qrs.sdk.virtualproxyconfig.md'} 87 | ] 88 | } 89 | }, 90 | 91 | jscs: { 92 | src: ['index.js', 'lib/**/*.js'], 93 | options: { 94 | config: '.jscs.json' 95 | } 96 | }, 97 | simplemocha: { 98 | options: { 99 | ui: 'bdd', 100 | reporter: 'tap' 101 | }, 102 | all: {src: ['test/**/*.js']} 103 | }, 104 | mocha_istanbul: { 105 | coverage: { 106 | src: 'test', 107 | options: { 108 | mask: '*.js', 109 | coverageFolder: 'coverage', 110 | check: { 111 | statements: 20, 112 | branches: 70, 113 | functions: 10, 114 | lines: 20 115 | } 116 | } 117 | } 118 | }, 119 | coveralls: { 120 | options: { 121 | force: false 122 | }, 123 | default: { 124 | src: 'coverage/*.info', 125 | options: { 126 | } 127 | } 128 | }, 129 | bump: { 130 | options: { 131 | push: true, 132 | pushTo: 'origin' 133 | } 134 | 135 | }, 136 | shell: { 137 | publish: { 138 | command: 'npm publish' 139 | } 140 | } 141 | }); 142 | 143 | grunt.loadNpmTasks('grunt-jsdoc-to-markdown'); 144 | grunt.loadNpmTasks('grunt-bump'); 145 | grunt.loadNpmTasks('grunt-shell'); 146 | grunt.loadNpmTasks('grunt-jscs'); 147 | grunt.loadNpmTasks('grunt-simple-mocha'); 148 | grunt.loadNpmTasks('grunt-mocha-istanbul'); 149 | grunt.loadNpmTasks('grunt-coveralls'); 150 | 151 | grunt.registerTask('patch', 'patch', function() { 152 | grunt.task.run('bump:patch', 'shell:publish'); 153 | }); 154 | 155 | grunt.registerTask('release', 'Release a new version, push it and publish it', function() { 156 | grunt.task.run('jscs', /*'simplemocha:all',*/ 'mocha_istanbul:coverage', 'jsdoc2md:multipleOutputfiles', 'bump:patch', 'shell:publish', 'coveralls:default'); 157 | }); 158 | 159 | }; 160 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Loïc Formont 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## qlik-utils 4 | [![GitHub version](https://badge.fury.io/gh/pouc%2Fmy-node-template.svg)](https://badge.fury.io/gh/pouc%2Fmy-node-template) 5 | [![npm version](https://badge.fury.io/js/my-node-template.svg)](https://badge.fury.io/js/my-node-template) 6 | [![NPM monthly downloads](https://img.shields.io/npm/dm/my-node-template.svg?style=flat)](https://npmjs.org/package/my-node-template) 7 | [![Build Status](https://travis-ci.org/pouc/my-node-template.svg?branch=master)](https://travis-ci.org/pouc/my-node-template) 8 | [![Dependency Status](https://gemnasium.com/badges/github.com/pouc/my-node-template.svg)](https://gemnasium.com/github.com/pouc/my-node-template) 9 | [![Coverage Status](https://coveralls.io/repos/github/pouc/my-node-template/badge.svg?branch=master)](https://coveralls.io/github/pouc/my-node-template?branch=master) 10 | [![Known Vulnerabilities](https://snyk.io/test/github/pouc/my-node-template/badge.svg)](https://snyk.io/test/github/pouc/my-node-template) 11 | 12 | A set of utilities to deal with Qlik Sense APIs 13 | 14 | Version 3 is compatible with enigma.js v2 15 | 16 | Not compatible with version 2 17 | 18 | **Author:** Loïc Formont 19 | **License**: MIT Licensed 20 | **Example** 21 | ```javascript 22 | var template = require("my-node-template"); 23 | ``` 24 | 25 | 26 | ### template.example(name) ⇒ \* 27 | Description of the function 28 | 29 | **Kind**: static method of [my-node-template](#module_my-node-template) 30 | **Returns**: \* - a value 31 | 32 | | Param | Type | Description | 33 | | --- | --- | --- | 34 | | name | type | the first parameter | 35 | 36 | **Example** 37 | Example of the function 38 | 39 | ```javascript 40 | var code = of.the.function; 41 | ``` 42 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Rx = require('rxjs/Rx'); 4 | var promise = require('q'); 5 | var mapObj = require('map-obj'); 6 | var values = require('object.values'); 7 | var extend = require('extend'); 8 | var sprintf = require('sprintf-js').sprintf; 9 | var arrayDivide = require('array-divide'); 10 | 11 | var undef = require('ifnotundef'); 12 | 13 | var qps = require('qlik-api-qps'); 14 | var qrs = require('qlik-api-qrs'); 15 | 16 | var Task = require('rxjs-task-subject'); 17 | 18 | /** 19 | * 20 | * [![GitHub version](https://badge.fury.io/gh/pouc%2Fqlik-utils.svg)](https://badge.fury.io/gh/pouc%2Fqlik-utils) 21 | * [![npm version](https://badge.fury.io/js/qlik-utils.svg)](https://badge.fury.io/js/qlik-utils) 22 | * [![NPM monthly downloads](https://img.shields.io/npm/dm/qlik-utils.svg?style=flat)](https://npmjs.org/package/qlik-utils) 23 | * [![Build Status](https://travis-ci.org/pouc/qlik-utils.svg?branch=master)](https://travis-ci.org/pouc/qlik-utils) 24 | * [![Dependency Status](https://gemnasium.com/badges/github.com/pouc/qlik-utils.svg)](https://gemnasium.com/github.com/pouc/qlik-utils) 25 | * [![Coverage Status](https://coveralls.io/repos/github/pouc/qlik-utils/badge.svg?branch=master)](https://coveralls.io/github/pouc/qlik-utils?branch=master) 26 | * [![Known Vulnerabilities](https://snyk.io/test/github/pouc/qlik-utils/badge.svg)](https://snyk.io/test/github/pouc/qlik-utils) 27 | * 28 | * A set of utility functions for simplifying the call to Qlik Sense APIs 29 | * 30 | * @module qlik-utils 31 | * @typicalname utils 32 | * @author Loic Formont 33 | * 34 | * @copyright Copyright (C) 2017 Loic Formont 35 | * @license MIT Licensed 36 | * 37 | * @example 38 | * ```javascript 39 | * var utils = require("qlik-utils"); 40 | * ``` 41 | */ 42 | module.exports = createUtils({}); 43 | module.exports.create = createUtils; 44 | 45 | function createUtils(utilsOptions) { 46 | undef.try(utilsOptions); 47 | 48 | var extPromise = undef.if(utilsOptions.promise, promise.promise); 49 | var returnObservable = undef.if(utilsOptions.returnObservable, false); 50 | 51 | var retVal = { 52 | 53 | /** 54 | * Generates a ticket on Qlik Sense QRS Api. If the targetId is not correct 55 | * then the ticket will redirect to the hub 56 | * 57 | * @example 58 | * ```javascript 59 | * utils.getTicket({ 60 | * restUri: 'https://10.76.224.72', 61 | * pfx: pfx, 62 | * passPhrase: '' 63 | * }, 64 | * { 65 | * UserId: 'qlikservice', 66 | * UserDirectory: '2008R2-0', 67 | * Attributes: [] 68 | * } 69 | * }).then(function(retVal) { 70 | * console.log(retVal); 71 | * }); 72 | * ``` 73 | * 74 | * @memberOf Qlik 75 | * 76 | * @param {options} options Qlik Sense connection options 77 | * @param {ticketParams} params the ticket parameters 78 | * @returns {Task.} a Task resolving to the generated ticket 79 | */ 80 | getTicket: function(options, params) { 81 | 82 | // Create connection to qps 83 | var qpsApi = qps(options); 84 | 85 | // Returned object 86 | var task = new Task(); 87 | 88 | // Ticket flow 89 | Rx.Observable 90 | .from(qpsApi.ticket.post(params)) 91 | .catch((err) => { 92 | if (err.match(/^Specified targetId .* was unknown$/)) { 93 | 94 | // if there was a problem with the target Id, try to generate another ticket by reseting target Id 95 | task.running('warning', `Wrong targetId: '${params.TargetId}', generating a ticket to default location`); 96 | delete params.TargetId; 97 | 98 | return Rx.Observable.from(module.exports.getTicket(options, params)); 99 | } else { 100 | return Rx.Observable.throw(new Error(err)); 101 | } 102 | }) 103 | .subscribe(task); 104 | 105 | if (returnObservable) { 106 | return task; 107 | } else { 108 | return task.toPromise(extPromise); 109 | } 110 | }, 111 | 112 | /** 113 | * Adds the given ip address to the websocket whitelist of the given virtual proxy. 114 | * Be careful: this restarts the proxy. The restart can take 1-2 seconds. All subsequent API 115 | * calls within this restart will fail miserably with various random & useless error messages. 116 | * 117 | * @example 118 | * ```javascript 119 | * readFile('./client.pfx').then(function(certif) { 120 | * 121 | * return utils.Qlik.addToWhiteList({ 122 | * restUri: 'https://10.76.224.72:4242', 123 | * pfx: certif, 124 | * passPhrase: '', 125 | * UserId: 'qlikservice', 126 | * UserDirectory: '2008R2-0', 127 | * params: { 128 | * ip: '10.76.224.72' 129 | * } 130 | * }); 131 | * 132 | * }).then(function(ret) { 133 | * console.log(ret); 134 | * }, function(ret) { 135 | * console.log(ret); 136 | * }); 137 | * ``` 138 | * 139 | * @memberOf Qlik 140 | * 141 | * @param {options} options Qlik Sense connection options 142 | * @param {string} params parameters to add to the whitelist 143 | * @param {string} params.ip the ip to add 144 | * @param {string|int} [params.vp] the prefix or id of the virtual proxy to update. If null or false, this function will update all vps 145 | * @returns {Promise.} a promise resolving to the virtual proxy configuration when successfull 146 | */ 147 | addToWhiteList: function(options, params) { 148 | 149 | // create connection to qrs 150 | var qrsApi = qrs(options); 151 | 152 | var ip = params.ip; 153 | var vp = params.vp; 154 | 155 | // Returned object 156 | var task = new Task(); 157 | 158 | // Promise chain 159 | // TODO: Convert this to a rx based logic 160 | var vpConfig = promise().then(function() { 161 | 162 | // Get proxy configuration 163 | return qrsApi.proxyservice.local.get(); 164 | 165 | }).then(function(settings) { 166 | 167 | // Get virtual proxy configuration 168 | 169 | var vpsettings = undef.child(settings, 'settings.virtualProxies', []).filter(function(elem, index) { 170 | if (undef.is(vp)) return true; 171 | else if (parseInt(vp) === vp) return index == vp; 172 | else return elem.prefix == vp; 173 | }); 174 | 175 | return promise.all(vpsettings.map(function(vp) { 176 | return qrsApi.virtualproxyconfig.id(vp.id).get(); 177 | })); 178 | 179 | }).then(function(vpSettings) { 180 | 181 | return promise.all(vpSettings.map(function(settings) { 182 | 183 | // If IP not already in whitelist 184 | 185 | if (settings.websocketCrossOriginWhiteList.indexOf(ip) == -1) { 186 | 187 | // Add it and push new config 188 | // The mechanism with dates below makes sure that there is no desync between 189 | // the client and the Qlik Sense server 190 | 191 | settings.websocketCrossOriginWhiteList.push(ip); 192 | 193 | var currDate = new Date(); 194 | var oldDate = new Date(settings.modifiedDate); 195 | var newDate; 196 | 197 | if (currDate > oldDate) { 198 | newDate = currDate; 199 | } else { 200 | newDate = oldDate; 201 | newDate.setSeconds(newDate.getSeconds() + 1); 202 | } 203 | 204 | settings.modifiedDate = newDate.toISOString(); 205 | 206 | return qrsApi.virtualproxyconfig.id(settings.id).put(settings); 207 | 208 | } else { 209 | 210 | // Else just return the VP config 211 | 212 | return settings; 213 | } 214 | })); 215 | 216 | }); 217 | 218 | // Convert the promise chain to an observable and register our task object 219 | Rx.Observable.from(vpConfig).subscribe(task); 220 | 221 | if (returnObservable) { 222 | return task; 223 | } else { 224 | return task.toPromise(extPromise); 225 | } 226 | 227 | }, 228 | 229 | /** 230 | * Mixins to be used with enigma.js 231 | */ 232 | mixins: { 233 | Global: { 234 | types: ['Global'], 235 | init: function(args) {}, 236 | extend: { 237 | 238 | odag: odagMixin, 239 | loopAndReload: loopAndReloadMixin 240 | 241 | } 242 | }, 243 | 244 | Doc: { 245 | types: ['Doc'], 246 | init: function(args) { 247 | return args.api.getAppLayout().then((layout) => { 248 | args.api.layout = layout 249 | }); 250 | }, 251 | extend: { 252 | 253 | getDimensions: getDimensionsMixin, 254 | getMeasures: getMeasuresMixin, 255 | getSheets: getSheetsMixin, 256 | qrs: qrsMixin, 257 | exportCube: exportCubeMixin, 258 | createListBox: createListBoxMixin, 259 | export: exportMixin 260 | 261 | } 262 | } 263 | } 264 | 265 | } 266 | 267 | function getDimensionsMixin() { 268 | return this.createSessionObject({ 269 | qDimensionListDef: { 270 | qType: 'dimension', 271 | qData: { 272 | title: '/qMetaDef/title', 273 | tags: '/qMetaDef/tags', 274 | grouping: '/qDim/qGrouping', 275 | info: '/qDimInfos' 276 | } 277 | }, qInfo: { 278 | qId: "DimensionList", 279 | qType: "DimensionList" 280 | } 281 | }).then((list) => { 282 | return list.getLayout(); 283 | }).then((listLayout) => { 284 | return listLayout.qDimensionList.qItems; 285 | }); 286 | } 287 | 288 | function getMeasuresMixin() { 289 | return this.createSessionObject({ 290 | qMeasureListDef: { 291 | qType: 'measure', 292 | qData: { 293 | title: '/qMetaDef/title', 294 | tags: '/qMetaDef/tags' 295 | } 296 | }, 297 | qInfo: { 298 | qId: 'MeasureList', 299 | qType: 'MeasureList' 300 | } 301 | }).then((list) => { 302 | return list.getLayout(); 303 | }).then((listLayout) => { 304 | return listLayout.qMeasureList.qItems; 305 | }); 306 | } 307 | 308 | function getSheetsMixin() { 309 | return this.createSessionObject({ 310 | "qInfo": { 311 | "qType": "SheetList" 312 | }, 313 | "qAppObjectListDef": { 314 | "qType": "sheet", 315 | "qData": { 316 | "title": "/qMetaDef/title", 317 | "description": "/qMetaDef/description", 318 | "thumbnail": "/thumbnail", 319 | "cells": "/cells", 320 | "rank": "/rank", 321 | "columns": "/columns", 322 | "rows": "/rows" 323 | } 324 | } 325 | }).then((list) => { 326 | return list.getLayout(); 327 | }).then((listLayout) => { 328 | return listLayout.qAppObjectList.qItems; 329 | }); 330 | } 331 | 332 | /** 333 | * Exposes qrs on the opened app 334 | * 335 | * @param options the connections infos to the QRS API 336 | * @returns {QRS} a QRS api object 337 | */ 338 | function qrsMixin(options) { 339 | return qrs(options).app.id(this.layout.qFileName); 340 | } 341 | 342 | /** 343 | * Exports a cube 344 | * 345 | * @param cubeDef the cube definition 346 | * @returns {Task.>} a task resolving to the cube pages 347 | */ 348 | function exportCubeMixin(cubeDef) { 349 | 350 | var app = this; 351 | var task = new Task(); 352 | 353 | promise().then(function() { 354 | 355 | // Start 356 | 357 | task.running('info', 'Starting!'); 358 | 359 | }).then(function(reply) { 360 | 361 | // Create cube 362 | 363 | return app.createSessionObject({ 364 | qHyperCubeDef: cubeDef, 365 | qInfo: { 366 | qType: 'mashup' 367 | } 368 | }).then(function(reply) { 369 | task.running('info', 'Cube generated'); 370 | return reply; 371 | }) 372 | 373 | }).then(function(sessionObject) { 374 | 375 | // Get cube layout 376 | 377 | return promise.all([ 378 | sessionObject, 379 | sessionObject.getLayout().then(function(reply) { 380 | task.running('info', 'Got cube layout'); 381 | return reply; 382 | }) 383 | ]) 384 | 385 | }).then(function([sessionObject, cubeLayout]) { 386 | 387 | // Get cube pages 388 | 389 | var columns = cubeLayout.qHyperCube.qSize.qcx; 390 | var totalheight = cubeLayout.qHyperCube.qSize.qcy; 391 | var pageheight = Math.floor(10000 / columns); 392 | var numberOfPages = Math.ceil(totalheight / pageheight); 393 | 394 | var pages = Array.apply(null, new Array(numberOfPages)).map(function(data, index) { 395 | 396 | return sessionObject.getHyperCubeData( 397 | '/qHyperCubeDef', 398 | [{ 399 | qTop: (pageheight * index), 400 | qLeft: 0, 401 | qWidth: columns, 402 | qHeight: pageheight 403 | }] 404 | ).then((page) => { 405 | task.running('page', page); 406 | return page; 407 | }); 408 | 409 | }, this); 410 | 411 | return promise.all(pages).then(function(pages) { 412 | return promise.all([ 413 | sessionObject, 414 | cubeLayout, 415 | pages 416 | ]); 417 | }); 418 | 419 | }).then(function([sessionObject, cubeLayout, pages]) { 420 | 421 | pages = [].concat.apply([], pages.map(function(item) { 422 | return item[0].qMatrix; 423 | })); 424 | 425 | task.done(pages); 426 | return pages; 427 | 428 | }).fail(function(err) { 429 | 430 | task.failed(err); 431 | return promise.reject(err); 432 | 433 | }); 434 | 435 | if (returnObservable) { 436 | return task; 437 | } else { 438 | return task.toPromise(extPromise); 439 | } 440 | 441 | } 442 | 443 | /** 444 | * Creates a list box on a field or on a dimension 445 | */ 446 | function createListBoxMixin({ id, field }) { 447 | return this.createObject({ 448 | qInfo: { 449 | qType: 'ListObject' 450 | }, 451 | qListObjectDef: { 452 | qStateName: '$', 453 | qLibraryId: undef.if(id, ''), 454 | qDef: { 455 | qFieldDefs: undef.if(field, [field], []) 456 | }, 457 | qInitialDataFetch: [{ 458 | qTop: 0, 459 | qLeft: 0, 460 | qHeight: 5, 461 | qWidth: 1 462 | }] 463 | } 464 | }).then((object) => { 465 | return object.getLayout(); 466 | }) 467 | } 468 | 469 | /** 470 | * Exports a set of dimensions and measures + applies filters 471 | * 472 | * @param options 473 | * @param params 474 | * @param task 475 | * @returns {Task.>} a task resolving to the cube rows 476 | */ 477 | function exportMixin(dimensions, measures, filters) { 478 | 479 | var app = this; 480 | var task = new Task(); 481 | 482 | promise().then(function() { 483 | 484 | // Start 485 | 486 | task.running('info', 'Starting export!'); 487 | 488 | }).then(function() { 489 | 490 | // Get app dimension & measure list 491 | 492 | return promise.all([ 493 | app.getDimensions().then(function(dims) { 494 | var dimensionMap = values(mapObj(undef.if(dimensions, {}), function(dimCode, dim) { 495 | return [dimCode, { 496 | dimensionCode: dimCode, 497 | dimensionName: dim.name, 498 | dimension: dim, 499 | qlikDimension: ((dim.dimensionType == 'MASTER' || dim.dimensionType == 'AUTO') ? 500 | dims.filter(function(masterDim) { 501 | return masterDim.qMeta.title == dim.name; 502 | }).map(function(item) { 503 | return { 504 | qLabel: dim.name, 505 | qLibraryId: item.qInfo.qId, 506 | qNullSuppression: true 507 | }; 508 | }) : []).concat((dim.dimensionType == 'FIELD' || dim.dimensionType == 'AUTO') ? [{ 509 | qDef: { 510 | qFieldDefs: [dim.name] 511 | }, 512 | qNullSuppression: true 513 | }] : []).concat((dim.dimensionType == 'IGNORE') ? [{ 514 | qDef: { 515 | qFieldDefs: ['=null()'] 516 | }, 517 | qNullSuppression: false 518 | }] : []) 519 | }]; 520 | })); 521 | 522 | dimensionMap = undef.if(dimensionMap, []); 523 | 524 | var dimensionNotFound = dimensionMap.filter(function(item) { 525 | return item.qlikDimension.length == 0; 526 | }); 527 | 528 | if (dimensionNotFound.length != 0) { 529 | task.running('dim-not-found', dimensionNotFound); 530 | return promise.reject('Dimension(s) not found!'); 531 | } else { 532 | return dimensionMap; 533 | } 534 | 535 | }), 536 | app.getMeasures().then(function(meas) { 537 | var measureMap = values(mapObj(undef.if(measures, {}), function(meaCode, mea) { 538 | return [meaCode, { 539 | measureCode: meaCode, 540 | measureName: mea.name, 541 | measure: mea, 542 | qlikMeasure: ((mea.measureType == 'MASTER' || mea.measureType == 'AUTO') ? 543 | meas.filter(function(masterMea) { 544 | return masterMea.qMeta.title == mea.name; 545 | }).map(function(item) { 546 | return { 547 | qLabel: mea.name, 548 | qLibraryId: item.qInfo.qId 549 | }; 550 | }) : []).concat((mea.measureType == 'FIELD' || mea.measureType == 'AUTO') ? [{ 551 | qDef: { 552 | qDef: mea.formula, 553 | qLabel: mea.name 554 | } 555 | }] : []).concat((mea.measureType == 'IGNORE') ? [{ 556 | qDef: { 557 | qDef: '=null()' 558 | } 559 | }] : []) 560 | }]; 561 | })); 562 | 563 | measureMap = undef.if(measureMap, []); 564 | 565 | var measureNotFound = measureMap.filter(function(item) { 566 | return item.qlikMeasure.length == 0; 567 | }); 568 | 569 | if (measureNotFound.length != 0) { 570 | task.running('mea-not-found', measureNotFound); 571 | return promise.reject('Measure(s) not found!'); 572 | } else { 573 | return measureMap; 574 | } 575 | 576 | }) 577 | ]); 578 | 579 | }).then(function([dimensionMap, measureMap]) { 580 | 581 | // Create cube 582 | 583 | task.running('info', 'Got measures & dimensions from app'); 584 | 585 | var cubeDef = { 586 | qInitialDataFetch: [{qHeight: 0, qWidth: 0}] 587 | }; 588 | 589 | if (dimensionMap.length > 0) { 590 | cubeDef.qDimensions = dimensionMap.map(function(item) { 591 | return item.qlikDimension[0]; 592 | }); 593 | } 594 | 595 | if (measureMap.length > 0) { 596 | cubeDef.qMeasures = measureMap.map(function(item) { 597 | return item.qlikMeasure[0]; 598 | }); 599 | } 600 | 601 | var next = promise(); 602 | if (typeof filters !== 'undefined') { 603 | 604 | next = next.then(function() { 605 | return app.clearAll(); 606 | }).then(function() { 607 | var step = promise.resolve([]); 608 | filters.filter(function(item) { 609 | return item.filters.length != 0; 610 | }).map(function(filter, index) { 611 | 612 | function promiseFactory() { 613 | return promise().then(function() { 614 | return app.getField(filter.field); 615 | }).then(function(field) { 616 | return field.selectValues( 617 | filter.filters.map(function(item) { 618 | return {qText: item}; 619 | }), 620 | false, 621 | false 622 | ); 623 | }).then(function() { 624 | return app.createListBox({field: filter.field}); 625 | }); 626 | } 627 | 628 | step = step.then(function(layouts) { 629 | return promiseFactory().then(function(layout) { 630 | layouts.push(layout); 631 | return layouts; 632 | }); 633 | }); 634 | }); 635 | 636 | return step.then(function(layouts) { 637 | var states = layouts.map(function(layout) { 638 | return layout.qListObject.qDimensionInfo.qStateCounts.qSelected != 0; 639 | }); 640 | 641 | if (!states.every(function(state) { return state; })) { 642 | return promise.reject('Not possible to apply desired filters'); 643 | } else { 644 | return promise.resolve('Filtering done'); 645 | } 646 | }).fail(function(err) { 647 | task.running('warning', err); 648 | }); 649 | }); 650 | } 651 | 652 | return next.then(function() { 653 | return Rx.Observable.from(app.exportCube(cubeDef)).toPromise(promise.promise); 654 | }); 655 | 656 | }).then(function(reply) { 657 | 658 | // Call callback function 659 | 660 | var pages = reply.map(function(row) { 661 | return row.map(function(cell) { 662 | return cell; 663 | }); 664 | }); 665 | 666 | task.done(pages); 667 | return pages; 668 | 669 | }).fail(function(err) { 670 | 671 | task.failed(err); 672 | return promise.reject(err); 673 | 674 | }); 675 | 676 | if (returnObservable) { 677 | return task; 678 | } else { 679 | return task.toPromise(extPromise); 680 | } 681 | } 682 | 683 | /** 684 | * @typedef {Object} replaceDef 685 | * @param {string} marker to be found in the script and replaced during the duplication. 686 | * @param {string} value the value to replace in the script 687 | */ 688 | 689 | /** 690 | * @typedef {Object} cloneDef 691 | * @param {Array.|replaceDef} replaces the list of values to be replaced. If this parameter is null or false, the script is not updated 692 | * @param {boolean} [duplicateApp=true] duplicates the template app. Overrides parent duplicateApp if defined 693 | * @param {boolean} [reloadApp=parent value] reload the app. Overrides parent reloadApp if defined 694 | * @param {boolean} [keepApp=parent value] keep the app when the operation finished. If false, app will be deleted. Overrides parent keepApp if defined 695 | * @param {boolean} [overwriteApp=parent value] Overwrite the replace app if it exists. If false, and if replace app exists, nothing will happen. Overrides parent replaceApp if defined 696 | * @param {boolean|string} [replaceApp=parent value] name or id of the app to replace. Can also be boolean (when false, don't replace the app, when true, replace the app which name == targetApp). Overrides parent replaceApp if defined 697 | * @param {string} [targetApp=parent value] name of the new app. Overrides parent targetApp if defined 698 | * @param {string} [publishStream] name or id of the stream to publish the new app in. If this parameter is null or false, then the app is not published. Overrides parent publishStreamId if defined 699 | * @param {boolean|Qlik.reloadTask} [createReloadTask=true] create a reload task associated to the generated app. If true, create a task with a daily trigger @ 4AM. If a reload task already exists it will be overwritten. If null or false, do nothing. Overrides parent createReloadTask if defined 700 | * @param {Object} [customProperties] custom properties to add to the newly created app. If this parameter is null or false, then the custom properties are not changed. Overrides parent customProperties if defined 701 | */ 702 | 703 | /** 704 | * Duplicates a template app, updates its script, reloads it and publishes it 705 | * 706 | * @example Simple mode 1 : duplicate a template app, replace a marker in the script, reload and publish (overwrites if target app exists in stream) 707 | * ```javascript 708 | * var task = new utils.Core.Task(); 709 | * task.start(); 710 | * 711 | * task.bind(function(task) { 712 | * console.log(task.val, task.detail); 713 | * }); 714 | * 715 | * readFile(testQlikSensePfx).then(function(pfx) { 716 | * 717 | * task.running('info', 'certificate loaded...'); 718 | * 719 | * return utils.Qlik.dynamicAppClone({ 720 | * restUri: 'http://10.20.30.40', 721 | * pfx: pfx, 722 | * UserId: 'qlikservice', 723 | * UserDirectory: '2008R2-0' 724 | * }, 725 | * { 726 | * templateApp: '3bcb8ed0-7ac5-4cd0-8913-37d1255d67c3', 727 | * replacesDef: { marker: '%Replace me!%', value: 'Employees.qvd' }, 728 | * publishStream: 'aaec8d41-5201-43ab-809f-3063750dfafd' 729 | * }, 730 | * task: task 731 | * }); 732 | * 733 | * }); 734 | * ``` 735 | * 736 | * @example Simple mode 2 : duplicate a template app, replace 3 markers in the script, reload and don't publish 737 | * ```javascript 738 | * var task = new utils.Core.Task(); 739 | * task.start(); 740 | * 741 | * task.bind(function(task) { 742 | * console.log(task.val, task.detail); 743 | * }); 744 | * 745 | * readFile(testQlikSensePfx).then(function(pfx) { 746 | * 747 | * task.running('info', 'certificate loaded...'); 748 | * 749 | * return utils.Qlik.dynamicAppClone({ 750 | * restUri: 'http://10.20.30.40', 751 | * pfx: pfx, 752 | * UserId: 'qlikservice', 753 | * UserDirectory: '2008R2-0' 754 | * }, 755 | * { 756 | * templateApp: '3bcb8ed0-7ac5-4cd0-8913-37d1255d67c3', 757 | * replacesDef: [ 758 | * { marker: '%Replace me1!%', value: 'Employees1.qvd' }, 759 | * { marker: '%Replace me2!%', value: 'Employees2.qvd' }, 760 | * { marker: '%Replace me3!%', value: 'Employees3.qvd' } 761 | * ] 762 | * }, 763 | * task: task 764 | * }); 765 | * 766 | * }); 767 | * ``` 768 | * 769 | * @example Advanced mode 1: clone a template app twice: 770 | * - first app will be called 'White App' and will have the markers replaced to 1 & 2 qvds. Don't reload it but save it. When publishing it, replace the app with name 'White App' already existing in the stream. 771 | * - second app will be called 'Black App' and will have the markers replaced to 3 & 4 qvds. Reload it. Don't save it (delete the copy). Don't publish it. 772 | * 773 | * ```javascript 774 | * var task = new utils.Core.Task(); 775 | * task.start(); 776 | * 777 | * task.bind(function(task) { 778 | * console.log(task.val, task.detail); 779 | * }); 780 | * 781 | * readFile(testQlikSensePfx).then(function(pfx) { 782 | * 783 | * task.running('info', 'certificate loaded...'); 784 | * 785 | * return utils.Qlik.dynamicAppClone({ 786 | * restUri: 'http://10.20.30.40', 787 | * pfx: pfx, 788 | * UserId: 'qlikservice', 789 | * UserDirectory: '2008R2-0' 790 | * }, 791 | * { 792 | * templateApp: '3bcb8ed0-7ac5-4cd0-8913-37d1255d67c3', 793 | * replacesDef: [ 794 | * { 795 | * replaces: [ 796 | * { marker: '%Replace me1!%', value: 'Employees1.qvd' } 797 | * { marker: '%Replace me2!%', value: 'Employees2.qvd' } 798 | * ], 799 | * replaceApp: 'White App', 800 | * reloadApp = false, 801 | * targetApp: 'White App' 802 | * }, 803 | * { 804 | * replaces: [ 805 | * { marker: '%Replace me1!%', value: 'Employees3.qvd' } 806 | * { marker: '%Replace me2!%', value: 'Employees4.qvd' } 807 | * ], 808 | * keepApp = false, 809 | * targetApp: 'Black App' 810 | * publishStream: false 811 | * } 812 | * ], 813 | * reloadApp = true, 814 | * replaceApp = false, 815 | * publishStream: 'aaec8d41-5201-43ab-809f-3063750dfafd' 816 | * }, 817 | * task: task 818 | * }); 819 | * 820 | * }); 821 | * ``` 822 | * 823 | * @memberOf Qlik 824 | * 825 | * @param {options} options Qlik Sense connection options 826 | * @param {string} params parameters for the cloning 827 | * @param {string} params.templateApp name or id of the template application 828 | * @param {int} [params.maxParDup=1] maximum number of // operations (opened sockets & QRS API queries) 829 | * @param {Array.|replaceDef|cloneDef} [params.replacesDef] definition of script replacement(s) to perform and parameters of the app to generate. If this parameter is null or false, the script is not updated 830 | * @param {boolean} [params.duplicateApp=true] duplicates the template app 831 | * @param {boolean} [params.reloadApp=true] reload the app 832 | * @param {boolean} [params.keepApp=true] keep the app when the operation finished. If false, app will be deleted 833 | * @param {boolean} [params.overwriteApp=true] Overwrite the replace app if it exists. If false, and if replace app exists, nothing will happen 834 | * @param {RegExp} [params.reloadRegex=/(.*)/] regex to track in the script reload trace. If this parameter is null or false, uses default regex that captures everything 835 | * @param {boolean|string} [params.replaceApp=true] name or id of the app to replace. Can also be boolean (when false, don't replace the app, when true, replace the app with name == targetApp) 836 | * @param {string} [params.targetApp=default] name of the new app. Defaults to %(templateAppName)s %(replace.value)s 837 | * @param {string} [params.publishStream] name or id of a stream to publish the new app in. If this parameter is null or false, then the app is not published 838 | * @param {boolean|Qlik.reloadTask} [params.createReloadTask=true] create a reload task associated to the generated app. If true, create a task with a daily trigger @ 4AM. If a reload task already exists it will be overwritten. If null or false, do nothing. 839 | * @param {Object} [params.customProperties] custom properties to add to the newly created app. If this parameter is null or false, then the custom properties are not changed. 840 | * @param {Task} task task that will trace the cloning progress 841 | * @returns {Promise} a promise that resolves when the process is finished 842 | */ 843 | function odagMixin(options, params) { 844 | 845 | var qrsApi = qrs(options); 846 | 847 | var global = this; 848 | var task = new Task(); 849 | 850 | var templateApp = params.templateApp; 851 | 852 | if (typeof templateApp == 'undefined') { 853 | return promise.reject('Please provide the templateApp parameter'); 854 | } 855 | 856 | var maxParDup = undef.if(params.maxParDup, 1); 857 | 858 | // Handles parameters default values 859 | 860 | var defaultReplaceApp = '%(targetAppName)s'; 861 | var defaultTargetApp = '%(templateAppName)s %(replaceValue)s'; 862 | var defaultCreateReloadTask = { 863 | task: { 864 | name: 'Reload for %(targetAppName)s', 865 | taskType: 0, 866 | enabled: true, 867 | taskSessionTimeout: 1440, 868 | maxRetries: 0, 869 | tags: [], 870 | app: {}, 871 | isManuallyTriggered: false, 872 | customProperties: [] 873 | }, 874 | compositeEvents: [], 875 | schemaEvents: [{ 876 | name: 'Hourly', 877 | enabled: true, 878 | eventType: 0, 879 | startDate: '2016-01-01T00:00:00.000Z', 880 | expirationDate: '9999-12-30T23:00:00.000Z', 881 | schemaFilterDescription: ['* * - * * * * *'], 882 | incrementDescription: '0 1 0 0', 883 | incrementOption: '1', 884 | privileges: ['read','update','create','delete'] 885 | }] 886 | }; 887 | 888 | var replacesDef = undef.if(params.replacesDef, false); 889 | var duplicateApp = undef.if(params.duplicateApp, true); 890 | var reloadApp = undef.if(params.reloadApp, true); 891 | var keepApp = undef.if(params.keepApp, true); 892 | var overwriteApp = undef.if(params.overwriteApp, true); 893 | var replaceFullRow = undef.if(params.replaceFullRow, false); 894 | var reloadRegex = undef.if(params.reloadRegex, /(.*)/); 895 | var replaceApp = undef.if(params.replaceApp, defaultReplaceApp); 896 | var targetApp = undef.if(params.targetApp, defaultTargetApp); 897 | var publishStream = undef.if(params.publishStream, false); 898 | var createReloadTask = undef.if(params.createReloadTask, false); 899 | var customProperties = undef.if(params.customProperties, false); 900 | 901 | // Convert simplified syntax to advanced syntax 902 | 903 | if (replacesDef && !Array.isArray(replacesDef)) { 904 | replacesDef = [replacesDef]; 905 | } 906 | 907 | replacesDef = replacesDef.map(function(item) { 908 | var retVal = {}; 909 | 910 | if (typeof item.replaces === 'undefined' && typeof item.marker !== 'undefined' && typeof item.value !== 'undefined') { 911 | var newReplace = {marker: item.marker, value: item.value}; 912 | if (typeof item.fullRow !== 'undefined') newReplace.fullRow = item.fullRow; 913 | retVal.replaces = [newReplace]; 914 | } else if (typeof item.replaces === 'undefined' && typeof item.marker === 'undefined' && typeof item.value === 'undefined') { 915 | retVal.replaces = false; 916 | } else if (typeof item.replaces !== 'undefined' && !Array.isArray(item.replaces)) { 917 | retVal.replaces = [item.replaces]; 918 | } else if (typeof item.replaces !== 'undefined' && Array.isArray(item.replaces)) { 919 | retVal.replaces = item.replaces; 920 | } else { 921 | task.running('warning', 'Unhandled replace parameter: %s', item); 922 | return undefined; 923 | } 924 | 925 | retVal.replaces.forEach(function(replace) { 926 | if (!Array.isArray(replace.value)) 927 | replace.value = [replace.value]; 928 | }); 929 | 930 | retVal.duplicateApp = undef.if(item.duplicateApp, duplicateApp); 931 | retVal.reloadApp = undef.if(item.reloadApp, reloadApp); 932 | retVal.keepApp = undef.if(item.keepApp, keepApp); 933 | retVal.overwriteApp = undef.if(item.overwriteApp, overwriteApp); 934 | retVal.replaceApp = undef.if(item.replaceApp, replaceApp); 935 | retVal.targetApp = undef.if(item.targetApp, targetApp); 936 | retVal.publishStream = undef.if(item.publishStream, publishStream); 937 | retVal.createReloadTask = undef.if(item.createReloadTask, createReloadTask); 938 | retVal.customProperties = undef.if(item.customProperties, customProperties); 939 | 940 | if (retVal.replaceApp == true) { 941 | retVal.replaceApp = defaultReplaceApp; 942 | } 943 | 944 | if (retVal.targetApp == true) { 945 | retVal.targetApp = defaultTargetApp; 946 | } 947 | 948 | if (retVal.createReloadTask == true) { 949 | retVal.createReloadTask = defaultCreateReloadTask; 950 | } 951 | 952 | if (retVal.customProperties && !Array.isArray(retVal.customProperties)) { 953 | retVal.customProperties = [retVal.customProperties]; 954 | } 955 | 956 | if (retVal.customProperties) { 957 | retVal.customProperties.forEach(function(customProperty) { 958 | if (customProperty.values && !Array.isArray(customProperty.values)) { 959 | customProperty.values = [customProperty.values]; 960 | } 961 | }); 962 | } 963 | 964 | return retVal; 965 | }).filter(function(item) { 966 | return typeof item !== 'undefined'; 967 | }); 968 | 969 | replacesDef = replacesDef.map(function(item) { 970 | return item.replaces.map(function(replace) { 971 | return replace.value.map(function(val) { 972 | return {marker: replace.marker, value: val, fullRow: ((typeof replace.fullRow !== 'undefined') ? replace.fullRow : false)}; 973 | }); 974 | }).reduce(function(pv, cv) { 975 | var retVal = []; 976 | cv.forEach(function(cvitem) { 977 | pv.forEach(function(pvitem) { 978 | retVal.push(pvitem.concat([cvitem])); 979 | }); 980 | }); 981 | 982 | return retVal; 983 | }, [[]]).map(function(replace) { 984 | return { 985 | replace: replace, 986 | duplicateApp: item.duplicateApp, 987 | reloadApp: item.reloadApp, 988 | keepApp: item.keepApp, 989 | overwriteApp: item.overwriteApp, 990 | replaceApp: item.replaceApp, 991 | targetApp: item.targetApp, 992 | publishStream: item.publishStream, 993 | createReloadTask: (item.createReloadTask) ? extend(true, {}, item.createReloadTask) : false, 994 | customProperties: (item.customProperties) ? extend(true, [], item.customProperties) : false 995 | }; 996 | }); 997 | }).reduce(function(pv, cv) { 998 | return pv.concat(cv); 999 | }); 1000 | 1001 | // If no clone task was provided, just return an empty array 1002 | 1003 | if (replacesDef.length == 0) { 1004 | task.done([]); 1005 | } else { 1006 | 1007 | // Else start the cloning process 1008 | 1009 | promise().then(function() { 1010 | 1011 | // Start 1012 | 1013 | task.running('info', 'Starting!'); 1014 | 1015 | }).then(function(reply) { 1016 | 1017 | // Connected 1018 | 1019 | return promise.all([ 1020 | global.getDocList().then(function(reply) { 1021 | task.running('info', 'Got document List'); 1022 | return reply; 1023 | }), 1024 | global.getStreamList().then(function(reply) { 1025 | task.running('info', 'Got Stream List'); 1026 | return reply; 1027 | }), 1028 | qrsApi.custompropertydefinition.get() 1029 | ]); 1030 | 1031 | }).then(function([docList, streamList, cpList]) { 1032 | 1033 | // Replace stream & app names by ids 1034 | 1035 | // Looping on replace definition 1036 | 1037 | var pendingActions = []; 1038 | var missingCpValues = {}; 1039 | var templateFound; 1040 | replacesDef.forEach(function(replaceDef) { 1041 | 1042 | // 1043 | 1044 | if (replaceDef.publishStream && !replaceDef.publishStream.match(/^[a-f0-9\-]{36}$/)) { 1045 | 1046 | task.running('info', 'Stream to publish into is an stream name (%s), trying to find the associated id', replaceDef.publishStream); 1047 | 1048 | var arrayStreamFound = streamList.filter(function(item) { 1049 | return (item.qName == replaceDef.publishStream); 1050 | }); 1051 | 1052 | if (arrayStreamFound.length > 1) { 1053 | task.running('warning', 'Several streams found : %s, using first one (%s)', arrayStreamFound.map(function(app) { 1054 | return app.qId; 1055 | }), arrayStreamFound[0].qId); 1056 | } else if (arrayStreamFound.length == 0) { 1057 | pendingActions.push(promise.reject('Stream not found: ' + replaceDef.publishStream)); 1058 | return; 1059 | } else { 1060 | task.running('info', 'Stream found'); 1061 | } 1062 | 1063 | replaceDef.publishStream = arrayStreamFound[0].qId; 1064 | 1065 | } else if (replaceDef.publishStream) { 1066 | 1067 | task.running('info', 'Stream to publish into is an id (%s), checking if it exists', replaceDef.publishStream); 1068 | 1069 | var arrayStreamFound = streamList.filter(function(item) { 1070 | return (item.qId == replaceDef.publishStream); 1071 | }); 1072 | 1073 | if (arrayStreamFound.length == 0) { 1074 | pendingActions.push(promise.reject('No :( Stream not found: ' + replaceDef.publishStream)); 1075 | return; 1076 | } else { 1077 | task.running('info', 'Yes! Stream found'); 1078 | } 1079 | 1080 | } 1081 | 1082 | // 1083 | 1084 | // 1085 | 1086 | var arrayTemplateFound = docList.filter(function(item) { 1087 | return (templateApp.match(/^[a-f0-9\-]{36}$/) && item.qDocId == templateApp) || 1088 | (!templateApp.match(/^[a-f0-9\-]{36}$/) && item.qDocName == templateApp); 1089 | }); 1090 | 1091 | if (arrayTemplateFound.length > 1) { 1092 | task.running('warning', 'Several apps found : %s, using first one (%s)', arrayTemplateFound.map(function(app) { 1093 | return app.qDocId; 1094 | }), arrayTemplateFound[0].qDocId); 1095 | } else if (arrayTemplateFound.length == 0) { 1096 | pendingActions.push(promise.reject('Template not found: ' + templateApp)); 1097 | return; 1098 | } else { 1099 | task.running('info', 'Template found'); 1100 | } 1101 | 1102 | templateFound = arrayTemplateFound[0]; 1103 | 1104 | // 1105 | 1106 | // 1107 | 1108 | var replaceDic = docList.map(function(doc) { 1109 | return {key: doc.qDocId, value: doc.qDocName}; 1110 | }).reduce(function(pv, cv) { 1111 | pv[cv.key] = cv.value; 1112 | return pv; 1113 | }, {}); 1114 | 1115 | var replaceValue = replaceDef.replace.map(function(item) { 1116 | return item.value; 1117 | }).reduce(function(pv, cv) { 1118 | return sprintf('%s %s', pv, cv); 1119 | }); 1120 | 1121 | if (replaceDef.targetApp) 1122 | replaceDef.targetApp = sprintf(replaceDef.targetApp, {templateAppId: arrayTemplateFound[0].qDocId, templateAppName: replaceDic[arrayTemplateFound[0].qDocId], replaceValue: replaceValue, replace: replaceDef.replace}); 1123 | 1124 | if (replaceDef.replaceApp) 1125 | replaceDef.replaceApp = sprintf(replaceDef.replaceApp, {templateAppId: arrayTemplateFound[0].qDocId, templateAppName: replaceDic[arrayTemplateFound[0].qDocId], replaceValue: replaceValue, replace: replaceDef.replace, targetAppName: replaceDef.targetApp}); 1126 | 1127 | if (replaceDef.createReloadTask && replaceDef.createReloadTask.task && replaceDef.createReloadTask.task.name) 1128 | replaceDef.createReloadTask.task.name = sprintf(replaceDef.createReloadTask.task.name, {templateAppId: arrayTemplateFound[0].qDocId, templateAppName: replaceDic[arrayTemplateFound[0].qDocId], replaceValue: replaceValue, replace: replaceDef.replace, targetAppName: replaceDef.targetApp}); 1129 | 1130 | if (replaceDef.customProperties) 1131 | replaceDef.customProperties.forEach(function(customProperty) { 1132 | if (customProperty.values) { 1133 | customProperty.values = customProperty.values.map(function(value) { 1134 | return sprintf(value, {templateAppId: arrayTemplateFound[0].qDocId, templateAppName: replaceDic[arrayTemplateFound[0].qDocId], replaceValue: replaceValue, replace: replaceDef.replace, targetAppName: replaceDef.targetApp}); 1135 | }); 1136 | } 1137 | }); 1138 | 1139 | // 1140 | 1141 | // 1142 | 1143 | if (replaceDef.replaceApp && !replaceDef.replaceApp.match(/^[a-f0-9\-]{36}$/)) { 1144 | 1145 | task.running('info', 'App to replace is an app name (%s), trying to find the associated id', replaceDef.replaceApp); 1146 | 1147 | var arrayAppFound = docList.filter(function(item) { 1148 | return ( 1149 | item.qDocName == replaceDef.replaceApp && 1150 | ( 1151 | (replaceDef.publishStream && item.qMeta.stream && item.qMeta.stream.id == replaceDef.publishStream) || 1152 | (!replaceDef.publishStream && !item.published) 1153 | ) 1154 | ); 1155 | }); 1156 | 1157 | if (arrayAppFound.length > 1) { 1158 | task.running('warning', 'Several apps found : %s, using first one (%s)', arrayAppFound.map(function(app) { 1159 | return app.qDocId; 1160 | }), arrayAppFound[0].qDocId); 1161 | replaceDef.replaceApp = arrayAppFound[0].qDocId; 1162 | } else if (arrayAppFound.length == 0) { 1163 | task.running('warning', 'App not found (%s)', replaceDef.replaceApp); 1164 | replaceDef.replaceApp = false; 1165 | } else { 1166 | task.running('info', 'App found'); 1167 | replaceDef.replaceApp = arrayAppFound[0].qDocId; 1168 | } 1169 | 1170 | } else if (replaceDef.replaceApp) { 1171 | 1172 | task.running('info', 'App to replace is an id (%s), checking if it exists', replaceDef.replaceApp); 1173 | 1174 | var arrayAppFound = docList.filter(function(item) { 1175 | return (item.qDocId == replaceDef.replaceApp && item.qMeta.stream && item.qMeta.stream.id == replaceDef.publishStream); 1176 | }); 1177 | 1178 | if (arrayAppFound.length == 0) { 1179 | task.running('warning', 'App not found (%s)', replaceDef.replaceApp); 1180 | replaceDef.replaceApp = false; 1181 | } else { 1182 | task.running('info', 'App found'); 1183 | } 1184 | 1185 | } 1186 | 1187 | // 1188 | 1189 | // 1190 | 1191 | if (replaceDef.customProperties) { 1192 | 1193 | replaceDef.customProperties.forEach(function(customProperty) { 1194 | 1195 | var arrayCpFound = cpList.filter(function(item) { 1196 | return (item.name == customProperty.name && !customProperty.name.match(/^[a-f0-9\-]{36}$/)) || 1197 | (item.id == customProperty.name && customProperty.name.match(/^[a-f0-9\-]{36}$/)); 1198 | }); 1199 | 1200 | if (arrayCpFound.length > 1) { 1201 | task.running('warning', 'Several matching custom properties found : %s, using first one (%s)', arrayCpFound.map(function(cp) { 1202 | return cp.id; 1203 | }), arrayCpFound[0].id); 1204 | } else if (arrayCpFound.length == 0) { 1205 | pendingActions.push(promise.reject('Custom property not found: ' + customProperty.name)); 1206 | return; 1207 | } else { 1208 | task.running('info', 'Custom property found'); 1209 | } 1210 | 1211 | customProperty.name = arrayCpFound[0].id; 1212 | 1213 | if (typeof missingCpValues[arrayCpFound[0].id] === 'undefined') { 1214 | missingCpValues[arrayCpFound[0].id] = []; 1215 | } 1216 | 1217 | var missingValues = missingCpValues[arrayCpFound[0].id]; 1218 | customProperty.values.forEach(function(value) { 1219 | if (arrayCpFound[0].choiceValues.indexOf(value) < 0 && missingValues.indexOf(value) < 0) { 1220 | missingValues.push(value); 1221 | } 1222 | }); 1223 | }); 1224 | 1225 | } 1226 | 1227 | // 1228 | 1229 | }); 1230 | 1231 | Object.keys(missingCpValues).forEach(function(cpkey) { 1232 | var missingValues = missingCpValues[cpkey]; 1233 | 1234 | var addToCp = qrsApi.custompropertydefinition.id(cpkey).get().then(function(cp) { 1235 | 1236 | if (cp.objectTypes.indexOf('App') < 0) { 1237 | return promise.reject('Custom property is not applicable to Apps...'); 1238 | } 1239 | 1240 | if (missingValues.length > 0) { 1241 | task.running('warning', 'Adding missing values in custom property: %s', missingValues); 1242 | cp.choiceValues = cp.choiceValues.concat(missingValues); 1243 | return qrsApi.custompropertydefinition.id(cpkey).put(cp); 1244 | } 1245 | 1246 | }); 1247 | 1248 | pendingActions.push(addToCp); 1249 | }); 1250 | 1251 | return promise.all(pendingActions).then(function() { 1252 | return promise.all([ 1253 | templateFound.qDocId, 1254 | templateFound.qDocName 1255 | ]); 1256 | }); 1257 | 1258 | }).then(function([templateId, templateName]) { 1259 | 1260 | // Clone app 1261 | 1262 | var newAppsStep = promise.resolve([]); 1263 | var newApps = []; 1264 | arrayDivide(replacesDef, maxParDup).forEach(function(scriptReplacesChunk) { 1265 | var newAppsChunk = []; 1266 | 1267 | scriptReplacesChunk.forEach(function(scriptReplace) { 1268 | newAppsChunk.push( 1269 | function() { 1270 | 1271 | if (!scriptReplace.duplicateApp) { 1272 | 1273 | task.running('warning', 'Working directly on template app (%s)!', templateId); 1274 | 1275 | return qrsApi.app.id(templateId).get().then(function(reply) { 1276 | scriptReplace.publishStream = (reply.published ? false : scriptReplace.publishStream); 1277 | scriptReplace.replaceApp = false; 1278 | 1279 | return { 1280 | scriptReplace: scriptReplace, 1281 | clonedApp: reply 1282 | }; 1283 | }); 1284 | 1285 | } else if (scriptReplace.replaceApp && !scriptReplace.overwriteApp) { 1286 | 1287 | task.running('info', 'App already exists! Working on existing app (%s)', scriptReplace.replaceApp); 1288 | 1289 | return qrsApi.app.id(scriptReplace.replaceApp).get().then(function(reply) { 1290 | scriptReplace.publishStream = (reply.published ? false : scriptReplace.publishStream); 1291 | scriptReplace.replaceApp = false; 1292 | 1293 | return { 1294 | scriptReplace: scriptReplace, 1295 | clonedApp: reply 1296 | }; 1297 | }); 1298 | 1299 | } else { 1300 | 1301 | var targetAppName = scriptReplace.targetApp; 1302 | task.running('info', 'Generating clone named \'%s\'', targetAppName); 1303 | 1304 | return qrsApi.app.id(templateId).copy.post(undefined, {name: targetAppName}).then(function(clonedApp) { 1305 | task.running('info', 'Clone named \'%s\' created successfully with id: %s', targetAppName, clonedApp.id); 1306 | return { 1307 | scriptReplace: scriptReplace, 1308 | clonedApp: clonedApp 1309 | }; 1310 | }); 1311 | } 1312 | } 1313 | ); 1314 | }); 1315 | 1316 | newAppsStep = newAppsStep.then(function(reply) { 1317 | newApps = newApps.concat(reply); 1318 | return promise.all(newAppsChunk.map(function(promiseFactory) { 1319 | return promiseFactory(); 1320 | })); 1321 | }); 1322 | 1323 | }); 1324 | 1325 | return newAppsStep.then(function(reply) { 1326 | return newApps.concat(reply); 1327 | }); 1328 | 1329 | }).then(function(newApps) { 1330 | 1331 | // Perform modifications on clone 1332 | 1333 | // Replace script marker and reload in // 1334 | // Only if marker is not null! 1335 | 1336 | var newAppsScriptReplacedStep = promise.resolve([]); 1337 | var newAppsScriptReplaced = []; 1338 | arrayDivide(newApps, maxParDup).forEach(function(newAppsChunk) { 1339 | 1340 | var newAppsScriptReplacedChunk = []; 1341 | newAppsChunk.forEach(function(newApp) { 1342 | 1343 | newAppsScriptReplacedChunk.push( 1344 | function() { 1345 | 1346 | var step = promise(); 1347 | 1348 | // If nothing to do just return 1349 | 1350 | if (!(newApp.scriptReplace.replace || newApp.scriptReplace.reloadApp || newApp.scriptReplace.publishStream)) return promise.resolve([]); 1351 | 1352 | // If anything to do, open socket 1353 | 1354 | if (newApp.scriptReplace.replace || newApp.scriptReplace.reloadApp) { 1355 | 1356 | step = step.then(function() { 1357 | 1358 | return global.openDoc(newApp.clonedApp.id).then(function(reply) { 1359 | task.running('info', {id: newApp.clonedApp.id, msg: 'Doc opened'}); 1360 | return reply; 1361 | }) 1362 | 1363 | }); 1364 | 1365 | } 1366 | 1367 | // If a replace is required, do it! 1368 | 1369 | if (newApp.scriptReplace.replace) { 1370 | 1371 | step = step.then(function(clonedApp) { 1372 | 1373 | // Replace the script marker by the replace value 1374 | 1375 | task.running('info', {id: newApp.clonedApp.id, msg: 'Application opened'}); 1376 | 1377 | return promise().then(function() { 1378 | return clonedApp.getScript(); 1379 | }).then(function(result) { 1380 | task.running('info', {id: newApp.clonedApp.id, msg: 'Application script extracted'}); 1381 | 1382 | var newScript = result; 1383 | newApp.scriptReplace.replace.forEach(function(replace) { 1384 | if (replace.fullRow) { 1385 | var re = new RegExp(sprintf('^.*%s.*$', replace.marker), 'm'); 1386 | newScript = newScript.replace(re, sprintf('%s // %s', replace.value, replace.marker)); 1387 | } else { 1388 | newScript = newScript.replace(replace.marker, replace.value); 1389 | } 1390 | }); 1391 | 1392 | return clonedApp.setScript(newScript); 1393 | }).then(function() { 1394 | task.running('info', {id: newApp.clonedApp.id, msg: 'Application script replaced'}); 1395 | return clonedApp.doSave(); 1396 | }).then(function() { 1397 | task.running('info', {id: newApp.clonedApp.id, msg: 'Application saved'}); 1398 | return clonedApp; 1399 | }) 1400 | 1401 | }); 1402 | 1403 | } 1404 | 1405 | // If a reload is required, do it! 1406 | 1407 | if (newApp.scriptReplace.reloadApp) { 1408 | 1409 | step = step.then(function(clonedApp) { 1410 | 1411 | // Reload and monitor reload progress 1412 | 1413 | var timer = setInterval(function() { 1414 | global.getProgress(0).then(function(result) { 1415 | if (result.qPersistentProgress) { 1416 | var rePattern = new RegExp(reloadRegex); 1417 | var match = rePattern.exec(result.qPersistentProgress); 1418 | while (match != null) { 1419 | task.running('reload', {id: newApp.clonedApp.id, msg: match[1]}); 1420 | match = rePattern.exec(result.qPersistentProgress); 1421 | } 1422 | } 1423 | }); 1424 | }, 1000); 1425 | 1426 | return clonedApp.doReload().then(function(result) { 1427 | if (result) { 1428 | clearInterval(timer); 1429 | 1430 | task.running('info', {id: newApp.clonedApp.id, msg: 'Application reloaded'}); 1431 | 1432 | if (newApp.clonedApp.published) { 1433 | 1434 | task.running('info', {id: newApp.clonedApp.id, msg: 'Save not needed!'}); 1435 | return clonedApp; 1436 | 1437 | } else { 1438 | 1439 | return clonedApp.doSave().then(function() { 1440 | 1441 | task.running('info', {id: newApp.clonedApp.id, msg: 'Application saved'}); 1442 | return clonedApp; 1443 | 1444 | }); 1445 | 1446 | } 1447 | } else { 1448 | 1449 | return clonedApp.doSave().then(function() { 1450 | 1451 | return promise.reject({id: newApp.clonedApp.id, msg: 'Application not reloaded'}); 1452 | 1453 | }); 1454 | 1455 | 1456 | } 1457 | }); 1458 | 1459 | }); 1460 | } 1461 | 1462 | // If a publish is required, do it! 1463 | 1464 | if (newApp.scriptReplace.publishStream) { 1465 | 1466 | step = step.then(function(clonedApp) { 1467 | 1468 | // Publish app in the given stream 1469 | 1470 | if (!newApp.scriptReplace.replaceApp) { 1471 | 1472 | // No app to replace 1473 | 1474 | return clonedApp.publish(newApp.scriptReplace.publishStream).then(function() { 1475 | task.running('info', {id: newApp.clonedApp.id, msg: 'Application published'}); 1476 | return clonedApp; 1477 | }) 1478 | 1479 | } else { 1480 | 1481 | // There was an app to replace. We need to use QRS API to publish it 1482 | 1483 | return qrsApi.app.id(newApp.clonedApp.id).replace.put(undefined, {app: newApp.scriptReplace.replaceApp}).then(function(repApp) { 1484 | 1485 | task.running('info', {id: newApp.clonedApp.id, msg: 'Application published (replaced)'}); 1486 | 1487 | return qrsApi.app.id(newApp.clonedApp.id).delete().then(function() { 1488 | task.running('info', {id: newApp.clonedApp.id, msg: 'Removed unpublished app'}); 1489 | 1490 | newApp.clonedApp = repApp; 1491 | 1492 | return; 1493 | }); 1494 | }); 1495 | } 1496 | }); 1497 | 1498 | } else { 1499 | 1500 | // Don't publish app. Let's check if we need to replace an unpublished app 1501 | 1502 | if (newApp.scriptReplace.replaceApp && !newApp.clonedApp.published) { 1503 | 1504 | step = step.then(function() { 1505 | 1506 | return qrsApi.app.id(newApp.scriptReplace.replaceApp).delete().then(function() { 1507 | task.running('info', {id: newApp.clonedApp.id, msg: 'Removed unpublished app'}); 1508 | return; 1509 | }); 1510 | 1511 | }); 1512 | 1513 | } 1514 | 1515 | } 1516 | 1517 | // If it is not required to keep the app after all this process (who knows...), just delete it! 1518 | 1519 | if (!newApp.scriptReplace.keepApp) { 1520 | 1521 | step = step.then(function() { 1522 | 1523 | // Delete app 1524 | 1525 | return qrsApi.app.id(newApp.clonedApp.id).delete().then(function() { 1526 | task.running('info', {id: newApp.clonedApp.id, msg: 'Removed cloned app'}); 1527 | return; 1528 | }); 1529 | 1530 | }); 1531 | 1532 | } else { 1533 | 1534 | // Else we can check the reload task 1535 | 1536 | if (newApp.scriptReplace.createReloadTask) { 1537 | 1538 | step = step.then(function() { 1539 | 1540 | return qrsApi.reloadtask.full.get().then(function(tasks) { 1541 | return tasks.filter(function(task) { 1542 | return task.app.id == newApp.clonedApp.id; 1543 | }); 1544 | }).then(function(reply) { 1545 | 1546 | if (reply.length > 0) { 1547 | task.running('warning', {id: newApp.clonedApp.id, msg: 'Reload task already exists on this app. Ignoring reload task creation...'}); 1548 | } else { 1549 | task.running('info', {id: newApp.clonedApp.id, msg: 'Creating new reload task'}); 1550 | 1551 | newApp.scriptReplace.createReloadTask.task.app.id = newApp.clonedApp.id; 1552 | 1553 | return qrsApi.reloadtask.create.post(newApp.scriptReplace.createReloadTask).then(function(reloadTask) { 1554 | task.running('info', {id: newApp.clonedApp.id, msg: 'Reload task created'}); 1555 | }); 1556 | 1557 | } 1558 | 1559 | }); 1560 | 1561 | }); 1562 | 1563 | } 1564 | 1565 | // And the custom properties 1566 | 1567 | if (newApp.scriptReplace.customProperties) { 1568 | 1569 | step = step.then(function() { 1570 | 1571 | return qrsApi.app.id(newApp.clonedApp.id).get() 1572 | 1573 | }).then(function(appDef) { 1574 | 1575 | appDef.customProperties = newApp.scriptReplace.customProperties.map(function(customProperty) { 1576 | return customProperty.values.map(function(value) { 1577 | return { 1578 | value: value, 1579 | definition: { 1580 | id: customProperty.name 1581 | } 1582 | }; 1583 | }); 1584 | }).reduce(function(pv, cv) { 1585 | return pv.concat(cv); 1586 | }); 1587 | 1588 | return qrsApi.app.id(newApp.clonedApp.id).put(appDef); 1589 | 1590 | }); 1591 | 1592 | } 1593 | } 1594 | 1595 | step = step.then(function() { 1596 | task.running('odag', newApp.clonedApp.id); 1597 | return newApp.clonedApp.id; 1598 | }); 1599 | 1600 | return step; 1601 | 1602 | } 1603 | ); 1604 | 1605 | }); 1606 | 1607 | newAppsScriptReplacedStep = newAppsScriptReplacedStep.then(function(reply) { 1608 | newAppsScriptReplaced = newAppsScriptReplaced.concat(reply); 1609 | return promise.all(newAppsScriptReplacedChunk.map(function(promiseFactory) { 1610 | return promiseFactory(); 1611 | })); 1612 | }); 1613 | 1614 | }); 1615 | 1616 | return newAppsScriptReplacedStep.then(function(reply) { 1617 | return newAppsScriptReplaced.concat(reply); 1618 | }); 1619 | 1620 | }).then(function(val) { 1621 | 1622 | task.done(val); 1623 | 1624 | }).fail(function(err) { 1625 | 1626 | task.failed(err); 1627 | return promise.reject(err); 1628 | 1629 | }); 1630 | 1631 | } 1632 | 1633 | if (returnObservable) { 1634 | return task; 1635 | } else { 1636 | return task.toPromise(extPromise); 1637 | } 1638 | 1639 | } 1640 | 1641 | function loopAndReloadMixin(options, params) { 1642 | 1643 | var global = this; 1644 | var task = new Task(); 1645 | 1646 | var loopColumn = params.loop.loopColumn; 1647 | var reduceColumn = params.loop.reduceColumn; 1648 | var nameColumn = params.loop.nameColumn; 1649 | var publishColumn = params.loop.publishColumn; 1650 | 1651 | var dimensions = { 1652 | loopColumn: {name: loopColumn, dimensionType: undef.if(loopColumn, 'AUTO', 'IGNORE')}, 1653 | reduceColumn: {name: reduceColumn, dimensionType: undef.if(reduceColumn, 'AUTO', 'IGNORE')}, 1654 | nameColumn: {name: nameColumn, dimensionType: undef.if(nameColumn, 'AUTO', 'IGNORE')}, 1655 | publishColumn: {name: publishColumn, dimensionType: undef.if(publishColumn, 'AUTO', 'IGNORE')} 1656 | }; 1657 | 1658 | var retVal = global.openDoc(params.loop.appId).then((app) => { 1659 | task.running('Doc opened'); 1660 | return Rx.Observable.from(app.export(dimensions)).toPromise(promise.promise); 1661 | 1662 | }).then(function(reply) { 1663 | task.running('Export done'); 1664 | 1665 | var dynamicCloneParams = extend(true, {}, params.reload, { 1666 | maxParDup: undef.child(params, ['reload', 'maxParDup'], 5), 1667 | replacesDef: reply.map(function(item) { 1668 | return { 1669 | replaces: undef.child(params, ['loopReload', 'addReplaces'], []).concat( 1670 | [{ 1671 | marker: undef.child(params, ['loopReload', 'loopMarker'], '%Replace me!%'), 1672 | value: undef.if(reduceColumn, item[1], undefined) 1673 | }] 1674 | ), 1675 | targetApp: undef.if(nameColumn, item[2], undefined), 1676 | publishStream: undef.if(publishColumn, item[3], undefined) 1677 | }; 1678 | }), 1679 | overwriteApp: undef.child(params, ['reload', 'overwriteApp'], false) 1680 | }); 1681 | 1682 | }); 1683 | 1684 | return Rx.Observable.from(global.odag(options, dynamicCloneParams)).toPromise(promise.promise); 1685 | 1686 | } 1687 | 1688 | return retVal; 1689 | } 1690 | 1691 | 1692 | 1693 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qlik-utils", 3 | "version": "2.0.60", 4 | "description": "a set of utility functions to deal with Qlik APIs", 5 | "main": "index.js", 6 | "author": "pouc (https://github.com/pouc)", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/pouc/qlik-utils.git" 11 | }, 12 | "scripts": { 13 | "test": "mocha && istanbul cover node_modules/mocha/bin/_mocha" 14 | }, 15 | "bugs": "https://github.com/pouc/qlik-utils/issues", 16 | "dependencies": { 17 | "array-divide": "^2.0.0", 18 | "extend": "^3.0.0", 19 | "ifnotundef": "^1.1.3", 20 | "map-obj": "^2.0.0", 21 | "object.values": "^1.0.4", 22 | "q": "^1.4.1", 23 | "qlik-api-qps": "^1.1.10", 24 | "qlik-api-qrs": "^1.1.7", 25 | "rxjs": "^5.1.0", 26 | "rxjs-task-subject": "^1.1.9", 27 | "sprintf-js": "^1.0.3" 28 | }, 29 | "devDependencies": { 30 | "chai": "^3.5.0", 31 | "chai-as-promised": "^6.0.0", 32 | "chai-string": "^1.3.0", 33 | "chai-things": "^0.2.0", 34 | "coveralls": "^2.11.16", 35 | "delay": "^1.3.1", 36 | "enigma.js": "^1.0.1", 37 | "grunt": "^1.0.1", 38 | "grunt-bump": "^0.8.0", 39 | "grunt-coveralls": "^1.0.1", 40 | "grunt-jscs": "^3.0.1", 41 | "grunt-jsdoc-to-markdown": "^2.0.0", 42 | "grunt-mocha-istanbul": "^5.0.2", 43 | "grunt-shell": "^2.1.0", 44 | "grunt-simple-mocha": "^0.4.1", 45 | "istanbul": "^0.4.5", 46 | "mocha": "^3.2.0", 47 | "mocha-lcov-reporter": "^1.2.0", 48 | "q": "^1.4.1", 49 | "qlik-fake-proxy": "^1.1.27", 50 | "rxjs-task-subject": "^1.1.10", 51 | "sinon": "^1.17.7", 52 | "sinon-chai": "^2.8.0", 53 | "sinon-chai-in-order": "^0.1.0", 54 | "util": "^0.10.3", 55 | "ws": "^2.0.3" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var promise = require('q'); 3 | var chai = require('chai'); 4 | var sinon = require('sinon'); 5 | var ws = require('ws'); 6 | 7 | var util = require('util'); 8 | var delay = require('delay'); 9 | 10 | chai.use(require('chai-as-promised')); 11 | chai.use(require('sinon-chai')); 12 | chai.use(require('chai-things')); 13 | chai.use(require('chai-string')); 14 | 15 | var expect = chai.expect; 16 | var should = chai.should(); 17 | 18 | var exports = require('../index.js'); 19 | 20 | var Task = require('rxjs-task-subject'); 21 | var proxy = require('qlik-fake-proxy'); 22 | var qrs = require('qlik-api-qrs'); 23 | 24 | var enigma = require('enigma.js'); 25 | var qixSchema = require('../node_modules/enigma.js/schemas/qix/3.1/schema.json'); 26 | 27 | var readFile = promise.denodeify(fs.readFile); 28 | 29 | function check(done, f) { 30 | 31 | return promise().then(() => { 32 | try { 33 | return f(); 34 | } catch (e) { 35 | return promise.reject(e); 36 | } 37 | }).then(() => { 38 | done(); 39 | }).fail((err) => { 40 | done(err); 41 | }); 42 | 43 | } 44 | 45 | var hooks = { 46 | params: {}, 47 | none: { 48 | before: () => {}, 49 | after: () => {} 50 | }, 51 | fake: { 52 | before: (done) => { 53 | promise.all([ 54 | proxy.createProxy({ port: 1337, secure: true }), 55 | readFile('./node_modules/qlik-fake-proxy/certs/client_key.pem'), 56 | readFile('./node_modules/qlik-fake-proxy/certs/client.pem'), 57 | readFile('./node_modules/qlik-fake-proxy/certs/root.pem'), 58 | ]).then((reply) => { 59 | check(done, function() { 60 | hooks.params.server = reply[0]; 61 | hooks.params.options = { 62 | qpsRestUri: `https://localhost:${reply[0].address().port}/qps`, 63 | qrsRestUri: `https://localhost:${reply[0].address().port}/qrs`, 64 | headers: { 65 | 'X-Qlik-User': `UserDirectory=qlik;UserId=lft`, 66 | }, 67 | wsPort: reply[0].address().port, 68 | key: reply[1], 69 | cert: reply[2], 70 | ca: reply[3] 71 | }; 72 | }); 73 | }).fail((err) => { 74 | done(err); 75 | }); 76 | }, 77 | after: (done) => { 78 | check(done, function() { 79 | hooks.params.server.close(); 80 | }); 81 | } 82 | }, 83 | real: { 84 | before: (done) => { 85 | promise.all([ 86 | readFile('./test/certs/client_key.pem'), 87 | readFile('./test/certs/client.pem'), 88 | readFile('./test/certs/root.pem'), 89 | ]).then((reply) => { 90 | check(done, function() { 91 | hooks.params.options = { 92 | qpsRestUri: 'https://localhost:4243/qps', 93 | qrsRestUri: 'https://localhost:4242/qrs', 94 | headers: { 95 | 'X-Qlik-User': `UserDirectory=DESKTOP-GRJ2NM5;UserId=qlikadmin`, 96 | }, 97 | timeout: 60000, 98 | wsPort: 4747, 99 | key: reply[0], 100 | cert: reply[1], 101 | ca: reply[2] 102 | }; 103 | }); 104 | }).fail((err) => { 105 | done(err); 106 | }); 107 | }, 108 | after: () => {} 109 | } 110 | } 111 | 112 | var hooksConfig = 'real'; 113 | 114 | describe('default conf...', () => { 115 | 116 | var utils = exports; 117 | 118 | beforeEach(hooks[hooksConfig].before); 119 | afterEach(hooks[hooksConfig].after); 120 | 121 | describe('getTicket...', () => { 122 | 123 | it('should be defined', function() { 124 | expect(utils.getTicket).to.not.be.undefined; 125 | }); 126 | 127 | it('should get ticket', function() { 128 | return utils.getTicket(hooks.params.options, { 129 | UserId: 'lft', 130 | UserDirectory: 'qlik', 131 | Attributes: [] 132 | }).should.eventually.have.property('Ticket').to.match(/^[\-_\.a-zA-Z0-9]*$/).to.have.length(16); 133 | }); 134 | 135 | it('should get ticket with fake targetId', function() { 136 | return utils.getTicket(hooks.params.options, { 137 | UserId: 'lft', 138 | UserDirectory: 'qlik', 139 | TargetId: 'this is fake', 140 | Attributes: [] 141 | }).should.eventually.have.property('Ticket').to.match(/^[\-_\.a-zA-Z0-9]*$/).to.have.length(16); 142 | }); 143 | 144 | it('should not get ticket if no UserId', function() { 145 | return utils.getTicket(hooks.params.options, { 146 | UserDirectory: 'qlik', 147 | Attributes: [] 148 | }).should.eventually.be.rejectedWith('Required property 'UserId' not found in JSON'); 149 | }); 150 | 151 | it('should not get ticket if no UserDirectory', function() { 152 | return utils.getTicket(hooks.params.options, { 153 | UserId: 'lft', 154 | Attributes: [] 155 | }).should.eventually.be.rejectedWith('Required property 'UserDirectory' not found in JSON'); 156 | }); 157 | 158 | }); 159 | 160 | describe('addToWhiteList...', () => { 161 | 162 | function randomInt(low, high) { 163 | return Math.floor(Math.random() * (high - low) + low); 164 | } 165 | 166 | it('should be defined', () => { 167 | expect(utils.addToWhiteList).to.not.be.undefined; 168 | }); 169 | 170 | 171 | 172 | it('should add to whitelist', function(done) { 173 | 174 | var qrsApi = qrs(hooks.params.options); 175 | 176 | var randomIp = util.format('10.0.%s.%s', randomInt(0, 254), randomInt(0, 254)); 177 | 178 | utils.addToWhiteList(hooks.params.options, { 179 | ip: randomIp 180 | }).then(function(reply) { 181 | 182 | reply.should.all.have.property('websocketCrossOriginWhiteList'); 183 | reply.forEach(function(item) { 184 | if (typeof item.websocketCrossOriginWhiteList !== 'undefined') { 185 | item.websocketCrossOriginWhiteList.should.include(randomIp); 186 | } 187 | }); 188 | 189 | }).should.notify(done); 190 | }); 191 | 192 | it('should add to whitelist with index filter', function(done) { 193 | var randomIp = util.format('10.0.%s.%s', randomInt(0, 254), randomInt(0, 254)); 194 | 195 | utils.addToWhiteList(hooks.params.options, { 196 | ip: randomIp, 197 | vp: 0 198 | }).then(function(reply) { 199 | 200 | reply.should.all.have.property('websocketCrossOriginWhiteList'); 201 | reply.forEach(function(item) { 202 | if (typeof item.websocketCrossOriginWhiteList !== 'undefined') { 203 | item.websocketCrossOriginWhiteList.should.include(randomIp); 204 | } 205 | }); 206 | 207 | }).should.notify(done); 208 | }); 209 | 210 | it('should add to whitelist with prefix filter', function(done) { 211 | var randomIp = util.format('10.0.%s.%s', randomInt(0, 254), randomInt(0, 254)); 212 | 213 | utils.addToWhiteList(hooks.params.options, { 214 | ip: randomIp, 215 | vp: '/' 216 | }).then(function(reply) { 217 | 218 | reply.should.all.have.property('websocketCrossOriginWhiteList'); 219 | reply.forEach(function(item) { 220 | if (typeof item.websocketCrossOriginWhiteList !== 'undefined') { 221 | item.websocketCrossOriginWhiteList.should.include(randomIp); 222 | } 223 | }); 224 | 225 | }).should.notify(done); 226 | }); 227 | 228 | it('should delay 5s', function() { 229 | this.timeout(10000); 230 | return delay(5000); 231 | }); 232 | 233 | }); 234 | 235 | describe('mixins...', () => { 236 | 237 | it('should be defined', function() { 238 | expect(utils.mixins).to.not.be.undefined; 239 | }); 240 | 241 | it('enigma should accept mixin', function() { 242 | this.timeout(30000); 243 | 244 | var config = { 245 | schema: qixSchema, 246 | host: 'https://localhost', 247 | session: { 248 | route: 'app/engineData', 249 | host: 'localhost', 250 | port: hooks.params.options.wsPort, 251 | unsecure: false, 252 | disableCache: true 253 | }, 254 | mixins: [utils.mixins.Doc], 255 | listeners: { 256 | "notification:OnAuthenticationInformation": ( authInfo ) => { 257 | console.log( authInfo ); 258 | }}, 259 | createSocket(url) { 260 | var sock = new ws(url, { 261 | ca: [hooks.params.options.ca], 262 | key: hooks.params.options.key, 263 | cert: hooks.params.options.cert, 264 | headers: hooks.params.options.headers, 265 | rejectUnauthorized: false 266 | }); 267 | return sock; 268 | }, 269 | }; 270 | 271 | return enigma.getService('qix', config).then((qix) => { 272 | return qix.global.getDocList().then((docList) => { 273 | return promise.all( 274 | docList.slice(0, 5).map((doc) => { 275 | return qix.global.openDoc(doc.qDocId).then(app => { 276 | return app.getSheets(); 277 | }); 278 | }) 279 | ); 280 | }); 281 | 282 | }).then((sh) => { 283 | return promise.all( 284 | expect(sh).to.be.a('array'), 285 | expect(sh[0]).to.be.a('array'), 286 | expect(sh[0][0]).to.have.deep.property('qInfo.qType').to.equal('sheet') 287 | ) 288 | }) 289 | 290 | }); 291 | 292 | }) 293 | 294 | }); 295 | 296 | describe('Observable...', () => { 297 | 298 | var utils = exports.create({returnObservable: true}); 299 | 300 | beforeEach(hooks[hooksConfig].before); 301 | afterEach(hooks[hooksConfig].after); 302 | 303 | describe('getTicket...', () => { 304 | 305 | it('should be defined', function() { 306 | expect(utils.getTicket).to.not.be.undefined; 307 | }); 308 | 309 | 310 | it('should get ticket', function(done) { 311 | var cb = sinon.spy(); 312 | 313 | var ticket = utils.getTicket(hooks.params.options, { 314 | UserId: 'lft', 315 | UserDirectory: 'qlik', 316 | Attributes: [] 317 | }); 318 | 319 | ticket.subscribe( 320 | (val) => cb(val), 321 | (err) => done(err), 322 | () => { 323 | check(done, () => { 324 | expect(cb).to.have.been.calledOnce; 325 | expect(ticket.info.val).to.have.property('Ticket').to.match(/^[\-_\.a-zA-Z0-9]*$/).to.have.length(16); 326 | }); 327 | } 328 | ); 329 | 330 | }); 331 | 332 | it('should get ticket with fake targetId', function(done) { 333 | 334 | var cb = sinon.spy(); 335 | 336 | var ticket = utils.getTicket(hooks.params.options, { 337 | UserId: 'lft', 338 | UserDirectory: 'qlik', 339 | TargetId: 'this is fake', 340 | Attributes: [] 341 | }); 342 | 343 | ticket.subscribe( 344 | (val) => cb(val), 345 | (err) => done(err), 346 | () => { 347 | check(done, () => { 348 | expect(cb).to.have.been.calledTwice; 349 | 350 | expect(cb.getCall(0).args[0]).to.be.an.instanceof(Task.TaskSubjectInfo); 351 | expect(cb.getCall(0).args[0]).to.have.property('status').to.equal('running'); 352 | expect(cb.getCall(0).args[0]).to.have.property('val').to.equal('warning'); 353 | expect(cb.getCall(0).args[0]).to.have.property('detail').to.equal('Wrong targetId: \'this is fake\', generating a ticket to default location'); 354 | 355 | expect(cb.getCall(1).args[0]).to.be.an.instanceof(Task.TaskSubjectInfo); 356 | expect(cb.getCall(1).args[0]).to.have.property('status').to.equal('running'); 357 | 358 | expect(cb.getCall(1).args[0]).to.have.property('val').to.have.property('UserId').to.equalIgnoreCase('lft'); 359 | expect(cb.getCall(1).args[0]).to.have.property('val').to.have.property('UserDirectory').to.equalIgnoreCase('qlik'); 360 | expect(cb.getCall(1).args[0]).to.have.property('val').to.have.property('Ticket').to.match(/^[\-_\.a-zA-Z0-9]*$/).to.have.length(16); 361 | 362 | expect(ticket.info.val).to.have.property('Ticket').to.match(/^[\-_\.a-zA-Z0-9]*$/).to.have.length(16); 363 | }); 364 | } 365 | ); 366 | }); 367 | 368 | it('should not get ticket if no UserId', function(done) { 369 | 370 | var cb = sinon.spy(); 371 | 372 | var ticket = utils.getTicket(hooks.params.options, { 373 | UserDirectory: 'qlik', 374 | Attributes: [] 375 | }); 376 | 377 | ticket.subscribe( 378 | (val) => done(val), 379 | (err) => { 380 | check(done, () => { 381 | expect(cb).to.not.have.been.called; 382 | 383 | expect(err).to.be.an.instanceof(Task.TaskSubjectInfo); 384 | 385 | expect(err).to.have.property('status').to.equal('failed'); 386 | expect(err).to.have.property('val').to.be.an.instanceof(Error); 387 | expect(err).to.have.property('val').to.have.property('message').to.startWith('Required property 'UserId' not found in JSON'); 388 | }); 389 | }, 390 | () => done('Error') 391 | ); 392 | 393 | }); 394 | 395 | it('should not get ticket if no UserDirectory', function(done) { 396 | 397 | var cb = sinon.spy(); 398 | 399 | var ticket = utils.getTicket(hooks.params.options, { 400 | UserId: 'lft', 401 | Attributes: [] 402 | }); 403 | 404 | ticket.subscribe( 405 | (val) => done(val), 406 | (err) => { 407 | check(done, () => { 408 | expect(cb).to.not.have.been.called; 409 | 410 | expect(err).to.be.an.instanceof(Task.TaskSubjectInfo); 411 | 412 | expect(err).to.have.property('status').to.equal('failed'); 413 | expect(err).to.have.property('val').to.be.an.instanceof(Error); 414 | expect(err).to.have.property('val').to.have.property('message').to.startWith('Required property 'UserDirectory' not found in JSON'); 415 | }); 416 | }, 417 | () => done('Error') 418 | ); 419 | 420 | }); 421 | 422 | 423 | }); 424 | 425 | describe('mixins...', () => { 426 | 427 | it('should be defined', function() { 428 | expect(utils.mixins).to.not.be.undefined; 429 | }); 430 | 431 | describe('Global...', () => { 432 | 433 | it('should be defined', function() { 434 | expect(utils.mixins.Global).to.not.be.undefined; 435 | }); 436 | 437 | describe('odag', function () { 438 | 439 | it('should authorize simple config 1', function (done) { 440 | this.timeout(30000); 441 | 442 | var qrsApi = qrs(hooks.params.options); 443 | 444 | var config = { 445 | schema: qixSchema, 446 | Promise: promise.promise, 447 | host: 'https://localhost', 448 | session: { 449 | route: 'app/engineData', 450 | host: 'localhost', 451 | port: hooks.params.options.wsPort, 452 | unsecure: false, 453 | disableCache: true 454 | }, 455 | mixins: [utils.mixins.Global], 456 | listeners: { 457 | "notification:OnAuthenticationInformation": ( authInfo ) => { 458 | console.log( authInfo ); 459 | }}, 460 | createSocket(url) { 461 | var sock = new ws(url, { 462 | ca: [hooks.params.options.ca], 463 | key: hooks.params.options.key, 464 | cert: hooks.params.options.cert, 465 | headers: hooks.params.options.headers, 466 | rejectUnauthorized: false 467 | }); 468 | return sock; 469 | }, 470 | }; 471 | 472 | enigma.getService('qix', config).then((qix) => { 473 | var odag = qix.global.odag(hooks.params.options, { 474 | templateApp: 'a356af1d-0a20-44e0-a152-ade3e56b16b9', 475 | replacesDef: { marker: '%Replace me!%', value: 1 }, 476 | publishStream: '96a06770-0c77-49ce-a95e-6d846bd41ec8' 477 | }); 478 | 479 | var toDelete; 480 | odag.subscribe( 481 | (val) => { 482 | if (val.status === 'done') { 483 | toDelete = val.val; 484 | } 485 | }, 486 | (err) => done(err), 487 | () => { 488 | promise.all( 489 | toDelete.map((app) => { 490 | return qrsApi.app.id(app).delete() 491 | }) 492 | ) 493 | .then(() => done()) 494 | .fail((err) => done(err)); 495 | } 496 | ); 497 | 498 | }).fail(done); 499 | 500 | }); 501 | 502 | it('should fail when script syntax error', function (done) { 503 | this.timeout(60000); 504 | 505 | var qrsApi = qrs(hooks.params.options); 506 | 507 | var config = { 508 | schema: qixSchema, 509 | Promise: promise.promise, 510 | host: 'https://localhost', 511 | session: { 512 | route: 'app/engineData', 513 | host: 'localhost', 514 | port: hooks.params.options.wsPort, 515 | unsecure: false, 516 | disableCache: true 517 | }, 518 | mixins: [utils.mixins.Global], 519 | listeners: { 520 | "notification:OnAuthenticationInformation": ( authInfo ) => { 521 | console.log( authInfo ); 522 | }}, 523 | createSocket(url) { 524 | var sock = new ws(url, { 525 | ca: [hooks.params.options.ca], 526 | key: hooks.params.options.key, 527 | cert: hooks.params.options.cert, 528 | headers: hooks.params.options.headers, 529 | rejectUnauthorized: false 530 | }); 531 | return sock; 532 | }, 533 | }; 534 | 535 | enigma.getService('qix', config).then((qix) => { 536 | var odag = qix.global.odag(hooks.params.options, { 537 | templateApp: 'a356af1d-0a20-44e0-a152-ade3e56b16b9', 538 | replacesDef: { marker: '%Replace me!%', value: 1, fullRow: true }, 539 | publishStream: '96a06770-0c77-49ce-a95e-6d846bd41ec8' 540 | }); 541 | 542 | odag.subscribe( 543 | (val) => { }, 544 | (err) => { 545 | if (err.status === 'failed' && err.val.msg === 'Application not reloaded') { 546 | qrsApi.app.id(err.val.id).delete() 547 | .then(() => done()) 548 | .fail((err) => { 549 | done(err) 550 | }); 551 | 552 | } else { 553 | done(err); 554 | } 555 | 556 | }, 557 | () => { done('Error'); } 558 | ); 559 | 560 | }).fail(done); 561 | 562 | }); 563 | 564 | it('should authorize simple config 2', function (done) { 565 | this.timeout(30000); 566 | 567 | var qrsApi = qrs(hooks.params.options); 568 | 569 | var config = { 570 | schema: qixSchema, 571 | Promise: promise.promise, 572 | host: 'https://localhost', 573 | session: { 574 | route: 'app/engineData', 575 | host: 'localhost', 576 | port: hooks.params.options.wsPort, 577 | unsecure: false, 578 | disableCache: true 579 | }, 580 | mixins: [utils.mixins.Global], 581 | listeners: { 582 | "notification:OnAuthenticationInformation": ( authInfo ) => { 583 | console.log( authInfo ); 584 | }}, 585 | createSocket(url) { 586 | var sock = new ws(url, { 587 | ca: [hooks.params.options.ca], 588 | key: hooks.params.options.key, 589 | cert: hooks.params.options.cert, 590 | headers: hooks.params.options.headers, 591 | rejectUnauthorized: false 592 | }); 593 | return sock; 594 | }, 595 | }; 596 | 597 | enigma.getService('qix', config).then((qix) => { 598 | 599 | var odag = qix.global.odag(hooks.params.options, { 600 | templateApp: 'a356af1d-0a20-44e0-a152-ade3e56b16b9', 601 | replacesDef: [ 602 | { marker: '%Replace me!%', value: 1 }, 603 | { marker: '%Replace me!%', value: 2 } 604 | ] 605 | }); 606 | 607 | var toDelete; 608 | odag.subscribe( 609 | (val) => { 610 | if (val.status === 'done') { 611 | toDelete = val.val; 612 | } 613 | }, 614 | (err) => done(err), 615 | () => { 616 | promise.all( 617 | toDelete.map((app) => { 618 | return qrsApi.app.id(app).delete() 619 | }) 620 | ) 621 | .then(() => done()) 622 | .fail((err) => done(err)); 623 | } 624 | ); 625 | 626 | }).fail(done); 627 | 628 | }); 629 | 630 | it('should authorize advanced config', function (done) { 631 | this.timeout(30000); 632 | 633 | var qrsApi = qrs(hooks.params.options); 634 | 635 | var config = { 636 | schema: qixSchema, 637 | Promise: promise.promise, 638 | host: 'https://localhost', 639 | session: { 640 | route: 'app/engineData', 641 | host: 'localhost', 642 | port: hooks.params.options.wsPort, 643 | unsecure: false, 644 | disableCache: true 645 | }, 646 | mixins: [utils.mixins.Global], 647 | listeners: { 648 | "notification:OnAuthenticationInformation": ( authInfo ) => { 649 | console.log( authInfo ); 650 | }}, 651 | createSocket(url) { 652 | var sock = new ws(url, { 653 | ca: [hooks.params.options.ca], 654 | key: hooks.params.options.key, 655 | cert: hooks.params.options.cert, 656 | headers: hooks.params.options.headers, 657 | rejectUnauthorized: false 658 | }); 659 | return sock; 660 | }, 661 | }; 662 | 663 | enigma.getService('qix', config).then((qix) => { 664 | 665 | var odag = qix.global.odag(hooks.params.options, { 666 | templateApp: 'a356af1d-0a20-44e0-a152-ade3e56b16b9', 667 | maxParDup: 1, 668 | replacesDef: [{ 669 | replaces: [ 670 | {marker: '%Replace me!%', value: 0}, 671 | {marker: '%Replace me2!%', value: 0} 672 | ], 673 | replaceApp: 'WTF App', 674 | reloadApp: false, 675 | keepApp: true, 676 | fullRow: false 677 | }, { 678 | replaces: [ 679 | {marker: '%Replace me!%', value: [1, 2]}, 680 | {marker: '%Replace me2!%', value: [4, 5]} 681 | ] 682 | }, { 683 | replaces: [ 684 | {marker: '%Replace me!%', value: 6}, 685 | {marker: '%Replace me2!%', value: 7} 686 | ] 687 | }], 688 | publishStream: '96a06770-0c77-49ce-a95e-6d846bd41ec8', 689 | overwriteApp: true, 690 | keepApp: true, 691 | createReloadTask: true, 692 | customProperties: [{name: 'Test', values: ['test %(replaceValue)s']}] 693 | }); 694 | 695 | var toDelete; 696 | odag.subscribe( 697 | (val) => { 698 | if (val.status === 'done') { 699 | toDelete = val.val; 700 | } 701 | }, 702 | (err) => done(err), 703 | () => { 704 | promise.all( 705 | toDelete.map((app) => { 706 | return qrsApi.app.id(app).delete() 707 | }) 708 | ) 709 | .then(() => done()) 710 | .fail((err) => done(err)); 711 | } 712 | ); 713 | 714 | }).fail(done); 715 | 716 | }); 717 | 718 | 719 | }); 720 | 721 | }); 722 | 723 | describe('Doc...', () => { 724 | 725 | it('should be defined', function() { 726 | expect(utils.mixins.Doc).to.not.be.undefined; 727 | }); 728 | 729 | it('qrs', function() { 730 | this.timeout(30000); 731 | 732 | var config = { 733 | schema: qixSchema, 734 | host: 'https://localhost', 735 | session: { 736 | route: 'app/engineData', 737 | host: 'localhost', 738 | port: hooks.params.options.wsPort, 739 | unsecure: false, 740 | disableCache: true 741 | }, 742 | mixins: [utils.mixins.Doc], 743 | listeners: { 744 | "notification:OnAuthenticationInformation": ( authInfo ) => { 745 | console.log( authInfo ); 746 | }}, 747 | createSocket(url) { 748 | var sock = new ws(url, { 749 | ca: [hooks.params.options.ca], 750 | key: hooks.params.options.key, 751 | cert: hooks.params.options.cert, 752 | headers: hooks.params.options.headers, 753 | rejectUnauthorized: false 754 | }); 755 | return sock; 756 | }, 757 | }; 758 | 759 | return enigma.getService('qix', config).then((qix) => { 760 | return qix.global.getDocList().then((docList) => { 761 | return promise.all( 762 | docList.slice(0, 5).map((doc) => { 763 | return qix.global.openDoc(doc.qDocId).then( 764 | // waiting for mixin init to complete 765 | delay(1000) 766 | ).then(app => { 767 | return app.qrs(hooks.params.options).get(); 768 | }); 769 | }) 770 | ); 771 | }); 772 | 773 | }).then((apps) => { 774 | return promise.all([ 775 | expect(apps).to.be.a('array'), 776 | expect(apps[0]).to.be.a('object'), 777 | expect(apps[0]).to.have.property('id'), 778 | expect(apps[0]).to.have.property('stream') 779 | ]) 780 | }); 781 | 782 | 783 | }) 784 | 785 | it('exportCube', function() { 786 | 787 | var config = { 788 | schema: qixSchema, 789 | host: 'https://localhost', 790 | session: { 791 | route: 'app/engineData', 792 | host: 'localhost', 793 | port: hooks.params.options.wsPort, 794 | unsecure: false, 795 | disableCache: true 796 | }, 797 | mixins: [utils.mixins.Doc], 798 | listeners: { 799 | "notification:OnAuthenticationInformation": ( authInfo ) => { 800 | console.log( authInfo ); 801 | }}, 802 | createSocket(url) { 803 | var sock = new ws(url, { 804 | ca: [hooks.params.options.ca], 805 | key: hooks.params.options.key, 806 | cert: hooks.params.options.cert, 807 | headers: hooks.params.options.headers, 808 | rejectUnauthorized: false 809 | }); 810 | return sock; 811 | }, 812 | }; 813 | 814 | return enigma.getService('qix', config).then((qix) => { 815 | return qix.global.getDocList().then((docList) => { 816 | return promise.all( 817 | docList.slice(0, 5).map((doc) => { 818 | return qix.global.openDoc(doc.qDocId).then(app => { 819 | return app.exportCube({ 820 | "qStateName":"$", 821 | "qDimensions":[{ 822 | "qLibraryId":"", 823 | "qNullSuppression":false, 824 | "qDef":{ 825 | "qGrouping":"N", 826 | "qFieldDefs":["$Table"], 827 | "qFieldLabels":[""] 828 | } 829 | }], 830 | "qMeasures":[{ 831 | "qLibraryId":"", 832 | "qDef":{ 833 | "qLabel":"", 834 | "qDescription":"", 835 | "qTags":["tags"], 836 | "qGrouping":"N", 837 | "qDef":"=Count($Field)" 838 | } 839 | }], 840 | "qInitialDataFetch":[{ 841 | "qTop":0, 842 | "qLeft":0, 843 | "qHeight":0, 844 | "qWidth":0 845 | }] 846 | }).toPromise(); 847 | }); 848 | }) 849 | ); 850 | }); 851 | 852 | }).then((counts) => { 853 | return promise.all([ 854 | expect(counts).to.be.a('array'), 855 | expect(counts[0]).to.be.a('array'), 856 | expect(counts[0][0]).to.be.a('array'), 857 | expect(counts[0][0][0]).to.be.a('object'), 858 | expect(counts[0][0][0]).to.have.property('qText'), 859 | expect(counts[0][0][0]).to.have.property('qNum'), 860 | expect(counts[0][0][0]).to.have.property('qElemNumber'), 861 | expect(counts[0][0][0]).to.have.property('qState') 862 | ]) 863 | }) 864 | 865 | }); 866 | 867 | it('export', function() { 868 | 869 | var config = { 870 | schema: qixSchema, 871 | host: 'https://localhost', 872 | session: { 873 | route: 'app/engineData', 874 | host: 'localhost', 875 | port: hooks.params.options.wsPort, 876 | unsecure: false, 877 | disableCache: true 878 | }, 879 | mixins: [utils.mixins.Doc], 880 | listeners: { 881 | "notification:OnAuthenticationInformation": ( authInfo ) => { 882 | console.log( authInfo ); 883 | }}, 884 | createSocket(url) { 885 | var sock = new ws(url, { 886 | ca: [hooks.params.options.ca], 887 | key: hooks.params.options.key, 888 | cert: hooks.params.options.cert, 889 | headers: hooks.params.options.headers, 890 | rejectUnauthorized: false 891 | }); 892 | return sock; 893 | }, 894 | }; 895 | 896 | return enigma.getService('qix', config).then((qix) => { 897 | return qix.global.getDocList().then((docList) => { 898 | return promise.all( 899 | docList.slice(0, 5).map((doc) => { 900 | return qix.global.openDoc(doc.qDocId).then( 901 | //waiting for mixin init to complete 902 | delay(1000) 903 | ).then(app => { 904 | return app.export({ 905 | d1: {name: '$Table', dimensionType: 'FIELD'}, 906 | d2: {name: '$Field', dimensionType: 'AUTO'}, 907 | d3: {name: 'Mouarf', dimensionType: 'IGNORE'} 908 | }, { 909 | m1: {name: 'M1', measureType: 'FIELD', formula: '=COUNT(DISTINCT $Field)'}, 910 | m2: {name: 'M2', measureType: 'AUTO', formula: '=COUNT($Field)'}, 911 | m3: {name: 'M3', measureType: 'IGNORE', formula: '=1'} 912 | }, [{ 913 | field: '$Table', 914 | filters: ['Calendar'] 915 | }]).toPromise(); 916 | }); 917 | }) 918 | ); 919 | }); 920 | 921 | }).then((fields) => { 922 | return promise.all([ 923 | expect(fields).to.be.a('array'), 924 | expect(fields[0]).to.be.a('array'), 925 | expect(fields[0][0]).to.be.a('array').to.have.lengthOf(6) 926 | ]) 927 | }); 928 | 929 | }); 930 | 931 | 932 | }); 933 | 934 | 935 | }); 936 | }) 937 | 938 | 939 | 940 | 941 | 942 | --------------------------------------------------------------------------------