Low-code find-replace in Qlik Sense app load scripts

Manually update app load scripts can be extremely time consuming. Ctrl-Q NR can assist in that use-case by both identifying apps that should be updated, doing a find-replace operation of all apps' load scripts, and finally reloading the apps.

Low-code find-replace in Qlik Sense app load scripts
Bulk find-replace in Qlik Sense app load scripts.

Ctrl-Q NR has a couple of new features in the qseow-app node:

Getting and setting load scripts in client-managed Qlik Sense apps.

This may at first not seem like a big use case, but it does have potential to be a real time-saver.

Let's say you have 50 Sense apps whose load scripts all use a certain data connection.
Let's also assume that data connection has been replaced with another one.

As always there are various way of approaching a problem.

You can edit the scripts of 50 apps manually, update the existing connection's definition (may not be ideal if its name is not suitable any more, for example) or you can automate the whole thing.

Let's look at the automation option.

⚠️
A friendly reminder and warning:

Any Ctrl-Q NR node that create, update or delete information in Qlik Sense has the potential to be dangerous.

Given the power of Node-RED it is quite possible to create flows that happily create/update/delete things in Sense before you realise what happened.

It's always a good idea to first verify that the input messages to the various Ctrl-Q NR nodes contain the correct info, then connect the Ctrl-Q NR nodes to the flow. That way you minimise the risk of problems.

The concept shown in currently available for client-managed Sense, but it can easily be implemented for Qlik Sense Cloud too.
Feel free to fork the GitHub repository and contribute to the qscloud-app node!

Structure of the demo

To better explain the concept some context is required:

  • There are two apps called "Ctrl-Q NR script replace demo 1" and "Ctrl-Q NR script replace demo 2".
  • Both apps are tagged with the tag "Ctrl-Q NR replace script demo".
  • Their load scripts are very basic, they both just define a variable called vText:
    let vText = 'This is version 1';
  • We would like to replace "version 1" with "version 2" using Node-RED and Ctrl-Q NR.

Solution

All right, we now have a foundation to work from.
The general concept of the flow below is as follows:

  1. Do some initial setup of data needed for the flow (for example which text to look for, and which text to replace with).
  2. Look up all app ids for apps tagged with "Ctrl-Q NR replace script demo". This will be an array of app ids.
  3. Split the single message coming from the lookup node into separate messages for each app.
  4. Wait for 3 seconds to avoid overloading the Sense server. Would actually be good to wait add a random delay here, to really spread the calls to the Sense API.
  5. Get the app's load script
  6. Do a find-replace on the load script
  7. Write the load script back to the app. Save the app, preserving any data already in the app.
  8. Join the messages from step 4-7 into a single message.
  9. Start the reload tasks associated with the two apps.
  10. Output some info about which and how many apps that were modified.

If you examine the flow you will see that the two reload task ids are hard coded in the qseow-task node. That's an ugly workaround for the fact that Ctrl-Q NR does not yet have a task lookup operation in qseow-task.

Similar to how that operation works in qseow-app, the task ditto would look up task ids given tags, app ids etc. Then the whole flow would be fully generic.
It would be an educated guess that this shortcoming may be adressed in a not too distant future...

Find and replace text in multiple Qlik Sense apps (client-managed).

The effect is probably easiest to understand by watching the video below.
It shows how the apps are automatically modified and also reloaded to include the updated load script.

Finally, below is the flow's JSON that can be imported into Node-RED.
Ctrl-Q NR 0.2.6 or later is required.

[{"id":"99a5cbd63b9d0f3a","type":"comment","z":"fef7656668dec91e","name":"Bulk replace of parts of load scripts (client-managed)","info":"","x":210,"y":40,"wires":[]},{"id":"253f65ec5f8e1854","type":"qseow-app","z":"fef7656668dec91e","name":"Tag name > app IDs","server":"5e42d0533190acfb","op":"app-id-lookup","appId":"","appSource1":"msg-in","appSource2":"msg-in","x":180,"y":220,"wires":[["fda26074343d8830","f93a0c315df7aaff"]]},{"id":"fda26074343d8830","type":"debug","z":"fef7656668dec91e","name":"Apps that will be updated","active":true,"tosidebar":false,"console":false,"tostatus":true,"complete":"payload.appId.length","targetType":"msg","statusVal":"payload","statusType":"auto","x":430,"y":180,"wires":[]},{"id":"f65b8e79d898d358","type":"split","z":"fef7656668dec91e","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":650,"y":220,"wires":[["6be56d78ac262b54"]]},{"id":"f93a0c315df7aaff","type":"change","z":"fef7656668dec91e","name":"Prep for message split","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.appId","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":460,"y":220,"wires":[["f65b8e79d898d358"]]},{"id":"430622394e73c081","type":"inject","z":"fef7656668dec91e","name":"Replace v1 with v2","props":[{"p":"oldText","v":"This is version 1","vt":"str"},{"p":"newText","v":"This is version 2","vt":"str"},{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"tagName\":[\"Ctrl-Q NR replace script demo\"]}","payloadType":"json","x":150,"y":100,"wires":[["d6089529e6a69e68"]]},{"id":"d6089529e6a69e68","type":"change","z":"fef7656668dec91e","name":"Set up for text find-replace","rules":[{"t":"set","p":"oldText","pt":"flow","to":"oldText","tot":"msg"},{"t":"set","p":"newText","pt":"flow","to":"newText","tot":"msg"},{"t":"set","p":"payload.started.reloadTask.length","pt":"msg","to":"0","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":400,"y":100,"wires":[["253f65ec5f8e1854","6ad094a5b61ef4d1"]]},{"id":"cb3fa0ab374e3f96","type":"inject","z":"fef7656668dec91e","name":"Replace v2 with v1","props":[{"p":"oldText","v":"This is version 2","vt":"str"},{"p":"newText","v":"This is version 1","vt":"str"},{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"tagName\":[\"Ctrl-Q NR replace script demo\"]}","payloadType":"json","x":150,"y":140,"wires":[["d6089529e6a69e68"]]},{"id":"e470fbdaac05462b","type":"qseow-task","z":"fef7656668dec91e","name":"","server":"5e42d0533190acfb","op":"start","taskId":"b6f76e31-3b75-42fa-9113-141406ddd931\n3eba4c7f-bad8-4fba-bc30-0b695567fd01","taskSource1":"predefined","x":390,"y":500,"wires":[["eb8111d3ff8a4d58","4d85513ca08ef8ea"]]},{"id":"eb8111d3ff8a4d58","type":"debug","z":"fef7656668dec91e","name":"Reload task result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":610,"y":500,"wires":[]},{"id":"4fe4e8af08370614","type":"delay","z":"fef7656668dec91e","name":"","pauseType":"delay","timeout":"3","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":230,"y":500,"wires":[["e470fbdaac05462b"]]},{"id":"b3f38b69c36c8b49","type":"join","z":"fef7656668dec91e","name":"","mode":"auto","build":"object","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":"false","timeout":"","count":"","reduceRight":false,"x":100,"y":500,"wires":[["4fe4e8af08370614"]]},{"id":"4d85513ca08ef8ea","type":"debug","z":"fef7656668dec91e","name":"Number of reload tasks started","active":true,"tosidebar":false,"console":false,"tostatus":true,"complete":"payload.started.reloadTask.length","targetType":"msg","statusVal":"payload","statusType":"auto","x":650,"y":540,"wires":[]},{"id":"6ad094a5b61ef4d1","type":"link out","z":"fef7656668dec91e","name":"link out 1","mode":"link","links":["1dfcf2347a518485"],"x":595,"y":100,"wires":[]},{"id":"1dfcf2347a518485","type":"link in","z":"fef7656668dec91e","name":"link in 1","links":["6ad094a5b61ef4d1"],"x":465,"y":540,"wires":[["4d85513ca08ef8ea"]]},{"id":"5074ebda15994733","type":"group","z":"fef7656668dec91e","name":"One message per app","style":{"label":true},"nodes":["1a6afb99cb254e51","6be56d78ac262b54","678478eb06030e56","64dd5d004b583081","a8f950f8a94d663b","17f65b52d95da6e2"],"x":34,"y":279,"w":732,"h":162},{"id":"1a6afb99cb254e51","type":"qseow-app","z":"fef7656668dec91e","g":"5074ebda15994733","name":"Get load script","server":"5e42d0533190acfb","op":"get-script","appId":"","appSource1":"msg-in","appSource2":"msg-in","x":660,"y":320,"wires":[["64dd5d004b583081"]]},{"id":"6be56d78ac262b54","type":"change","z":"fef7656668dec91e","g":"5074ebda15994733","name":"Prep for text replace","rules":[{"t":"set","p":"appId","pt":"msg","to":"payload","tot":"msg"},{"t":"set","p":"payload","pt":"msg","to":"{\"app\":[]}","tot":"json"},{"t":"set","p":"payload.app[0].id","pt":"msg","to":"appId","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":320,"y":320,"wires":[["678478eb06030e56"]]},{"id":"678478eb06030e56","type":"delay","z":"fef7656668dec91e","g":"5074ebda15994733","name":"","pauseType":"delay","timeout":"3","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":500,"y":320,"wires":[["1a6afb99cb254e51"]]},{"id":"64dd5d004b583081","type":"function","z":"fef7656668dec91e","g":"5074ebda15994733","name":"Replace old script with new","func":"let oldText = flow.get(\"oldText\");\nlet newText = flow.get(\"newText\");\n\n\nnode.log('oldText:' + oldText);\nnode.log('newText:' + newText);\n\nlet origScript = msg.payload.app[0].script;\nvar regex = new RegExp(oldText, \"g\");\nmsg.payload.app[0].script = origScript.replace(regex, newText)\n\n// msg.payload.app[0].script = msg.payload.app[0].script.replace('/' + oldText + '/', newText);\n//msg.payload.app[0].script = msg.payload.app[0].script.replace(/This is version 1/, 'This is version 2');\n\n\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":180,"y":400,"wires":[["17f65b52d95da6e2"]]},{"id":"a8f950f8a94d663b","type":"qseow-app","z":"fef7656668dec91e","g":"5074ebda15994733","name":"Update app with new script","server":"5e42d0533190acfb","op":"set-script","appId":"","appSource1":"msg-in","appSource2":"msg-in","x":620,"y":400,"wires":[["b3f38b69c36c8b49"]]},{"id":"17f65b52d95da6e2","type":"delay","z":"fef7656668dec91e","g":"5074ebda15994733","name":"","pauseType":"delay","timeout":"3","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":410,"y":400,"wires":[["a8f950f8a94d663b"]]},{"id":"5e42d0533190acfb","type":"qseow-sense-server","name":"Dummy Qlik Sense server","authType":"cert","certFile":"c:\\secret\\client.pem","keyFile":"c:\\secret\\client_key.pem","certCaFile":"","jwt":""}]